Initial commit: dial — interactive SSH host picker

A fast, read-only ~/.ssh/config picker that exec's the system ssh client.
Includes: static high-contrast picker with recency/frequency/name sort and
tag grouping, `dial keygen` (native ed25519/RSA + optional config entry), and
an optional offline YubiKey launch gate with one-time recovery codes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:18:23 +10:00
commit 8af44dfcd2
31 changed files with 4219 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
# dial — v2 Plan
Written against the v1 REPORT.md handoff. v1 is complete, tested, and out of
scope to modify except where noted. This plan is scoped to be handed to
Claude Code as-is; every judgment call is flagged so it can be overridden.
---
## 1. Alphabetical sort (small, do first)
- Add a third sort mode to the existing `s` toggle: **recently used → most
used → alphabetical → (back to recently used)**.
- Alphabetical sorts by `Host` alias (the thing the user actually types/sees
first in the list), case-insensitive.
- Favourites still pin to the top regardless of sort mode (unchanged v1
behaviour).
- Persist the selected mode the same way recents/frequency already persist
(`~/.ssh/.dial/data.json`).
No open questions here — this is a straightforward extension of existing code.
---
## 2. SSH key generation tool (`dial keygen`)
Rebuilt from the logic in `create_ssh_keypair.sh`, fixing the cross-platform
and UX issues found in it, and wired into `dial` as a new subcommand rather
than folded into the interactive picker.
**Why a separate subcommand, not part of the picker:** the picker's core
read-only-config guarantee is a deliberate v1 property. Keygen necessarily
*writes* to `~/.ssh/config` and creates key files, so it should be a distinct,
explicitly-invoked action (`dial keygen`) — not something that can happen by
accidentally pressing a key while browsing hosts.
### Fixes over the original script
- Replace the macOS-only `sed -i ''` public-key-comment logic with something
that works identically on macOS/Linux/Windows (Go string handling, no shell
`sed` dependency at all).
- Default algorithm: **ed25519**. Offer `--rsa` (4096-bit) as an explicit
opt-out for hosts that don't support ed25519 (some older network gear, e.g.
legacy broadcast equipment, may not).
- **Keep the `.pub` file.** Don't delete it after displaying it once — that
behaviour in the original script means a lost copy-paste = a wasted key.
Still print it to the terminal for convenience, but leave the file in place.
- Proper quoting/escaping when appending to `~/.ssh/config` (avoid the blind
`read` + string-concat approach in the original).
### Proposed flow
```
dial keygen
→ prompt: key name / label
→ prompt: algorithm (default ed25519, flag for rsa)
→ generate into ~/.ssh/<label> (and .pub alongside, kept)
→ prompt: add a Host entry to ~/.ssh/config now? (y/n)
→ if yes: Host alias / HostName / User / Port (default 22) / tag (optional, using existing `# tag:` convention)
→ print the public key once for easy copy, key file left on disk
```
**Assumption flagged:** this reuses `dial`'s existing static-list, low-light-
friendly prompt styling rather than plain unstyled `read` prompts like the
original script, for consistency with the rest of the tool.
---
## 3. Remaining v2 features (from REPORT.md open questions)
Going through the report's nine open questions individually, with a decision
made and flagged for each so this can go straight to Claude Code.
### 3.1 SFTP / file transfer mode — **in scope for v2: full in-app browser**
- After selecting a host, a second action (`t` for transfer, alongside `enter`
to shell in) opens an **in-app SFTP file browser** — not a shell-out to the
system `sftp` binary.
- Built directly against a Go SFTP library (`pkg/sftp` + `golang.org/x/crypto/
ssh` for the underlying connection) rather than shelling out, so dial owns
the whole UI.
- **Core browser UX** (following the same static, high-contrast, low-light-
friendly list styling as the host picker — no cramped/dense text):
- Split view or toggle-view of local vs. remote directory listing.
- Arrow-key navigation through directories on both sides; `enter` on a
directory descends into it, a clear "up one level" entry at the top of
each listing.
- **Batch selection:** a keypress (e.g. `space`) toggles selection on the
highlighted entry without moving the cursor, same pattern as v1's `f`
favourite toggle. Multiple files (and/or directories) can be selected
before triggering a push/pull.
- Push (local → remote) or pull (remote → local) on the current selection —
a single file, multiple files, a single directory, or a mix.
- **Directory transfers are recursive**, walking the full tree on the
source side and recreating structure on the destination side.
- Progress UI: per-item progress plus an aggregate "3 of 12 files, 40% of
total" view when a batch/directory transfer is in flight — legible at the
same size/contrast as the rest of the UI.
- Clear error states: connection drop mid-transfer, permission denied, disk
full, file already exists (prompt overwrite/skip/rename rather than
silently clobbering) — applies per-item within a batch, with an option to
apply the same choice to all remaining conflicts rather than prompting
per file.
- **Resume on failure: basic, same-session only.** If a transfer (or a batch/
directory transfer) is interrupted — network drop, connection reset — and
retried within the same `dial` session, dial seeks both the local and
remote file to the last confirmed byte offset and continues rather than
restarting from zero. This applies per-file within a batch/directory
transfer (finished files stay finished; only the interrupted one resumes).
- **Explicitly not included:** resume that survives closing/restarting
`dial` or the machine. That requires persisting transfer state to disk and
verifying partial files weren't corrupted before trusting them — a
meaningfully bigger, separate piece of work. Flag if this turns out to
matter in practice (e.g. regularly moving very large files where a lost
session is costly) and it can be scoped as a v2.x follow-up.
- Reuses the connection details already resolved from `~/.ssh/config` for the
selected host (same auth, same `ProxyJump` if present) — no separate
credential entry.
- **Gate coverage:** as already decided in §3.4, the YubiKey gate (when
enabled) covers entry into this mode too, not just app launch.
### 3.2 How to pick the action (shell vs SFTP) — **decided**
- Second keypress on the highlighted host, not a separate menu: `enter` = ssh,
`t` = sftp. Keeps the static-list simplicity intact rather than adding a
submenu/modal.
### 3.3 Distribution/trust (signing, Homebrew, Scoop, CI) — **deferred, not v2**
- This is packaging/ops work, not a feature, and doesn't block daily use on
your own machines via Syncthing + manual install. Flagging as a separate
future task rather than bundling into this feature-focused v2 pass.
### 3.4 Gate ergonomics — **small change: gate now also covers SFTP**
- Every-launch touch stays as-is (no idle-timeout added — the report notes
this as a "worth adding later" maybe, not a v2 must-have).
**Assumption flagged:** leaving idle-timeout out of v2; say the word if you
want it added now instead.
- The YubiKey gate (when enabled) now also gates the new `t` (SFTP) action,
not just app launch, since it's a second way to move data off/onto a host.
### 3.5 Recovery-code UX — **unchanged**
- Current design (terminal-only, shown once, single-use, auto-rotates) stays
as-is. No changes requested and the report doesn't flag it as a problem.
### 3.6 Richer metadata (tag grouping/colour-coding) — **in scope, small**
- Add optional **section headers by tag** in the static list when sorted
alphabetically or by tag (new sort mode candidate — see note below).
- Colour-code tag pills using the existing `internal/theme` high-contrast
palette (e.g. a distinct colour per tag), not free-form colour — keeps the
low-light-readable requirement intact.
- **Open question for you, not decided here:** should "group by tag" be its
own 4th+ sort mode, or a toggle independent of sort? Recommend: independent
toggle (`g` to group), since it's orthogonal to alphabetical/recent/most-used
ordering. Flagging for Claude Code to confirm with you if ambiguous during
build.
### 3.7 Search behaviour (fuzzy vs substring) — **kept as substring, not changed**
- Report explicitly chose substring to honour the "readable, not cramped"
ethos. No reason surfaced in this conversation to revisit — leaving as-is
for v2.
### 3.8 Multi-select / batch actions across hosts — **deferred, flagged as scope creep**
- Note: this is distinct from the batch *file* selection now included in the
SFTP browser (§3.1), which is about selecting multiple files/folders within
one host's transfer session. This item is about running a command or
transfer across *multiple hosts* at once — the report itself raises this as
possible scope creep. Recommend leaving it out of v2 — that's a meaningfully
different tool shape (more like an orchestration tool than a picker) and
deserves its own dedicated design pass if wanted later, not a bolt-on here.
### 3.9 Connection history/analytics beyond recents — **small addition**
- Add a **last-connected timestamp** shown in the preview pane (already
tracking recency internally for sort — this just surfaces it visually).
- Per-host notes: **deferred** — no clear use case surfaced yet; easy to add
later without restructuring anything, so not worth the effort now.
---
## v2 Scope Summary
**Building now:**
1. Alphabetical sort mode
2. `dial keygen` subcommand (cross-platform, ed25519-default, config-append)
3. In-app SFTP file browser (`t` to open, `pkg/sftp`-based — local/remote
listings, batch file/folder selection, recursive directory transfers,
same-session resume-on-failure, aggregate progress, overwrite prompts)
4. Tag grouping toggle + colour-coded tag pills
5. YubiKey gate extended to cover the new SFTP browser
6. Last-connected timestamp in preview pane
**Explicitly deferred (not this pass):**
- Resume that survives closing/restarting dial (crash-persistent resume)
- Idle-timeout gate option
- Distribution/signing/CI/package managers
- Multi-select/batch actions *across hosts* (§3.8) — distinct from in-browser
batch file selection, which is now in scope
- Per-host notes
- Fuzzy search
---
## Notes for Claude Code
- The SFTP browser is a genuinely new subsystem (`internal/sftp` or similar),
not a reuse of `internal/connect`'s exec-handover pattern — it needs its own
connection lifecycle via `pkg/sftp`/`golang.org/x/crypto/ssh` since dial
stays in the foreground driving the UI, rather than handing off the process
like the SSH connect flow does.
- `dial keygen` is a new top-level subcommand alongside the existing
`yubikey enable/disable/status/recovery`, `version`, `help` — not part of
the TUI picker's keybindings.
- Confirm with the user during implementation: tag-grouping as toggle vs sort
mode (§3.6), if it's not obvious once in the code.
- Given the SFTP browser is now the largest single piece of v2, worth
sequencing it last (after alphabetical sort, keygen, tag grouping, last-
connected timestamp) so the smaller wins land first and the browser gets
full attention/testing time.