diff --git a/docs/src/content/docs/commands/feedback.md b/docs/src/content/docs/commands/cli/feedback.md similarity index 67% rename from docs/src/content/docs/commands/feedback.md rename to docs/src/content/docs/commands/cli/feedback.md index 34488887..065aef54 100644 --- a/docs/src/content/docs/commands/feedback.md +++ b/docs/src/content/docs/commands/cli/feedback.md @@ -1,18 +1,14 @@ --- -title: feedback +title: cli feedback description: Send feedback about the Sentry CLI --- Send feedback about your experience with the CLI. -## Commands - -### `sentry feedback` - -Submit feedback about the CLI directly to the Sentry team. +## Usage ```bash -sentry feedback +sentry cli feedback ``` **Arguments:** @@ -25,13 +21,13 @@ sentry feedback ```bash # Send positive feedback -sentry feedback i love this tool +sentry cli feedback i love this tool # Report an issue -sentry feedback the issue view is confusing +sentry cli feedback the issue view is confusing # Suggest an improvement -sentry feedback would be great to have a search command +sentry cli feedback would be great to have a search command ``` ## Notes diff --git a/docs/src/content/docs/commands/cli/index.md b/docs/src/content/docs/commands/cli/index.md new file mode 100644 index 00000000..61325cb9 --- /dev/null +++ b/docs/src/content/docs/commands/cli/index.md @@ -0,0 +1,13 @@ +--- +title: cli +description: CLI-related commands for managing the Sentry CLI itself +--- + +Commands for managing the Sentry CLI itself, including sending feedback and upgrading to newer versions. + +## Commands + +| Command | Description | +|---------|-------------| +| [`feedback`](./feedback/) | Send feedback about the CLI | +| [`upgrade`](./upgrade/) | Update the CLI to the latest version | diff --git a/docs/src/content/docs/commands/upgrade.md b/docs/src/content/docs/commands/cli/upgrade.md similarity index 76% rename from docs/src/content/docs/commands/upgrade.md rename to docs/src/content/docs/commands/cli/upgrade.md index 37e27f23..345f9e1f 100644 --- a/docs/src/content/docs/commands/upgrade.md +++ b/docs/src/content/docs/commands/cli/upgrade.md @@ -1,5 +1,5 @@ --- -title: upgrade +title: cli upgrade description: Update the Sentry CLI to the latest version --- @@ -8,14 +8,12 @@ Self-update the Sentry CLI to the latest or a specific version. ## Usage ```bash -sentry upgrade # Update to latest version -sentry upgrade 0.5.0 # Update to specific version -sentry upgrade --check # Check for updates without installing -sentry upgrade --method npm # Force using npm to upgrade +sentry cli upgrade # Update to latest version +sentry cli upgrade 0.5.0 # Update to specific version +sentry cli upgrade --check # Check for updates without installing +sentry cli upgrade --method npm # Force using npm to upgrade ``` -**Alias:** `sentry update` - ## Options | Option | Description | @@ -41,7 +39,7 @@ The CLI auto-detects how it was installed and uses the same method to upgrade: ### Check for updates ```bash -sentry upgrade --check +sentry cli upgrade --check ``` ``` @@ -49,13 +47,13 @@ Installation method: curl Current version: 0.4.0 Latest version: 0.5.0 -Run 'sentry upgrade' to update. +Run 'sentry cli upgrade' to update. ``` ### Upgrade to latest ```bash -sentry upgrade +sentry cli upgrade ``` ``` @@ -71,7 +69,7 @@ Successfully upgraded to 0.5.0. ### Upgrade to specific version ```bash -sentry upgrade 0.5.0 +sentry cli upgrade 0.5.0 ``` ### Force installation method @@ -79,5 +77,5 @@ sentry upgrade 0.5.0 If auto-detection fails or you want to switch installation methods: ```bash -sentry upgrade --method npm +sentry cli upgrade --method npm ``` diff --git a/docs/src/content/docs/commands/index.md b/docs/src/content/docs/commands/index.md index cbb6182b..45aae5d5 100644 --- a/docs/src/content/docs/commands/index.md +++ b/docs/src/content/docs/commands/index.md @@ -10,12 +10,12 @@ The Sentry CLI provides commands for interacting with various Sentry resources. | Command | Description | |---------|-------------| | [`auth`](./auth/) | Authentication management | +| [`cli`](./cli/) | CLI-related commands (feedback, upgrade) | | [`org`](./org/) | Organization operations | | [`project`](./project/) | Project operations | | [`issue`](./issue/) | Issue tracking | | [`event`](./event/) | Event inspection | | [`api`](./api/) | Direct API access | -| [`feedback`](./feedback/) | Send feedback about the CLI | ## Global Options diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 0633a528..ddc36945 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -364,11 +364,15 @@ sentry api /organizations/ --include sentry api /projects/my-org/my-project/issues/ --paginate ``` -### Upgrade +### Cli -Update the Sentry CLI to the latest version +CLI-related commands + +#### `sentry cli feedback ` -#### `sentry upgrade ` +Send feedback about the CLI + +#### `sentry cli upgrade ` Update the Sentry CLI to the latest version @@ -376,29 +380,6 @@ Update the Sentry CLI to the latest version - `--check - Check for updates without installing` - `--method - Installation method to use (curl, npm, pnpm, bun, yarn)` -### Feedback - -Send feedback about the CLI - -#### `sentry feedback ` - -Send feedback about the CLI - -**Examples:** - -```bash -sentry feedback - -# Send positive feedback -sentry feedback i love this tool - -# Report an issue -sentry feedback the issue view is confusing - -# Suggest an improvement -sentry feedback would be great to have a search command -``` - ## Output Formats ### JSON Output diff --git a/src/app.ts b/src/app.ts index 6d4b32ff..0200ccc8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,13 +8,12 @@ import { } from "@stricli/core"; import { apiCommand } from "./commands/api.js"; import { authRoute } from "./commands/auth/index.js"; +import { cliRoute } from "./commands/cli/index.js"; import { eventRoute } from "./commands/event/index.js"; -import { feedbackCommand } from "./commands/feedback.js"; import { helpCommand } from "./commands/help.js"; import { issueRoute } from "./commands/issue/index.js"; import { orgRoute } from "./commands/org/index.js"; import { projectRoute } from "./commands/project/index.js"; -import { upgradeCommand } from "./commands/upgrade.js"; import { CLI_VERSION } from "./lib/constants.js"; import { CliError, getExitCode } from "./lib/errors.js"; import { error as errorColor } from "./lib/formatters/colors.js"; @@ -24,16 +23,12 @@ export const routes = buildRouteMap({ routes: { help: helpCommand, auth: authRoute, + cli: cliRoute, org: orgRoute, project: projectRoute, issue: issueRoute, event: eventRoute, api: apiCommand, - upgrade: upgradeCommand, - feedback: feedbackCommand, - }, - aliases: { - update: "upgrade", }, defaultCommand: "help", docs: { diff --git a/src/commands/feedback.ts b/src/commands/cli/feedback.ts similarity index 88% rename from src/commands/feedback.ts rename to src/commands/cli/feedback.ts index 64c93fd1..6173d445 100644 --- a/src/commands/feedback.ts +++ b/src/commands/cli/feedback.ts @@ -4,15 +4,15 @@ * Allows users to submit feedback about the CLI. * All arguments after 'feedback' are joined into a single message. * - * @example sentry feedback i love this tool - * @example sentry feedback the issue view is confusing + * @example sentry cli feedback i love this tool + * @example sentry cli feedback the issue view is confusing */ // biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import import * as Sentry from "@sentry/bun"; import { buildCommand } from "@stricli/core"; -import type { SentryContext } from "../context.js"; -import { ValidationError } from "../lib/errors.js"; +import type { SentryContext } from "../../context.js"; +import { ValidationError } from "../../lib/errors.js"; export const feedbackCommand = buildCommand({ docs: { diff --git a/src/commands/cli/index.ts b/src/commands/cli/index.ts new file mode 100644 index 00000000..bf7ed31d --- /dev/null +++ b/src/commands/cli/index.ts @@ -0,0 +1,16 @@ +import { buildRouteMap } from "@stricli/core"; +import { feedbackCommand } from "./feedback.js"; +import { upgradeCommand } from "./upgrade.js"; + +export const cliRoute = buildRouteMap({ + routes: { + feedback: feedbackCommand, + upgrade: upgradeCommand, + }, + docs: { + brief: "CLI-related commands", + fullDescription: + "Commands for managing the Sentry CLI itself, including sending feedback " + + "and upgrading to newer versions.", + }, +}); diff --git a/src/commands/upgrade.ts b/src/commands/cli/upgrade.ts similarity index 82% rename from src/commands/upgrade.ts rename to src/commands/cli/upgrade.ts index 45a139cb..79f0cbaa 100644 --- a/src/commands/upgrade.ts +++ b/src/commands/cli/upgrade.ts @@ -1,13 +1,13 @@ /** - * sentry upgrade + * sentry cli upgrade * * Self-update the Sentry CLI to the latest or a specific version. */ import { buildCommand } from "@stricli/core"; -import type { SentryContext } from "../context.js"; -import { CLI_VERSION } from "../lib/constants.js"; -import { UpgradeError } from "../lib/errors.js"; +import type { SentryContext } from "../../context.js"; +import { CLI_VERSION } from "../../lib/constants.js"; +import { UpgradeError } from "../../lib/errors.js"; import { detectInstallationMethod, executeUpgrade, @@ -16,7 +16,7 @@ import { parseInstallationMethod, VERSION_PREFIX_REGEX, versionExists, -} from "../lib/upgrade.js"; +} from "../../lib/upgrade.js"; type UpgradeFlags = { readonly check: boolean; @@ -30,10 +30,10 @@ export const upgradeCommand = buildCommand({ "Check for updates and upgrade the Sentry CLI to the latest or a specific version.\n\n" + "By default, detects how the CLI was installed (npm, curl, etc.) and uses the same method to upgrade.\n\n" + "Examples:\n" + - " sentry upgrade # Update to latest version\n" + - " sentry upgrade 0.5.0 # Update to specific version\n" + - " sentry upgrade --check # Check for updates without installing\n" + - " sentry upgrade --method npm # Force using npm to upgrade", + " sentry cli upgrade # Update to latest version\n" + + " sentry cli upgrade 0.5.0 # Update to specific version\n" + + " sentry cli upgrade --check # Check for updates without installing\n" + + " sentry cli upgrade --method npm # Force using npm to upgrade", }, parameters: { positional: { @@ -94,7 +94,9 @@ export const upgradeCommand = buildCommand({ if (CLI_VERSION === target) { stdout.write("\nYou are already on the target version.\n"); } else { - const cmd = version ? `sentry upgrade ${target}` : "sentry upgrade"; + const cmd = version + ? `sentry cli upgrade ${target}` + : "sentry cli upgrade"; stdout.write(`\nRun '${cmd}' to update.\n`); } return; diff --git a/src/lib/version-check.ts b/src/lib/version-check.ts index 82ab21d0..abeb2b6a 100644 --- a/src/lib/version-check.ts +++ b/src/lib/version-check.ts @@ -133,7 +133,7 @@ function getUpdateNotificationImpl(): string | null { return null; } - return `\n${muted("Update available:")} ${cyan(CLI_VERSION)} → ${cyan(latestVersion)} Run ${cyan('"sentry upgrade"')} to update.\n`; + return `\n${muted("Update available:")} ${cyan(CLI_VERSION)} → ${cyan(latestVersion)} Run ${cyan('"sentry cli upgrade"')} to update.\n`; } catch (error) { // DB access failed - report to Sentry but don't crash CLI Sentry.captureException(error); diff --git a/test/commands/cli.test.ts b/test/commands/cli.test.ts new file mode 100644 index 00000000..7b658e00 --- /dev/null +++ b/test/commands/cli.test.ts @@ -0,0 +1,192 @@ +/** + * CLI Route Tests + * + * Tests for the sentry cli command group. + */ + +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; +import { feedbackCommand } from "../../src/commands/cli/feedback.js"; +import { upgradeCommand } from "../../src/commands/cli/upgrade.js"; + +describe("feedbackCommand.func", () => { + test("throws ValidationError for empty message", async () => { + // Access func through loader + const func = await feedbackCommand.loader(); + const mockContext = { + stdout: { write: mock(() => true) }, + stderr: { write: mock(() => true) }, + }; + + await expect(func.call(mockContext, {}, "")).rejects.toThrow( + "Please provide a feedback message." + ); + }); + + test("throws ValidationError for whitespace-only message", async () => { + const func = await feedbackCommand.loader(); + const mockContext = { + stdout: { write: mock(() => true) }, + stderr: { write: mock(() => true) }, + }; + + await expect(func.call(mockContext, {}, " ")).rejects.toThrow( + "Please provide a feedback message." + ); + }); + + test("writes telemetry disabled message when Sentry is disabled", async () => { + const func = await feedbackCommand.loader(); + const stderrWrite = mock(() => true); + const mockContext = { + stdout: { write: mock(() => true) }, + stderr: { write: stderrWrite }, + }; + + // Sentry is disabled in test environment (no DSN) + await func.call(mockContext, {}, "test", "feedback"); + + expect(stderrWrite).toHaveBeenCalledWith( + "Feedback not sent: telemetry is disabled.\n" + ); + expect(stderrWrite).toHaveBeenCalledWith( + "Unset SENTRY_CLI_NO_TELEMETRY to enable feedback.\n" + ); + }); +}); + +// Test the upgrade command func +describe("upgradeCommand.func", () => { + let originalFetch: typeof globalThis.fetch; + + beforeEach(() => { + originalFetch = globalThis.fetch; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + // Note: We skip testing "unknown installation method" case because + // detectInstallationMethod() runs actual shell commands (npm list, etc.) + // which can be slow/flaky in CI. The unknown method handling is tested + // indirectly through the upgrade.ts unit tests in lib/upgrade.test.ts. + + test("shows installation info with specified method", async () => { + globalThis.fetch = (async () => + new Response(JSON.stringify({ tag_name: "v0.0.0-dev" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + })) as typeof fetch; + + const func = await upgradeCommand.loader(); + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + // Use method flag to bypass detection (curl uses GitHub) + await func.call(mockContext, { check: false, method: "curl" }); + + expect(stdoutWrite).toHaveBeenCalledWith("Installation method: curl\n"); + expect(stdoutWrite).toHaveBeenCalledWith( + expect.stringContaining("Current version:") + ); + expect(stdoutWrite).toHaveBeenCalledWith("\nAlready up to date.\n"); + }); + + test("check mode shows update available", async () => { + // curl uses GitHub API which returns { tag_name: "vX.X.X" } + globalThis.fetch = (async () => + new Response(JSON.stringify({ tag_name: "v99.0.0" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + })) as typeof fetch; + + const func = await upgradeCommand.loader(); + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + await func.call(mockContext, { check: true, method: "curl" }); + + expect(stdoutWrite).toHaveBeenCalledWith( + "\nRun 'sentry cli upgrade' to update.\n" + ); + }); + + test("check mode with version shows versioned command", async () => { + globalThis.fetch = (async () => + new Response(JSON.stringify({ tag_name: "v99.0.0" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + })) as typeof fetch; + + const func = await upgradeCommand.loader(); + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + await func.call(mockContext, { check: true, method: "curl" }, "2.0.0"); + + expect(stdoutWrite).toHaveBeenCalledWith("Target version: 2.0.0\n"); + expect(stdoutWrite).toHaveBeenCalledWith( + "\nRun 'sentry cli upgrade 2.0.0' to update.\n" + ); + }); + + test("check mode shows already on target when versions match", async () => { + globalThis.fetch = (async () => + new Response(JSON.stringify({ tag_name: "v0.0.0-dev" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + })) as typeof fetch; + + const func = await upgradeCommand.loader(); + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + await func.call(mockContext, { check: true, method: "curl" }); + + expect(stdoutWrite).toHaveBeenCalledWith( + "\nYou are already on the target version.\n" + ); + }); + + test("throws UpgradeError when specified version does not exist", async () => { + // First call: fetch latest (returns 99.0.0) + // Second call: check if version exists (returns 404) + let callCount = 0; + globalThis.fetch = (async () => { + callCount += 1; + if (callCount === 1) { + // Latest version check + return new Response(JSON.stringify({ tag_name: "v99.0.0" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + // Version exists check - return 404 + return new Response("Not Found", { status: 404 }); + }) as typeof fetch; + + const func = await upgradeCommand.loader(); + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + // Specify a version that doesn't exist + await expect( + func.call(mockContext, { check: false, method: "curl" }, "999.0.0") + ).rejects.toThrow("Version 999.0.0 not found"); + }); +}); diff --git a/test/commands/upgrade.test.ts b/test/commands/upgrade.test.ts deleted file mode 100644 index 06eb48f0..00000000 --- a/test/commands/upgrade.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Upgrade Command Tests - * - * Tests for the sentry upgrade command registration and module exports. - */ - -import { describe, expect, test } from "bun:test"; -import { upgradeCommand } from "../../src/commands/upgrade.js"; - -describe("upgradeCommand", () => { - test("is exported and defined", () => { - expect(upgradeCommand).toBeDefined(); - }); -}); diff --git a/test/lib/version-check.test.ts b/test/lib/version-check.test.ts index 0316d801..9bfae97b 100644 --- a/test/lib/version-check.test.ts +++ b/test/lib/version-check.test.ts @@ -82,7 +82,7 @@ describe("getUpdateNotification", () => { expect(notification).not.toBeNull(); expect(notification).toContain("Update available:"); expect(notification).toContain("99.0.0"); - expect(notification).toContain("sentry upgrade"); + expect(notification).toContain("sentry cli upgrade"); }); });