Skip to content

feat(cli): Complete flags at every command depth in bash, zsh, and fish - #1760

Merged
BYK merged 1 commit into
mainfrom
feat/cli-shell-completion-depth
Oct 30, 2026
Merged

BYK merged 1 commit into
mainfrom
feat/cli-shell-completion-depth

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Follows #1751. The generated completion scripts only knew two command levels. That caused four problems:

  • In bash, flag names for nested commands (alert issues list --<TAB>, dashboard widget add --<TAB>) completed nothing.
  • Zsh completed no directories.
  • Fish offered a group's subcommands in later words (event send --level <TAB> offered list) and showed \( in descriptions.
  • Bash added a space after dist/ and inserted docs dir/ unescaped.

What changed

  • __complete now completes flag names. When the word being completed starts with -, it suggests the resolved command's visible flags and aliases, booleans included, each with its brief.
  • Flags already given are left out, whatever spelling was used (--limit 5, -n 5, --limit=5), unless they're variadic.
  • The generator writes each command's visible flags to a new flagNames field. Existing fields are unchanged.
  • All three scripts keep their static lists of commands and subcommands. For every later word they ask __complete, so flags, values, paths, and nested subcommands come from the same TypeScript code in every shell, at any depth. The per-command flag and enum tables are gone from the bash and fish scripts.
  • Bash escapes suggestions with printf %q, keeping a leading ~/, and removes escapes from the words it passes on. It sets compopt -o nospace when the only suggestion ends in /. It does not use -o default.
  • Zsh unquotes words with (Q) and shows descriptions through _describe. It adds suggestions ending in / with -S '', so no space follows them.
  • Fish runs string unescape on the current token. Each group's subcommands are offered only right after the group name.

Tradeoff

In bash and fish, flag names used to come from static tables. Each flag TAB now starts a __complete process, as positional values already did. On the npm bundle in this sandbox, one call took about 600 ms: node -e 0 takes about 50 ms, and requiring dist/index.cjs alone takes about 420 ms. I didn't measure the SEA binary. Command and subcommand TABs stay instant because those lists are still static. If the delay bothers people, the fix is a lighter entry point for __complete, not more static tables.

Verification

  • New unit tests cover flag-name completion: nested commands, aliases, leaving out flags already given, variadic flags, and a flag typed right after a flag that takes a value.
  • A bash harness test sources the generated script with an _init_completion stub. It runs the real CLI through a sentry wrapper on PATH for issue list --, alert issues list --li, sourcemap upload d and docs\ d (escaping and no trailing space), and event send --level.
  • A fish test runs complete -C. It's skipped when fish isn't installed.
  • I also ran real interactive shells in a pseudo-terminal: bash 5.2 with readline, zsh 5.9 with compinit, and fish 4.9.3. I checked the line after TAB. All three insert docs\ dir/ and dist/ with no trailing space, and --limit and warning with one. Zsh isn't in the automated tests.

Known gaps

  • Bash still splits words at : and = (bash-completion's default), so project create name:plat<TAB> and --flag=<TAB> don't complete in bash.
  • Groups with a default command (sentry issue --<TAB>) don't complete that command's flags.

via BYK.

--

View Session in Sentry

The generated shell scripts only knew the first two command levels, so
flag names for nested commands (`alert issues list --<TAB>`) completed
nothing, zsh completed no paths, and fish leaked a
group's subcommands into later words. Bash also added a space after a
directory and inserted names with spaces unescaped.

`__complete` now answers flag names: a partial starting with `-` gets
the resolved command's visible flags and aliases, with their briefs,
minus those already given unless they are variadic. The generator emits
each command's visible flags for it. The three scripts keep their static
command and subcommand lists and ask `__complete` for every later word,
so all shells share one tested code path. Bash escapes suggestions with
`printf %q` and sets `nospace` after a single `/` suggestion; zsh adds
`/` suggestions with `-S ''` and unquotes words before passing them on;
fish unescapes the current token. Fish descriptions no longer show
backslashes before parentheses.
@vercel

vercel Bot commented Oct 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
sentry-local Skipped Skipped Oct 30, 2026 9:09pm UTC

Request Review

@github-actions github-actions Bot added the risk: medium PR risk score: medium label Oct 30, 2026
@BYK
BYK enabled auto-merge (squash) October 30, 2026 21:15
@BYK
BYK merged commit 3bbc00e into main Oct 30, 2026
26 checks passed
@BYK
BYK deleted the feat/cli-shell-completion-depth branch October 30, 2026 21:22
BYK added a commit that referenced this pull request Oct 31, 2026
Shell completion runs `sentry __complete` on every TAB press, and since
#1760 also for every flag name in bash and fish. Each call was paying
for most of the CLI before it printed anything. This addresses the
review thread on #1751 (`complete.ts`).

**Where the time went**
- The npm bin required the whole 7.6 MB `dist/index.cjs` just to reach
the completion branch in `cli.ts`.
- The completion engine's eager import graph still reached
`@sentry/node`, even with telemetry off. `db/auth.ts` and `db/user.ts`
imported `withDbSpan` from `telemetry.ts`, the cache modules imported
`recordCacheHit`, and `logger.ts` imported `createConsolaReporter`. That
graph was 659 modules: Sentry SDK, OpenTelemetry, and the API client.
The completers themselves cost under 10 ms.

**What changed**
- `src/bin.ts` and `dist/bin.cjs` check for `__complete` first. They
call `runCompletion` in `src/lib/complete.ts` without loading `cli.ts`.
The npm package ships that path as a separate `dist/complete.cjs` (322
KB). The SEA binary takes it as a lazily evaluated chunk of its single
bundle.
- The database modules now get `withDbSpan` and `recordCacheHit` from
`db/index.ts`. Like the existing tracing proxy there, these require
telemetry lazily and skip it when telemetry is off, which
`runCompletion` sets. `initSentry` now passes the Sentry reporter to
`attachSentryReporter`, so `logger.ts` no longer imports the SDK.
- The eager graph is down to 40 modules. Completion output and the
protocol are unchanged: I diffed the output of several cases between the
old and new npm and SEA builds.

**Numbers.** Median of 20 runs on Node 24 (Linux x64), against a seeded
config DB:

| | bare node | `issue list ""` | `issue list --` | `init ./`
(directory) |
|---|---|---|---|---|
| npm before | 26 ms | 356 ms | 359 ms | 346 ms |
| npm after | 26 ms | 67 ms | 72 ms | 76 ms |
| SEA before | 28 ms | 145 ms | 148 ms | 145 ms |
| SEA after | 27 ms | 97 ms | 97 ms | 99 ms |

In dev (tsx), the same calls went from about 650 ms to about 315 ms;
most of what's left is tsx's own startup.

**Guard.** `test/script/cli-startup.test.ts` bundles `lib/complete.ts`
without its deferred imports. It fails on any package outside
`COMPLETION_PACKAGES`, naming the importer, or when there are more than
50 source modules. It also now covers `bin.ts`. The npm e2e suite checks
that `__complete` loads `complete.cjs` and never `index.cjs`. The "Shell
completion" section of `AGENTS.md` describes the rule.

**What's left.** `consola` is about 15 ms of the remaining npm time, and
Stricli (through `errors.ts`) about 3 ms. In the SEA binary, the rest is
mostly the cost of loading the single 7.6 MB embedded script. Each of
these would need a larger change (a lazy logger, a second SEA script),
so they are left for follow-ups.

<!-- junior-request-attribution:start -->
via **BYK**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3A58bcfa0dc51d17a81b0bf4c80824fe21 -->

--

[View Session in
Sentry](https://sentry.sentry.io/explore/conversations/agent%3A58bcfa0dc51d17a81b0bf4c80824fe21/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <burak.kaya@sentry.io>

This branch was previously deployed

1 inactive deployment
Preview — 17451729 Deployed Oct 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant