feat: add shell completion (#132) - #134
Conversation
…ing a script (eqrm#132) `ct completion zsh|bash|fish` prints a ~15-line hook that hands the command line back to `ct` on every Tab; the candidates are computed live from the Commander tree of the binary that is actually installed. The per-shell dialect (zsh `compdef`/`compadd`, bash `complete`/`compgen`, fish `complete -a`) comes from omelette, so this repo owns no shell syntax of its own and nothing has to be re-emitted when a command is added. Delegating at runtime is what makes dynamic candidates possible at all — a generated script structurally cannot know them: - `--env` completes the environments this config repo declares in `ct.envs.json` - `ct state rm <type> <key>` completes the registry's types and the keys actually under management, following the `--env`/`--state` already on the line - path-taking options (`--config`, `--state`, `--backup-dir`) complete from disk Completion is offline by construction: it reads local, non-secret files and nothing else — no client, no token store, no `prepareEnv`. Every source is wrapped so a missing, malformed or slow file yields no candidates rather than an error or a hang; a completion that spills a stack trace into the command line is worse than one that offers nothing. omelette over `@pnpm/tabtab`: tabtab reads its hook templates off disk at runtime, which in the `bun build --compile` binaries this project releases resolves to the *build machine's* `node_modules` and fails with ENOENT on every user's machine (verified). omelette inlines its hooks as strings and has no dependencies, so the same code path works from npm, from `dist/`, and from a standalone binary with no `node_modules` in sight. Claude-Session: https://claude.ai/code/session_018XbTXWQnBB5rgbXwJYRFHM
e78e8f4 to
3555d9f
Compare
|
Hey, ich habe hier direkt auf deinem Branch umgebaut statt lange zu kommentieren — sag Bescheid, wenn dir das zu weit ging. Dein ursprünglicher Commit Was mich am generierten Skript gestört hat Von den 338 Zeilen in Jetzt gibt Warum omelette und nicht tabtab ist die bessere Bibliothek, fällt hier aber aus einem Grund raus, der genau unsere Release-Pipeline trifft: Sie lädt ihre Hook-Templates zur Laufzeit von der Platte ( Preis dafür: keine Beschreibungstexte an den Kandidaten in zsh (omelette nutzt Und ja, omelette ist praktisch eingefroren — letztes Release September 2021, letzter Commit Januar 2022. Bewusst in Kauf genommen: ~350 Zeilen MIT, ohne Dependencies, deren ganzer Job das Ausgeben von drei statischen Hook-Strings ist. Der realistische Ausfallmodus ist "bekommt nie ein Feature", nicht "geht kaputt", und im Notfall ist Vendoring ein Nachmittag. Die Begründung steht im Code, nicht nur hier. Kleinigkeiten: Die fish habe ich installiert und die Completion dort auch wirklich durchgetrieben, nicht nur Schau bitte drüber, ob du das so mittragen kannst — vor allem beim Verzicht auf die Beschreibungstexte hätte ich gern deine Meinung. |
…nd stock bash Review of eqrm#134 turned up six ways the completion answers the wrong thing, or nothing at all: - `splitCompletionLine` sliced the last token off the line rather than the token the cursor is on, so Tab anywhere but at the end of the line completed the wrong word. Split at `fragment` instead, keeping the old shape-of-the-line behaviour as the fallback for the one case it is still needed: bash derives the index from `COMP_CWORD`, whose word breaks the hook's colon fudge does not fully account for, so it can arrive past the end of the line. - `statePathFor` hardcoded the `ct-state.<env>.json` convention and ignored a profile's own `state` field, which `prepareEnv` does honour — so in any repo that overrides it, `ct state rm --env <e> <Tab>` offered nothing at all. Read the field offline, with the same precedence the commands use. - `state rm` key candidates were not narrowed by the `<type>` already typed, so completion happily offered a key the command then refuses. - omelette's bash branch calls two helpers from the `bash-completion` package. Stock macOS bash 3.2 — the one the README's `~/.bash_profile` line gets you — has neither, so every Tab printed two "command not found" lines. Prepend minimal stand-ins, defined only when the real ones are absent. - `paths()` never expanded `~`, and the hooks turn off the shell's own filename fallback, so a tilde path completed to nothing. - `ct adopt <type>` takes the same registry-typed argument as `ct state rm <type>` but had no entry in the dynamic table. Claude-Session: https://claude.ai/code/session_018XbTXWQnBB5rgbXwJYRFHM
Tab completion by calling
ctback, not by generating a scriptReworked from the generated-script approach.
ct completion zsh|bash|fishnow prints a ~15-line hook that hands the command line back tocton every Tab; candidates are computed live from the Commander tree of the binary that is actually installed. This repo owns no shell syntax — the zsh/bash/fish dialects come from omelette.Why not a generated script. It has to be re-emitted whenever a command changes, it needs a hand-written emitter per shell (232 of the previous 338 lines were exactly that), and it structurally cannot offer candidates that depend on the user's files.
What the delegation buys.
--envcompletes the environments this config repo declares inct.envs.jsonct state rm <type> <key>completes registry types and the keys actually under management, following the--env/--statealready on the line--config,--state,--backup-dircomplete from the filesystemCompletion is offline by construction. It reads local, non-secret files and nothing else — no client, no token store, no
prepareEnv. Every source is wrapped so a missing, malformed or slow file yields no candidates rather than an error or a hang: a completion that spills a stack trace into the command line is worse than one that offers nothing. The test suite mocks the session module to throw, so any route to the network fails CI.Why omelette over
@pnpm/tabtab. tabtab is the better-featured library, but it reads its hook templates off disk at runtime (path.join(__dirname, 'templates', …)). In thebun build --compilebinaries this project releases,__dirnameresolves to the build machine'snode_modules, soct completion zshworks on the CI runner and fails with ENOENT on every user's machine — verified by compiling a probe and removingnode_modules. Our smoke tests would not have caught it, because they run on a checkout that hasnode_modules. omelette inlines its hooks as strings and has zero dependencies, so the same code path works from npm, fromdist/, and from a standalone binary with nonode_modulesin sight.The trade-off is no per-candidate description text in zsh: omelette uses
compadd, tabtab would use_describe. Working standalone binaries win that trade.omelette's maintenance status, accepted deliberately. Last release 0.4.17 in September 2021, last commit January 2022 — effectively frozen. Accepted because it is ~350 lines of MIT-licensed, dependency-free CommonJS whose entire job is emitting three static hook strings, so the realistic failure mode is "never gains a feature", not "breaks". If it ever does break, vendoring it is an afternoon's work and the licence allows it. The reasoning is recorded in
src/completion/shell.tsrather than only here.<shell>stays an explicit required argument. zsh and bash receive the same hook (it branches oncompdef/completeitself); only fish differs. Detecting the shell from$SHELLwould be unreliable and wrong whenever someone generates for a different shell on purpose.Also: README install docs rewritten for the hook-based flow. The
/reference/and/reports/.gitignorelines are dropped — they belong to #133.Verification
npm test— 828 passed, 5 skippednpm run typecheck,npm run lint,npm run format:check,npm run buildnode .github/scripts/docs-staleness.mjs— all pages currentbash -n,zsh -nandfish -n(fish 4.8.1)ct plan --env <Tab>→dev prod,ct state rm --env dev campus <Tab>→ the managed keysbun --compilebinary run outside anynode_modulesCloses #132