Skip to content

feat(cli): Declare a completion kind for every positional - #1751

Merged
BYK merged 8 commits into
mainfrom
feat/cli-positional-completion
Oct 30, 2026
Merged

BYK merged 8 commits into
mainfrom
feat/cli-positional-completion

Conversation

@sentry-junior

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

Copy link
Copy Markdown
Contributor

Every visible positional now declares how shell completion handles it, and positionals that can complete now do. Before this, 53 of the 126 positionals had no kind. sentry alert issues <TAB>, sentry sourcemap upload <TAB>, sentry help <TAB>, and sentry cli completion <TAB> all returned nothing.

What changed

  • Kinds. New kinds: file, directory, org-project-or-directory, command-path, choice, account, project-spec, and text (free text, no suggestions).
    • COMPLETERS in complete.ts is the only list of kinds. CompletionTarget is its keys, and it is frozen.
    • The *_TARGET constants in completion-target.ts are each checked with satisfies CompletionTarget, so a kind with no completer is a type error.
  • Parsers. Commands import pre-tagged parsers: parseFilePath, parseDirectoryPath, parseText, parseUrl, parseCommandPath, parseAccountSelector, and choiceParser(values).
  • Generator and __complete. The generator now also writes choice values, including Stricli enum flag values, so zsh and fish get them through __complete. It also writes every route group's subcommands.
    • __complete completes nested subcommands (alert issues <TAB>, dashboard widget <TAB>) and help's command path.
    • For path arguments it lists directory entries. Directories get a trailing /, and dotfiles show only when the partial starts with ..
  • Per-command behavior.
    • project create uses the project-spec kind instead of the cmdPath special case.
    • auth switch, auth logout, and auth login complete stored accounts. parseLoginEmail moved to db/accounts.ts so PARSER_TARGETS can list it.
    • init takes at most one target and one directory, in either order, and completion follows the arguments already typed. A word that doesn't look like a path completes org/project targets, because init reads it as the target. A path-like word (./, ../, /, ~/) completes directories and keeps its prefix. After a target only directories are offered, and nothing is offered once both are given.
    • cli completion completes bash/zsh/fish. cli defaults completes its registry keys, on the first word only. cli upgrade completes nightly/stable/undo.
  • Enforcement. test/lib/completion-coverage.test.ts walks the real route tree. It fails for any visible positional without a kind and names the command and placeholder. It has no allow-list.
  • Docs. The "Shell completion" section of packages/cli/AGENTS.md explains which parser to use for each argument type and how to add a kind or an entity.

Follow-ups

  • text positionals that could complete: release deploy environment, issue tags key, debug-files find id, monitor run slug, build download id, api endpoint (from the API schema), schema query, and local sessions. Local sessions live only in the daemon's memory, so there is no cheap synchronous read. event send completes paths only, not its optional target.
  • Flags: the 184 String-parsed flags, numeric parsers, --fields from output schemas, and --org/--project/--platform/--environment.
  • Shell scripts: zsh flag specs, static nested groups, and file directives (-o nospace/filenames in bash, so a directory doesn't get a trailing space).
  • Entity kinds: releases, environments, teams, monitors, alert rules.

via BYK.

--

View Session in Sentry

sentry-junior Bot and others added 2 commits October 30, 2026 00:35
No command uses `parseOrgTarget`, `parseOrgProjectTarget`, or
`parseIssueTarget` since #1564 moved every target and ID positional onto
bound parsers, which carry the completion kind themselves. Remove them,
and drop them and the already-removed `parseDashboardTarget` from the lint
rules that banned them in commands.

The lint rule now bans `withCompletionTarget` in commands instead, so a
command cannot recreate an identity parser that only tags the kind. The
introspection tests tag their own fixture parsers.

Part of #1564

Co-Authored-By: BYK <burak.kaya@sentry.io>
Every visible positional now says how shell completion handles it, and
test/lib/completion-coverage.test.ts fails, naming the command and
placeholder, when a new one does not.

New kinds in completion-target.ts, with pre-tagged parsers commands
import instead of tagging String: parseFilePath, parseDirectoryPath,
parseText, parseCommandPath, parseAccountSelector, and choiceParser(values).
project create's name:platform completer becomes the project-spec kind
instead of a special case in getCompletions, and parseLoginEmail moves to
db/accounts.ts so PARSER_TARGETS can list it.

The generator also writes choice values (including enum flag values, so
zsh and fish get them through __complete) and the subcommands of every
route group. __complete uses them to complete nested subcommands
(alert issues <TAB>) and help's command path, and lists files and
directories for path arguments.

Co-Authored-By: Claude <noreply@anthropic.com>
@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 7:52pm UTC

Request Review

…ompletion

# Conflicts:
#	packages/cli/AGENTS.md
#	packages/cli/src/lib/completion-target.ts
#	packages/cli/test/lib/introspect.test.ts
Comment thread packages/cli/src/commands/init.ts Resolved
init reads a word as its directory only when it looks like a path
(`./`, `../`, `/`, `~/`); anything else is the target. Bare `name/`
suggestions from the directory completer were therefore parsed as an
org target. init now completes org/project targets, and directories
only once the word looks like a path, keeping that prefix.

Co-Authored-By: Claude <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 37dba0d. Configure here.

Comment thread packages/cli/src/lib/complete.ts Resolved
init accepts at most one target and one directory, in either order.
The completer now offers only a directory after a target, only a
target after a directory, and nothing once both are present, so Tab
never completes into a command init rejects.
…ompletion

# Conflicts:
#	packages/cli/src/commands/api.ts

Co-Authored-By: BYK <burak.kaya@sentry.io>

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, this is AMAZING work well done!

{
placeholder: "url",
parse: String,
parse: parseText,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not parseUrl or something??

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, parseText hid that it's a URL. f2ee33a adds parseUrl in completion-target.ts and uses it here.

It still returns the input unchanged. issue link/unlink already validate the URL further down (parseHttpUrl in issue-links.ts, issue-integrations.ts, issue-app-links.ts) with their own ValidationErrors. Rejecting the URL in the Stricli parser instead would change both the error and the exit code (an argument parse failure exits 252), so I left that out of this PR.

Comment thread packages/cli/src/lib/complete.ts Resolved
Comment thread packages/cli/src/lib/complete.ts Resolved
export const TEXT_TARGET = "text";

/** Every completion target kind, in a stable order. */
export const COMPLETION_TARGETS = [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this "poor man's enum" without using enum??

I was expecting a const map of "target name" to "target completer" or something. Then we can do Object.keys() or completers.keys() and also define the type based on that easily?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, fixed in f2ee33a. COMPLETERS in complete.ts is now the only definition, written as an object literal with satisfies Record<string, Completer>, and export type CompletionTarget = keyof typeof COMPLETERS.

  • The 14 *_TARGET constants and the COMPLETION_TARGETS array are gone. completion-target.ts re-exports the type.
  • Commands pass the kind as a string literal (boundPositional("org/project", …)), and the type checks it. A kind with no completer is now a type error.
  • Each constant's doc comment moved onto its completer entry.
  • CompletionEntityKind is now plain "issue" | "dashboard". Deriving it from CompletionTarget made the type circular.

The regenerated completion-targets.json is byte-identical, so completion output doesn't change.

The kinds were a list of string constants plus an array that existed only
to derive the CompletionTarget type, kept in step with COMPLETERS by hand.
COMPLETERS is now the one definition: the type is its keys, so a parser
tagged with a kind that has no completer is a type error, and commands
name kinds as checked string literals ("org/project").

The issue link/unlink URL positional parses with a named parseUrl. It
returns its input like before; link and unlink keep validating the URL
with their own errors.

The generated completion-targets.json is byte-identical.
@github-actions github-actions Bot added the risk: medium PR risk score: medium label Oct 30, 2026

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of the new string literals spread over the old const names so would be great if we can get that back. Rest looks good.

Comment thread packages/cli/src/lib/complete.ts Outdated
* The completer of every kind a parser can declare. Its keys are the
* kinds: {@link CompletionTarget} is derived from them.
*/
const COMPLETERS = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also do Object.freeze() and as const at the end?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.freeze() is in, as of 5384964: const COMPLETERS = Object.freeze({ … } satisfies Record<string, Completer>).

I left as const off because it would change nothing here. Object.freeze already types the result as Readonly<…>, so the properties are readonly. as const only narrows literal values, and every value in this map is a function, so there is nothing for it to narrow. keyof typeof COMPLETERS still gives the same 14-member union, and I confirmed tsc rejects "bogus" satisfies CompletionTarget.

Also in 5384964, per your review: the *_TARGET names are back at every call site. Each one is now export const ORG_TARGET = "org" satisfies CompletionTarget, so COMPLETERS stays the only list of kinds, and a name without a completer fails to compile. completion-targets.json is still byte-identical.

Commands, helpers, and tests name kinds with the ORG_TARGET-style
constants again instead of string literals. Each constant is checked
with `satisfies CompletionTarget`, so the keys of COMPLETERS stay the
only list of kinds and a name with no completer is a type error.

COMPLETERS is frozen, so no code can add or swap a completer at runtime.
@github-actions github-actions Bot removed the risk: medium PR risk score: medium label Oct 30, 2026
@BYK
BYK enabled auto-merge (squash) October 30, 2026 20:00
@github-actions github-actions Bot added the risk: medium PR risk score: medium label Oct 30, 2026
@BYK
BYK merged commit a5f4455 into main Oct 30, 2026
24 of 26 checks passed
@BYK
BYK deleted the feat/cli-positional-completion branch October 30, 2026 20:03
BYK pushed a commit that referenced this pull request Oct 30, 2026
…sh (#1760)

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.

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

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

--

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

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

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

This branch was previously deployed

1 inactive deployment
Preview — 53849640 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