8af44dfcd2
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>
212 lines
11 KiB
Markdown
212 lines
11 KiB
Markdown
# 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.
|