PHP port of charmbracelet/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.
ββ wishlist ββ
filter:
βΈ production β deploy@prod.example.com:2222
staging β stage.example.com
dev β dev.example.com
β/β select Β· Enter connect Β· Esc quit Β· type to filter
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/wishlistwishlist looks for, in order:
--config <path>(CLI flag)~/.config/wishlist.yml~/.config/wishlist.yaml~/.config/wishlist.jsonwishlist.{yml,yaml,json}in the current directory
- 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[
{ "name": "production", "host": "prod.example.com", "port": 2222, "user": "deploy" },
{ "name": "staging", "host": "stage.example.com" }
]| Key | Action |
|---|---|
| β / k | Move up |
| β / j | Move down |
| Enter | Connect to highlighted endpoint |
| Esc / ^C | Quit without connecting |
| (typing) | Type-to-filter; Backspace clears |
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.
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);
}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 |
Host * global defaults are inherited by all subsequent host blocks. Host patterns are used as the endpoint name (when no HostName is specified, the pattern itself becomes the host).
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');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 grapheme clusters) for ranked, highlighted filter results.
Phase 10.28 β SSH config import. 69 tests / 176 assertions. Endpoint, Config (JSON + flat-YAML + SSH config), Picker, Launcher, SshConfigParser are all covered.
