sugarcraft / sugar-wishlist
SSH endpoint launcher — a TUI directory of `ssh user@host` shortcuts. Port of charmbracelet/wishlist. Loads endpoints from YAML/JSON, renders an interactive picker, then `exec`s into the chosen ssh client (with full host-key prompt / agent forwarding fidelity).
Requires
- php: >=8.3
- ext-pcntl: *
- sugarcraft/candy-core: dev-master
- sugarcraft/candy-fuzzy: dev-master
Requires (Dev)
- phpunit/phpunit: ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-03 15:34:12 UTC
README
SugarWishlist
A PHP take on the concept of charmbracelet/wishlist (Charmed's SSH host directory, itself inspired by Charlie Gleason's original wishlist) — a TUI directory of SSH endpoints. Launch wishlist, pick a host, hit Enter, and the current process is replaced with ssh connecting to it.
This is a re-implementation, not a format-compatible port: the config schema here is a flat top-level list of endpoint objects. Upstream's nested host:-keyed wishlist.yml is deliberately not accepted — feeding one in fails loudly with wishlist yaml: unparseable line, never silently.
── wishlist ──
filter:
▸ production ─ deploy@prod.example.com:2222
staging ─ stage.example.com
dev ─ dev.example.com
↑/↓ select · Enter connect · Esc quit · type to filter
Install
The wishlist binary lives at bin/wishlist. Composer adds it to your global vendor/bin/ when installed as a project dependency, or you can add the repo's bin/ to your $PATH.
composer require sugarcraft/sugar-wishlist
~/.composer/vendor/bin/wishlist
Configure
wishlist resolves its config in this order:
--config <path>(CLI flag) — wins outrightwishlist.yml/wishlist.yaml/wishlist.jsonin the current directory (first that exists)~/.config/wishlist.yml/.yaml/.json(only when$HOMEis set)
So a config in the directory you launch from takes precedence over your home config. Other flags:
--ssh <binary>— absolute path to the ssh executable (default/usr/bin/ssh).pcntl_execdoes not search$PATH, so a baresshwill be rejected; the path must exist and be executable.--help— print the usage line and exit 0. Unrecognised--flagsexit 2 naming the offending arg.
YAML
- name: production host: prod.example.com port: 2222 user: deploy identity_file: ~/.ssh/prod-deploy - name: staging host: stage.example.com user: deploy - name: jumpbox host: bastion.example.com options: - ServerAliveInterval=30 - ProxyJump=gw.example.com
JSON
[
{ "name": "production", "host": "prod.example.com", "port": 2222, "user": "deploy" },
{ "name": "staging", "host": "stage.example.com" }
]
Keybindings
| Key | Action |
|---|---|
| ↑ / k | Move up |
| ↓ / j | Move down |
| Enter | Connect to highlighted endpoint |
| Esc / ^C | Quit without connecting |
| (typing) | Type-to-filter; Backspace clears |
Implementation
The picker is a tiny standalone widget — not a full SugarBits List. The lifecycle is
read config → render picker → read keys → choose → pcntl_exec(ssh, argv)
That last pcntl_exec is the critical line: it replaces the PHP process with ssh. File descriptors, environment, and the controlling tty all flow through unchanged, so the user sees a normal ssh session — host-key prompts, agent forwarding, MOTD, exit status, all native. We never proxy bytes; we get out of the way.
Import from SSH Config
wishlist can import endpoints directly from your OpenSSH config file (~/.ssh/config):
use SugarCraft\Wishlist\Config; $endpoints = Config::importFromSshConfig('/home/user/.ssh/config');
The parser handles:
| SSH Config Key | Endpoint Field |
|---|---|
Host <pattern> |
name |
HostName <value> |
host |
User <value> |
user |
Port <value> |
port |
IdentityFile <path> |
identityFiles[] |
ProxyJump <host> |
proxyJump |
Precedence follows ssh_config(5): for each parameter, the first obtained value will be used. A Host * block wins only where it appears before any matching specific block — conventionally it is written last and therefore acts as a fallback. IdentityFile is the documented exception and accumulates in file order. Host patterns are used as the endpoint name (when no HostName is specified, the pattern itself becomes the host).
Programmatic use
use SugarCraft\Wishlist\Config; use SugarCraft\Wishlist\Picker; use SugarCraft\Wishlist\Launcher; $endpoints = Config::load('/etc/wishlist.yml'); $picked = (new Picker())->pick($endpoints); if ($picked !== null) { (new Launcher())->dispatch($picked); } // Or import from SSH config: $sshEndpoints = Config::importFromSshConfig('/home/user/.ssh/config');
Shared foundations
sugar-wishlist uses candy-fuzzy — SmithWatermanMatcher::matchAll() replaces ad-hoc str_contains-style filtering. The picker now surfaces scored ranking and match-highlight indices (ANSI bold+cyan on matched characters) for ranked, highlighted filter results.
Status
Phase 10.28 — SSH config import. 257 tests / 1160 assertions. Endpoint, Config (JSON + flat-YAML + SSH config), Picker, Launcher, SshConfigParser are all covered.
