feat(cli): codegraph completions for zsh/bash/fish/powershell with auto-detected install paths#263
Open
HelgeSverre wants to merge 6 commits into
Open
feat(cli): codegraph completions for zsh/bash/fish/powershell with auto-detected install paths#263HelgeSverre wants to merge 6 commits into
HelgeSverre wants to merge 6 commits into
Conversation
Currently codegraph has no Tab-completion: users typing
`codegraph i<TAB>` get nothing. Hand-written emitters walk
commander's command/option/argument tree and produce a static script
per shell, mirroring the approach used by sema-lisp (clap_complete)
and fedit (System.CommandLine hand-rolled).
- `codegraph completions <zsh|bash|fish>` prints the script to stdout
- `--install` writes to the standard per-shell location:
zsh → ~/.zsh/completions/_codegraph
bash → ~/.local/share/bash-completion/completions/codegraph
fish → ~/.config/fish/completions/codegraph.fish
- Argument-name heuristic infers file/directory hints (positional
named `path` → _files; option value `<path>` → file completion)
- Aliases are routed to the canonical command function so e.g.
`codegraph plugin<TAB>` works the same as `codegraph plugins<TAB>`
(no aliases today, but the dispatcher is alias-aware)
- Zero new runtime dependencies — uses commander's existing
introspection API (`cmd.options`, `cmd.registeredArguments`,
`cmd.commands`)
PowerShell and elvish deferred — minimal demand for codegraph's
audience; can be added by dropping in another emitter under
src/completions/.
Tests: 16 structural assertions covering shell parsing, install
paths, per-shell output shape, and the alias-dispatch / value-hint
logic. Snapshot tests intentionally avoided since they break on
every CLI description tweak.
The 16 vitest tests in __tests__/completions.test.ts pin substrings
in the generated script but don't prove that the script actually
works once installed. A malformed _arguments spec parses fine,
autoloads fine, and silently produces zero completions — pure-text
assertions miss that whole class of bug.
This adds an opt-in smoke harness that:
- npm pack's the actual published artifact (catches packaging regressions)
- builds a pinned Node 22 + zsh + bash + fish container
- installs the tarball + runs `codegraph completions <shell> --install`
- drives real shell-completion machinery and asserts content:
- bash: sources the file, verifies `complete -F _codegraph codegraph`
registered the function, then drives COMPREPLY assertions
for top-level commands, subcommand flags, --path file hints,
and global --help/--version
- fish: uses `complete -C "codegraph …"` (fish exposes the full
completion path non-interactively) and asserts stdout
- zsh: structural — script parses (`zsh -n`), is registered by
compinit (`whence -w _codegraph`), and a cross-section of
per-subcommand helpers is defined after autoload
zsh content-testing is intentionally NOT done. `_values` and
`_arguments` require `_main_complete` running under a real ZLE widget
context (compstate, state, opt_args); scripts can't manufacture that,
so a `compadd` shim captures nothing — verified by a failed canary
attempt. PTY+expect is the only way to drive the full path and is too
flaky across zsh 5.7/5.8/5.9 for CI. Industry standard (clap_complete,
oclif, click, Commander.js itself) is structural-only for zsh; we
match that bar.
Isolation:
- Wired only via `npm run smoke:completions`. Not in `npm test`.
- vitest.config.ts unchanged (smoke count: 0).
- docker/ is excluded from npm pack by the existing `files:` allowlist
["dist","scripts","README.md"], so this adds no published surface.
- No new runtime or dev dependencies.
Verified end-to-end:
- Clean run: `smoke: zsh bash fish OK` (exit 0)
- Injection: removing the `complete -F` line from src/completions/bash.ts
causes smoke to fail at the bash registration check with exit 1.
The previous --install logic wrote to a fixed path per shell, which
meant zsh users got a file at ~/.zsh/completions/_codegraph and a
follow-up "now edit ~/.zshrc to add this to your fpath" hint. That
matches what gh, kubectl, rustup, Jottacloud all do — universally
broken UX where the install isn't actually self-contained.
This adds real detection so --install picks a location that's
already on the shell's load path wherever possible:
zsh: $ZSH/completions (oh-my-zsh) →
<prefix>/share/zsh/site-functions if writable →
~/.zsh/completions (fallback, with fpath hint)
bash: <homebrew>/etc/bash_completion.d if writable →
XDG ~/.local/share/bash-completion/completions
fish: ~/.config/fish/completions (already auto-discovered)
powershell: standalone ~/.config/powershell/codegraph.ps1
+ idempotent dot-source line in $PROFILE
The installer reports `(detected: <tier>)` on every run so users see
which path won. Unknown shells (nushell, etc.) exit non-zero with a
hint instead of writing somewhere wrong.
PowerShell support follows the clap_complete static pattern:
Register-ArgumentCompleter -Native, walk $commandAst.CommandElements
into a semicolon-joined path, switch on that path, emit
[CompletionResult] entries filtered by $wordToComplete. Tested
non-interactively via TabExpansion2 inside pwsh -NoProfile — clean
analog to fish's complete -C.
Smoke harness extensions:
- Dockerfile pulls pwsh from PowerShell GitHub releases (multi-arch
tarball; MS's Debian apt repo has no arm64). PowerShell 7.6.1 pinned.
- run.sh now parses the installer's output to find the path it
actually wrote to — no longer assumes the fallback tier.
- test-powershell.sh dot-sources the script and uses TabExpansion2.
- test-zsh-ohmyzsh.sh creates a fake $ZSH dir to exercise tier-1.
- Idempotency check: re-running powershell --install must not append
the $PROFILE line twice.
- Graceful-error check: `completions nushell` errors with "Unsupported
shell" instead of writing somewhere wrong.
vitest: 31 tests (up from 16) — adds detectInstallTarget coverage
per shell, powershell emitter assertions, single-quote escape check.
The prior section covered the detection table but skipped the practical follow-up: what to do after --install, when to restart the shell, which shells need extra packages, what the fpath hint actually looks like. Without these, users land on the section, install, and then wonder why Tab does nothing. Adds: - "Restart your shell" callout next to the --install example - Stdout/pipe example block for each shell (the "without --install" path was implicit before) - Per-shell requirements: bash-completion install for bash, fpath snippet for zsh fallback tier (with note that tier 1/2 don't need it), pwsh 7.x vs 5.1 caveat - PowerShell idempotency note moved into the detection table
Found via interactive pwsh testing on local macOS: `codegraph <TAB>` listed `--version` twice and was missing `-h`. Root causes: 1. Commander 14's `.version()` registers `-V/--version` on `program.options`, which the introspector already walked — so my hand-added `--version` line in the powershell + bash emitters was a duplicate. 2. Commander does NOT expose `-h/--help` via `program.options` (helpOption sits behind internal machinery). My emitters relied on per-shell hardcoded lines that I only added at the root, meaning subcommand completions silently dropped --help. Fix at the introspect layer: synthesize the help option once in describeCommand() so every command (root + subs) sees it as a regular option. All four emitters get it for free and the duplicate hardcoded lines come out. After fix, `codegraph <TAB>` in pwsh shows: --help / -h (new — was missing) --version / -V (no longer duplicated) 13 subcommands And `codegraph init -<TAB>` now correctly shows --help/-h alongside the per-subcommand flags. Verified via TabExpansion2 on local pwsh 7.7 (macOS) + full Docker smoke (zsh bash fish powershell OK). 31 vitest tests still pass.
…rsion dedupe cb2389e fixed two bugs found via interactive pwsh: 1. duplicate --version at root (commander auto-registered + my hardcoded) 2. missing -h/--help on subcommands (commander hides help from .options) Existing assertions used set-membership checks (`contains "--help"`) which would have passed even with the bugs present. This adds: vitest (8 new assertions, now 39 total): - zsh: count of `'(-h --help)'{-h,--help}` specs == root + every sub - bash: every flag-completion compgen list (filtered by content, not by the surrounding command line) contains --help - fish: count of `-s h -l help` declarations >= 4 - powershell: every switch arm contains both '--help' and '-h' - powershell: --version appears exactly 2 times (one CompletionResult emission = 2 string literals); >2 means duplicated - bash root flag list contains --version exactly once - fish root contains `-l version` exactly once - zsh `(-V --version)` pair appears exactly once smoke (per-shell runtime assertions): - bash: count -x --version in top-level COMPREPLY == 1; init subcommand COMPREPLY contains --help + -h - fish: complete -C "codegraph init -" contains --help + -h - powershell: init/--help, init/-h; top-level -h, -V; --version count via Where-Object pipeline == 1 Both layers catch both bug classes — vitest fast (~750 ms), smoke slower but exercises the actual installed scripts in real shells.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
codegraph completions <shell>for zsh, bash, fish, and PowerShell. With--install, the command auto-detects the right location for the user's environment (oh-my-zsh, Homebrew, XDG,$PROFILE) and writes there — so the install is actually a one-shot, not "drop a file then go edit your dotfile" like every other CLI's completion installer.codegraph completions zsh --install # → reports (detected: <tier>) codegraph completions bash --install codegraph completions fish --install codegraph completions powershell --installWithout
--install, the script goes to stdout — pipe it wherever you want.Why
Every CLI that ships an
--installfor completions handles the path the same broken way: the docs tell you to redirect to${fpath[1]}. On most systems${fpath[1]}is/usr/local/share/zsh/site-functionsor similar — root-owned. The "one-line install" silently requiressudo, or fails, or the user adds a custom dir to$fpathand remembers to do it again on every machine.gh,kubectl,helm,rustup, Jottacloud's docs — same brittle pattern.This PR does detection properly. The priority order per shell:
$ZSH/completions/)<prefix>/share/zsh/site-functions/if writable~/.zsh/completions/(printsfpathhint)<homebrew>/etc/bash_completion.d/if writable~/.local/share/bash-completion/completions/~/.config/fish/completions/.ps1in~/.config/powershell/(or~/Documents/PowerShell/on Windows) + idempotent dot-source line in$PROFILEThe installer reports
(detected: <tier>)so the user can see which rule fired. Unknown shells (nushell,xonsh, etc.) exit non-zero with a hint, writes nothing.What's in the PR
src/completions/index.tsemit(),parseShell(),installCompletions()src/completions/introspect.tsCommandtree → plainCommandDesc(decouples emitters from commander internals)src/completions/zsh.ts_arguments/_valuesemittersrc/completions/bash.tscompgen/complete -Femittersrc/completions/fish.tscomplete -c codegraphdeclarationssrc/completions/powershell.tsRegister-ArgumentCompleter -Nativestatic script (clap_complete pattern)src/completions/install.tsdetectInstallTarget()per-shell tiers + idempotent$PROFILEappendsrc/bin/codegraph.ts.command('completions <shell>')__tests__/completions.test.tsdocker/smoke-completions/*README.md### codegraph completionssection under CLI ReferenceZero new dependencies. Uses commander's existing introspection API (
cmd.options,cmd.registeredArguments,cmd.commands,cmd.aliases()).Design calls worth flagging
zsh is structural-tested, not content-tested
zsh has no
complete -Cequivalent. Full content testing would require_main_completerunning under a real ZLE widget context with a properly-set$compstate/$state/$opt_args— which scripts cannot manufacture. I tried acompaddshim against_codegraph_commands(the simplest helper, which uses_values); it silently captured nothing because_valuesshort-circuits on missing context. PTY +expectis the only path to full testing, and that's flaky across zsh 5.7/5.8/5.9 and acrossTERM/ locale /zleconfigurations — too unreliable for CI.What clap_complete, oclif, click, and Commander.js itself all ship for zsh: structural-only. This PR matches that bar. The smoke verifies:
zsh -nparses the scriptcompinitregisters_codegraphas an autoloadable completion_codegraph_init,_codegraph_query, etc.) — catches "script crashed mid-body"If you want stronger zsh coverage in the future, the realistic path is
zpty-based testing (what Fig uses) — but that's a separate engineering effort.PowerShell follows the clap_complete static pattern
Not the cobra/gh pattern (which calls back into the binary at tab-press time via a hidden
__completesubcommand). The clap_complete idiom is fully static:Register-ArgumentCompleter -Native, walk$commandAst.CommandElementsinto a semicolon-joined path string,switchon that string, emit[CompletionResult]::new(...)per candidate, filter by$wordToComplete.The static pattern is testable non-interactively via
TabExpansion2— clean analog to fish'scomplete -C. Full content assertions for top-level subcommands, flags, and alias dispatch are in the smoke (docker/smoke-completions/test-powershell.sh).--installfor PowerShell modifies$PROFILETouching the user's
$PROFILEis invasive. The reasoning for doing it anyway:--install— modifying things is the contract.$PROFILEdot-source is the only standard load path.# codegraph completions) is used to detect "already wired up" — re-running--installwon't duplicate the line. Verified in the smoke harness.Matches what oclif does. If you'd rather punt the
$PROFILEedit to the user (write the .ps1 and tell them the line to add), I'm happy to change it.Testing
The smoke harness:
npm packs the artifact andnpm install -gs it inside a pinned Node 22 + zsh + bash + fish + pwsh 7.6.1 containerfiles:list, missing chmod, etc.)COMPREPLY-driven), fish (complete -C), and PowerShell (TabExpansion2); structural for zsh$ZSHdir mid-test--installidempotency on re-runnushell) exit non-zero with a clear messageThe pwsh binary is pulled from PowerShell's GitHub releases (multi-arch tarball, pinned to 7.6.1) because Microsoft's Debian apt repo doesn't ship arm64. Image rebuilds in ~30 s cold, ~2 s warm. Adds ~250 MB to the image; not pushed anywhere, just used locally.
Isolation from the existing test suite
npm testbehavior is unchanged. The smoke is opt-in vianpm run smoke:completions.vitest.config.tsuntouched.docker/which is excluded fromnpm packby the existingfiles: ["dist","scripts","README.md"]allowlist — nothing new ships to npm consumers.package.json.Verified
npx tsc --noEmitcleannpm run buildsucceedsnpx vitest run __tests__/completions.test.ts→ 39/39 passnpx vitest run→ 681/682 pass (1 pre-existing watcher flake on Node 26; unrelated to this PR —git stash && vitest run __tests__/watcher.test.tsreproduces on master)npm run smoke:completions→ exit 0complete -F _codegraph codegraphfrom the bash emitter, rebuild, rerun smoke → fails at thebash:registrationassertion with exit 1. Restored.--versionat root (commander's auto-registeredOptioncolliding with a hardcoded one) and missing-h/--helpon subcommands (commander hideshelpOptionfromcmd.options). Both fixed in commitcb2389evia a singlehelpOptioninjection in the introspect layer; commit45b8c3badds 8 new vitest assertions + per-shell smoke assertions that explicitly count occurrences (would have caught both bugs). Demonstrates the smoke harness's value end-to-end — and exactly the kind of bug class structural tests miss.Out of scope (deliberately)
.github/workflows/. Adding one expands PR scope and is your call. The npm script is CI-ready when wanted.process.platform === 'win32'branch indetectInstallTarget) is covered by unit tests with mockedprocess, but not end-to-end. Adding a Windows container is possible but expensive.--uninstall—npm uninstall -gdoesn't clean these files (and shouldn't — they survive the package removal and become inert). Easy follow-up if you want it.$PROFILEconfirmation prompt — see "Design calls" above.src/completions/if you want to add one later — the pattern is uniform.Suggested CHANGELOG entry
Commits
This PR is 6 logically-distinct commits, reviewable in order:
feat(cli): add codegraph completions <shell> for zsh/bash/fish— the emitter foundationtest(completions): add docker-based end-to-end smoke test— the harnessfeat(completions): auto-detect install target + add PowerShell support— the detection tiers + pwshdocs(completions): expand README section with per-shell setup— usage docsfix(completions): inject --help/-h once, drop duplicate --version— bug found via interactive pwsh testing on macOS; fixed at introspect layertest(completions): regression coverage for --help/-h on subs and --version dedupe— vitest + smoke assertions that would have caught the bugHappy to squash if you'd prefer a single commit on merge.