Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/cli-docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ Manage mobile build artifacts

CLI-related commands

- `sentry cli completion <shell>` — Print the shell completion script
- `sentry cli defaults <key value...>` — View and manage default settings
- `sentry cli feedback <message...>` — Send feedback about the CLI
- `sentry cli fix` — Diagnose and repair CLI database issues
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ requires:

CLI-related commands

### `sentry cli completion <shell>`

Print the shell completion script

### `sentry cli defaults <key value...>`

View and manage default settings
Expand Down
71 changes: 71 additions & 0 deletions packages/cli/src/commands/cli/completion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* sentry cli completion <shell>
*
* 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<string, never>,
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"
);
Comment thread
MunifTanjim marked this conversation as resolved.
Comment thread
sentry[bot] marked this conversation as resolved.
}

yield new CommandOutput<string>(script);
},
});
2 changes: 2 additions & 0 deletions packages/cli/src/commands/cli/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -9,6 +10,7 @@ import { upgradeCommand } from "./upgrade.js";

export const cliRoute = buildRouteMap({
routes: {
completion: completionCommand,
defaults: defaultsCommand,
feedback: feedbackCommand,
fix: fixCommand,
Expand Down
83 changes: 83 additions & 0 deletions packages/cli/test/commands/cli/completion.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});