diff --git a/apps/cli-docs/src/content/docs/contributing.md b/apps/cli-docs/src/content/docs/contributing.md index 4a6bbcdbd..4eff832a0 100644 --- a/apps/cli-docs/src/content/docs/contributing.md +++ b/apps/cli-docs/src/content/docs/contributing.md @@ -55,7 +55,7 @@ cli/ │ │ ├── alert/ # create, delete, edit, list, view │ │ ├── auth/ # login, logout, refresh, status, token, whoami │ │ ├── build/ # download, upload -│ │ ├── cli/ # defaults, feedback, fix, import, setup, uninstall, upgrade +│ │ ├── cli/ # completion, defaults, feedback, fix, import, setup, uninstall, upgrade │ │ ├── code-mappings/# upload │ │ ├── conversation/# list, view │ │ ├── dart-symbol-map/# upload diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 76a735492..970463ea7 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -408,6 +408,7 @@ Manage mobile build artifacts CLI-related commands +- `sentry cli completion ` — Print the shell completion script - `sentry cli defaults ` — View and manage default settings - `sentry cli feedback ` — Send feedback about the CLI - `sentry cli fix` — Diagnose and repair CLI database issues diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md index 125392af0..a17399931 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md @@ -11,6 +11,10 @@ requires: CLI-related commands +### `sentry cli completion ` + +Print the shell completion script + ### `sentry cli defaults ` View and manage default settings diff --git a/packages/cli/src/commands/cli/completion.ts b/packages/cli/src/commands/cli/completion.ts new file mode 100644 index 000000000..9399bb7d3 --- /dev/null +++ b/packages/cli/src/commands/cli/completion.ts @@ -0,0 +1,71 @@ +/** + * sentry cli completion + * + * Print the shell completion script to stdout. Unlike `cli setup`, which writes + * completion files to disk, this command lets users and package managers install + * completions however they like. + * + * Usage: + * sentry cli completion zsh > ~/.local/share/zsh/site-functions/_sentry + * eval "$(sentry cli completion bash)" + * + * When no shell is given, the current shell is detected from $SHELL. + */ + +import type { SentryContext } from "../../context.js"; +import { buildCommand } from "../../lib/command.js"; +import { getCompletionScript } from "../../lib/completions.js"; +import { ValidationError } from "../../lib/errors.js"; +import { CommandOutput } from "../../lib/formatters/output.js"; +import { detectShellType } from "../../lib/shell.js"; + +export const completionCommand = buildCommand({ + auth: false, + docs: { + brief: "Print the shell completion script", + fullDescription: + "Print the shell completion script for the given shell to stdout.\n\n" + + "Supported shells: bash, zsh, fish.\n" + + "When no shell is given, it is detected from the $SHELL environment variable.\n\n" + + "Examples:\n" + + " sentry cli completion zsh > ~/.local/share/zsh/site-functions/_sentry\n" + + ' eval "$(sentry cli completion bash)"', + }, + output: { + human: (script: string) => script, + }, + parameters: { + flags: {}, + positional: { + kind: "tuple", + parameters: [ + { + placeholder: "shell", + brief: "Shell to generate completions for (bash, zsh, or fish)", + parse: String, + optional: true, + }, + ], + }, + }, + // biome-ignore lint/suspicious/useAwait: Stricli requires AsyncGenerator but script generation is synchronous + async *func( + this: SentryContext, + _flags: Record, + shell?: string + ) { + const shellType = shell + ? detectShellType(shell) + : detectShellType(this.env.SHELL); + + const script = getCompletionScript(shellType); + if (script === null) { + throw new ValidationError( + `Unsupported shell: ${shell || shellType}. Supported shells: bash, zsh, fish`, + "shell" + ); + } + + yield new CommandOutput(script); + }, +}); diff --git a/packages/cli/src/commands/cli/index.ts b/packages/cli/src/commands/cli/index.ts index c6c9e24c8..071c42e87 100644 --- a/packages/cli/src/commands/cli/index.ts +++ b/packages/cli/src/commands/cli/index.ts @@ -1,4 +1,5 @@ import { buildRouteMap } from "../../lib/route-map.js"; +import { completionCommand } from "./completion.js"; import { defaultsCommand } from "./defaults.js"; import { feedbackCommand } from "./feedback.js"; import { fixCommand } from "./fix.js"; @@ -9,6 +10,7 @@ import { upgradeCommand } from "./upgrade.js"; export const cliRoute = buildRouteMap({ routes: { + completion: completionCommand, defaults: defaultsCommand, feedback: feedbackCommand, fix: fixCommand, diff --git a/packages/cli/test/commands/cli/completion.test.ts b/packages/cli/test/commands/cli/completion.test.ts new file mode 100644 index 000000000..a6f613f77 --- /dev/null +++ b/packages/cli/test/commands/cli/completion.test.ts @@ -0,0 +1,83 @@ +/** + * Tests for `sentry cli completion` command. + * + * Verifies each supported shell prints a script, an unsupported shell errors + * without emitting a script, and the shell is auto-detected from $SHELL. + */ + +import { run } from "@stricli/core"; +import { describe, expect, test } from "vitest"; +import { app } from "../../../src/app.js"; +import type { SentryContext } from "../../../src/context.js"; + +/** + * Run the completion command via Stricli's `run()` and capture stdout. + * + * @param args - Args after `cli completion` (e.g. `["bash"]`) + * @param shellEnv - Value to inject as $SHELL for auto-detection tests + */ +async function runCompletion( + args: string[], + shellEnv?: string +): Promise<{ output: string; exitCode: number | undefined }> { + let output = ""; + const env = { ...process.env, SHELL: shellEnv }; + const mockContext: SentryContext = { + process: { + ...process, + exitCode: undefined, + } as typeof process, + env, + cwd: process.cwd(), + homeDir: "/tmp", + configDir: "/tmp", + stdout: { + write(data: string | Uint8Array) { + output += + typeof data === "string" ? data : new TextDecoder().decode(data); + return true; + }, + }, + stderr: { + write() { + return true; + }, + }, + stdin: process.stdin, + }; + + await run(app, ["cli", "completion", ...args], mockContext); + return { output, exitCode: mockContext.process.exitCode }; +} + +describe("sentry cli completion", () => { + test("bash prints a bash completion script", async () => { + const { output, exitCode } = await runCompletion(["bash"]); + expect(output).toContain("complete -F _sentry_completions sentry"); + expect(exitCode ?? 0).toBe(0); + }); + + test("zsh prints a zsh completion script", async () => { + const { output, exitCode } = await runCompletion(["zsh"]); + expect(output).toContain("#compdef sentry"); + expect(exitCode ?? 0).toBe(0); + }); + + test("fish prints a fish completion script", async () => { + const { output, exitCode } = await runCompletion(["fish"]); + expect(output).toContain("complete -c sentry"); + expect(exitCode ?? 0).toBe(0); + }); + + test("unsupported shell errors and prints no script", async () => { + const { output, exitCode } = await runCompletion(["nonsense"]); + expect(output).toBe(""); + expect(exitCode).toBeGreaterThan(0); + }); + + test("auto-detects the shell from $SHELL when no arg is given", async () => { + const { output, exitCode } = await runCompletion([], "/bin/zsh"); + expect(output).toContain("#compdef sentry"); + expect(exitCode ?? 0).toBe(0); + }); +});