From c044ce812ba8405c4a870dea08c6be5ab49607dd Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Thu, 5 Feb 2026 21:02:45 +0530 Subject: [PATCH 1/9] feat: initial draft --- src/app.ts | 2 + src/commands/init.ts | 139 ++++++++++++++++++++++++++++++++++++ src/lib/wizard.ts | 129 +++++++++++++++++++++++++++++++++ test/commands/init.test.ts | 141 +++++++++++++++++++++++++++++++++++++ 4 files changed, 411 insertions(+) create mode 100644 src/commands/init.ts create mode 100644 src/lib/wizard.ts create mode 100644 test/commands/init.test.ts diff --git a/src/app.ts b/src/app.ts index 0200ccc8..4fc666bb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,6 +11,7 @@ import { authRoute } from "./commands/auth/index.js"; import { cliRoute } from "./commands/cli/index.js"; import { eventRoute } from "./commands/event/index.js"; import { helpCommand } from "./commands/help.js"; +import { initCommand } from "./commands/init.js"; import { issueRoute } from "./commands/issue/index.js"; import { orgRoute } from "./commands/org/index.js"; import { projectRoute } from "./commands/project/index.js"; @@ -22,6 +23,7 @@ import { error as errorColor } from "./lib/formatters/colors.js"; export const routes = buildRouteMap({ routes: { help: helpCommand, + init: initCommand, auth: authRoute, cli: cliRoute, org: orgRoute, diff --git a/src/commands/init.ts b/src/commands/init.ts new file mode 100644 index 00000000..b1183248 --- /dev/null +++ b/src/commands/init.ts @@ -0,0 +1,139 @@ +/** + * sentry init + * + * Initialize Sentry in your project using the Sentry Wizard. + * Supports React Native, Flutter, iOS, Android, Cordova, Angular, + * Electron, Next.js, Nuxt, Remix, SvelteKit, and sourcemaps setup. + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../context.js"; +import { getDefaultOrganization } from "../lib/db/defaults.js"; +import { runWizard, type WizardOptions } from "../lib/wizard.js"; + +type InitFlags = { + readonly integration?: string; + readonly org?: string; + readonly project?: string; + readonly url?: string; + readonly debug: boolean; + readonly uninstall: boolean; + readonly quiet: boolean; + readonly "skip-connect": boolean; + readonly saas: boolean; + readonly signup: boolean; + readonly "disable-telemetry": boolean; +}; + +export const initCommand = buildCommand({ + docs: { + brief: "Initialize Sentry in your project", + fullDescription: + "Set up Sentry in your project using the Sentry Wizard.\n\n" + + "Supported platforms: React Native, Flutter, iOS, Android, Cordova, Angular,\n" + + "Electron, Next.js, Nuxt, Remix, SvelteKit, and sourcemaps.\n\n" + + "Examples:\n" + + " sentry init # Interactive setup\n" + + " sentry init -i nextjs # Setup for Next.js\n" + + " sentry init -i reactNative # Setup for React Native\n" + + " sentry init --uninstall # Remove Sentry from project", + }, + parameters: { + flags: { + integration: { + kind: "parsed", + parse: String, + brief: "Integration to setup (nextjs, reactNative, flutter, etc.)", + optional: true, + variadic: false, + }, + org: { + kind: "parsed", + parse: String, + brief: "Sentry organization slug", + optional: true, + variadic: false, + }, + project: { + kind: "parsed", + parse: String, + brief: "Sentry project slug", + optional: true, + variadic: false, + }, + url: { + kind: "parsed", + parse: String, + brief: "Sentry URL (for self-hosted)", + optional: true, + variadic: false, + }, + debug: { + kind: "boolean", + brief: "Enable verbose logging", + default: false, + }, + uninstall: { + kind: "boolean", + brief: "Revert project setup", + default: false, + }, + quiet: { + kind: "boolean", + brief: "Don't prompt for input", + default: false, + }, + "skip-connect": { + kind: "boolean", + brief: "Skip connecting to Sentry server", + default: false, + }, + saas: { + kind: "boolean", + brief: "Skip self-hosted/SaaS selection", + default: false, + }, + signup: { + kind: "boolean", + brief: "Redirect to signup if not logged in", + default: false, + }, + "disable-telemetry": { + kind: "boolean", + brief: "Don't send telemetry to Sentry", + default: false, + }, + }, + }, + async func(this: SentryContext, flags: InitFlags): Promise { + const { stdout } = this; + + // Build wizard options from our flags + const options: WizardOptions = { + integration: flags.integration, + org: flags.org, + project: flags.project, + url: flags.url, + debug: flags.debug, + uninstall: flags.uninstall, + quiet: flags.quiet, + skipConnect: flags["skip-connect"], + saas: flags.saas, + signup: flags.signup, + disableTelemetry: flags["disable-telemetry"], + }; + + // Auto-populate org from CLI config if not provided and user is authenticated + if (!options.org) { + const defaultOrg = await getDefaultOrganization(); + if (defaultOrg) { + options.org = defaultOrg; + stdout.write(`Using organization: ${defaultOrg}\n`); + } + } + + stdout.write("Starting Sentry Wizard...\n\n"); + + await runWizard(options); + }, +}); diff --git a/src/lib/wizard.ts b/src/lib/wizard.ts new file mode 100644 index 00000000..41f10dbc --- /dev/null +++ b/src/lib/wizard.ts @@ -0,0 +1,129 @@ +/** + * Sentry Wizard Integration + * + * Wraps @sentry/wizard for project initialization. + * This abstraction allows future migration to a native implementation + * without changing the public interface. + */ + +import { spawn } from "node:child_process"; + +/** + * Options for running the Sentry Wizard. + * These map to our CLI's interface, not the wizard's flags directly. + */ +export type WizardOptions = { + /** Platform/framework to setup (nextjs, reactNative, flutter, etc.) */ + integration?: string; + /** Sentry organization slug */ + org?: string; + /** Sentry project slug */ + project?: string; + /** Sentry URL (for self-hosted installations) */ + url?: string; + /** Enable verbose logging */ + debug?: boolean; + /** Revert project setup */ + uninstall?: boolean; + /** Non-interactive mode - don't prompt for input */ + quiet?: boolean; + /** Skip connecting to Sentry server */ + skipConnect?: boolean; + /** Skip self-hosted/SaaS selection prompt */ + saas?: boolean; + /** Redirect to signup if not logged in */ + signup?: boolean; + /** Don't send telemetry data to Sentry */ + disableTelemetry?: boolean; +}; + +/** + * Map our options to wizard CLI arguments. + * This is an internal implementation detail - the public interface is WizardOptions. + */ +function buildWizardArgs(options: WizardOptions): string[] { + const args: string[] = []; + + if (options.integration) { + args.push("-i", options.integration); + } + if (options.org) { + args.push("--org", options.org); + } + if (options.project) { + args.push("--project", options.project); + } + if (options.url) { + args.push("-u", options.url); + } + if (options.debug) { + args.push("--debug"); + } + if (options.uninstall) { + args.push("--uninstall"); + } + if (options.quiet) { + args.push("--quiet"); + } + if (options.skipConnect) { + args.push("--skip-connect"); + } + if (options.saas) { + args.push("--saas"); + } + if (options.signup) { + args.push("-s"); + } + if (options.disableTelemetry) { + args.push("--disable-telemetry"); + } + + return args; +} + +/** + * Run the Sentry Wizard to initialize a project. + * + * Spawns `npx @sentry/wizard@latest` with the provided options. + * Uses stdio: "inherit" for the interactive terminal UI. + * + * @param options - Wizard configuration options + * @throws Error if npx is not found or wizard exits with non-zero code + */ +export function runWizard(options: WizardOptions): Promise { + return new Promise((resolve, reject) => { + const npx = Bun.which("npx"); + if (!npx) { + reject( + new Error( + "npx not found. Please install Node.js/npm to use the init command." + ) + ); + return; + } + + const args = buildWizardArgs(options); + + const proc = spawn(npx, ["@sentry/wizard@latest", ...args], { + stdio: "inherit", + env: process.env, + }); + + proc.on("close", (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Wizard exited with code ${code}`)); + } + }); + + proc.on("error", (err) => { + reject(new Error(`Failed to start wizard: ${err.message}`)); + }); + }); +} + +/** + * Build wizard args from options (exported for testing). + */ +export { buildWizardArgs as _buildWizardArgs }; diff --git a/test/commands/init.test.ts b/test/commands/init.test.ts new file mode 100644 index 00000000..5e86857d --- /dev/null +++ b/test/commands/init.test.ts @@ -0,0 +1,141 @@ +/** + * Init Command Tests + * + * Tests for the sentry init command and wizard flag mapping. + */ + +import { describe, expect, test } from "bun:test"; +import { _buildWizardArgs, type WizardOptions } from "../../src/lib/wizard.js"; + +describe("buildWizardArgs", () => { + test("returns empty array when no options provided", () => { + const args = _buildWizardArgs({}); + expect(args).toEqual([]); + }); + + test("maps integration option to -i flag", () => { + const args = _buildWizardArgs({ integration: "nextjs" }); + expect(args).toEqual(["-i", "nextjs"]); + }); + + test("maps org option to --org flag", () => { + const args = _buildWizardArgs({ org: "my-org" }); + expect(args).toEqual(["--org", "my-org"]); + }); + + test("maps project option to --project flag", () => { + const args = _buildWizardArgs({ project: "my-project" }); + expect(args).toEqual(["--project", "my-project"]); + }); + + test("maps url option to -u flag", () => { + const args = _buildWizardArgs({ url: "https://sentry.example.com" }); + expect(args).toEqual(["-u", "https://sentry.example.com"]); + }); + + test("maps debug option to --debug flag", () => { + const args = _buildWizardArgs({ debug: true }); + expect(args).toEqual(["--debug"]); + }); + + test("does not include --debug when debug is false", () => { + const args = _buildWizardArgs({ debug: false }); + expect(args).not.toContain("--debug"); + }); + + test("maps uninstall option to --uninstall flag", () => { + const args = _buildWizardArgs({ uninstall: true }); + expect(args).toEqual(["--uninstall"]); + }); + + test("maps quiet option to --quiet flag", () => { + const args = _buildWizardArgs({ quiet: true }); + expect(args).toEqual(["--quiet"]); + }); + + test("maps skipConnect option to --skip-connect flag", () => { + const args = _buildWizardArgs({ skipConnect: true }); + expect(args).toEqual(["--skip-connect"]); + }); + + test("maps saas option to --saas flag", () => { + const args = _buildWizardArgs({ saas: true }); + expect(args).toEqual(["--saas"]); + }); + + test("maps signup option to -s flag", () => { + const args = _buildWizardArgs({ signup: true }); + expect(args).toEqual(["-s"]); + }); + + test("maps disableTelemetry option to --disable-telemetry flag", () => { + const args = _buildWizardArgs({ disableTelemetry: true }); + expect(args).toEqual(["--disable-telemetry"]); + }); + + test("combines multiple options correctly", () => { + const options: WizardOptions = { + integration: "reactNative", + org: "test-org", + project: "test-project", + debug: true, + saas: true, + }; + + const args = _buildWizardArgs(options); + + expect(args).toContain("-i"); + expect(args).toContain("reactNative"); + expect(args).toContain("--org"); + expect(args).toContain("test-org"); + expect(args).toContain("--project"); + expect(args).toContain("test-project"); + expect(args).toContain("--debug"); + expect(args).toContain("--saas"); + }); + + test("preserves argument order for predictable output", () => { + const options: WizardOptions = { + integration: "nextjs", + org: "my-org", + url: "https://custom.sentry.io", + debug: true, + }; + + const args = _buildWizardArgs(options); + + // Verify the order matches the buildWizardArgs implementation + expect(args).toEqual([ + "-i", + "nextjs", + "--org", + "my-org", + "-u", + "https://custom.sentry.io", + "--debug", + ]); + }); + + test("handles all boolean flags together", () => { + const options: WizardOptions = { + debug: true, + uninstall: true, + quiet: true, + skipConnect: true, + saas: true, + signup: true, + disableTelemetry: true, + }; + + const args = _buildWizardArgs(options); + + expect(args).toContain("--debug"); + expect(args).toContain("--uninstall"); + expect(args).toContain("--quiet"); + expect(args).toContain("--skip-connect"); + expect(args).toContain("--saas"); + expect(args).toContain("-s"); + expect(args).toContain("--disable-telemetry"); + expect(args).toHaveLength(7); + }); +}); From 35a7b5a73bc61807636cc51ab929a616cc395871 Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Fri, 6 Feb 2026 00:42:26 +0530 Subject: [PATCH 2/9] fix: added tests and aliases --- src/commands/init.ts | 1 + test/commands/init.test.ts | 230 ++++++++++++++++++++++++++++++++++++- test/lib/wizard.test.ts | 185 +++++++++++++++++++++++++++++ 3 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 test/lib/wizard.test.ts diff --git a/src/commands/init.ts b/src/commands/init.ts index b1183248..fe3e648d 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -104,6 +104,7 @@ export const initCommand = buildCommand({ default: false, }, }, + aliases: { i: "integration", u: "url", s: "signup" }, }, async func(this: SentryContext, flags: InitFlags): Promise { const { stdout } = this; diff --git a/test/commands/init.test.ts b/test/commands/init.test.ts index 5e86857d..9d98f19e 100644 --- a/test/commands/init.test.ts +++ b/test/commands/init.test.ts @@ -4,9 +4,36 @@ * Tests for the sentry init command and wizard flag mapping. */ -import { describe, expect, test } from "bun:test"; +import { beforeEach, describe, expect, mock, test } from "bun:test"; import { _buildWizardArgs, type WizardOptions } from "../../src/lib/wizard.js"; +// Track runWizard calls for assertions +let runWizardCalls: WizardOptions[] = []; +let mockGetDefaultOrg: (() => Promise) | null = null; + +beforeEach(() => { + runWizardCalls = []; + mockGetDefaultOrg = null; + + // Mock runWizard to avoid spawning the wizard process + mock.module("../../src/lib/wizard.js", () => ({ + runWizard: async (options: WizardOptions) => { + runWizardCalls.push(options); + }, + _buildWizardArgs, + })); + + // Mock getDefaultOrganization + mock.module("../../src/lib/db/defaults.js", () => ({ + getDefaultOrganization: async () => { + if (mockGetDefaultOrg) { + return mockGetDefaultOrg(); + } + return null; + }, + })); +}); + describe("buildWizardArgs", () => { test("returns empty array when no options provided", () => { const args = _buildWizardArgs({}); @@ -139,3 +166,204 @@ describe("buildWizardArgs", () => { expect(args).toHaveLength(7); }); }); + +describe("initCommand.func", () => { + test("maps flags to WizardOptions and calls runWizard", async () => { + const { initCommand } = await import("../../src/commands/init.js"); + const func = await initCommand.loader(); + + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + const flags = { + integration: "nextjs", + org: "test-org", + project: "test-project", + url: "https://sentry.example.com", + debug: true, + uninstall: false, + quiet: false, + "skip-connect": true, + saas: true, + signup: false, + "disable-telemetry": true, + }; + + await func.call(mockContext, flags); + + expect(runWizardCalls).toHaveLength(1); + expect(runWizardCalls[0]).toMatchObject({ + integration: "nextjs", + org: "test-org", + project: "test-project", + url: "https://sentry.example.com", + debug: true, + uninstall: false, + quiet: false, + skipConnect: true, + saas: true, + signup: false, + disableTelemetry: true, + }); + }); + + test("auto-populates org from defaults when --org not provided", async () => { + mockGetDefaultOrg = async () => "my-default-org"; + + const { initCommand } = await import("../../src/commands/init.js"); + const func = await initCommand.loader(); + + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + const flags = { + debug: false, + uninstall: false, + quiet: false, + "skip-connect": false, + saas: false, + signup: false, + "disable-telemetry": false, + }; + + await func.call(mockContext, flags); + + expect(runWizardCalls).toHaveLength(1); + expect(runWizardCalls[0].org).toBe("my-default-org"); + expect(stdoutWrite).toHaveBeenCalledWith( + "Using organization: my-default-org\n" + ); + }); + + test("does not override org when explicitly provided", async () => { + mockGetDefaultOrg = async () => "default-org"; + + const { initCommand } = await import("../../src/commands/init.js"); + const func = await initCommand.loader(); + + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + const flags = { + org: "explicit-org", + debug: false, + uninstall: false, + quiet: false, + "skip-connect": false, + saas: false, + signup: false, + "disable-telemetry": false, + }; + + await func.call(mockContext, flags); + + expect(runWizardCalls).toHaveLength(1); + expect(runWizardCalls[0].org).toBe("explicit-org"); + // Should not write "Using organization" when org is explicitly provided + expect(stdoutWrite).not.toHaveBeenCalledWith( + expect.stringContaining("Using organization:") + ); + }); + + test("does not write 'Using organization' when no default org", async () => { + mockGetDefaultOrg = async () => null; + + const { initCommand } = await import("../../src/commands/init.js"); + const func = await initCommand.loader(); + + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + const flags = { + debug: false, + uninstall: false, + quiet: false, + "skip-connect": false, + saas: false, + signup: false, + "disable-telemetry": false, + }; + + await func.call(mockContext, flags); + + expect(runWizardCalls).toHaveLength(1); + expect(runWizardCalls[0].org).toBeUndefined(); + // Should not write "Using organization" when no default org + expect(stdoutWrite).not.toHaveBeenCalledWith( + expect.stringContaining("Using organization:") + ); + }); + + test("writes 'Starting Sentry Wizard...' to stdout", async () => { + const { initCommand } = await import("../../src/commands/init.js"); + const func = await initCommand.loader(); + + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + const flags = { + debug: false, + uninstall: false, + quiet: false, + "skip-connect": false, + saas: false, + signup: false, + "disable-telemetry": false, + }; + + await func.call(mockContext, flags); + + expect(stdoutWrite).toHaveBeenCalledWith("Starting Sentry Wizard...\n\n"); + }); + + test("handles all flags correctly including integration", async () => { + const { initCommand } = await import("../../src/commands/init.js"); + const func = await initCommand.loader(); + + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + const flags = { + integration: "reactNative", + debug: true, + uninstall: true, + quiet: true, + "skip-connect": true, + saas: true, + signup: true, + "disable-telemetry": true, + }; + + await func.call(mockContext, flags); + + expect(runWizardCalls).toHaveLength(1); + expect(runWizardCalls[0]).toMatchObject({ + integration: "reactNative", + debug: true, + uninstall: true, + quiet: true, + skipConnect: true, + saas: true, + signup: true, + disableTelemetry: true, + }); + }); +}); diff --git a/test/lib/wizard.test.ts b/test/lib/wizard.test.ts new file mode 100644 index 00000000..a4613991 --- /dev/null +++ b/test/lib/wizard.test.ts @@ -0,0 +1,185 @@ +/** + * Wizard Module Tests + * + * Tests for the runWizard function that spawns @sentry/wizard. + */ + +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; +import { EventEmitter } from "node:events"; + +// Store original Bun.which for restoration +const originalBunWhich = Bun.which; + +// Mock spawn function and captured calls +let mockSpawn: ReturnType; +let lastSpawnedProc: EventEmitter | null = null; +let spawnCalls: Array<{ command: string; args: string[]; options: unknown }> = + []; + +beforeEach(() => { + spawnCalls = []; + lastSpawnedProc = null; + + // Create mock spawn that returns an EventEmitter + mockSpawn = mock((command: string, args: string[], options: unknown) => { + spawnCalls.push({ command, args, options }); + const proc = new EventEmitter(); + lastSpawnedProc = proc; + return proc; + }); + + // Mock node:child_process + mock.module("node:child_process", () => ({ + spawn: mockSpawn, + })); +}); + +afterEach(() => { + // Restore Bun.which + (Bun as { which: typeof Bun.which }).which = originalBunWhich; +}); + +/** + * Helper to mock Bun.which + */ +function mockBunWhich(returnValue: string | null) { + (Bun as { which: typeof Bun.which }).which = () => returnValue; +} + +describe("runWizard", () => { + test("rejects when npx is not found", async () => { + mockBunWhich(null); + + // Import after mocking + const { runWizard } = await import("../../src/lib/wizard.js"); + + await expect(runWizard({})).rejects.toThrow( + "npx not found. Please install Node.js/npm to use the init command." + ); + }); + + test("resolves when wizard exits with code 0", async () => { + mockBunWhich("/usr/local/bin/npx"); + + const { runWizard } = await import("../../src/lib/wizard.js"); + + const promise = runWizard({}); + + // Give the promise time to set up the event listener + await Bun.sleep(10); + + // Emit close with success code + lastSpawnedProc?.emit("close", 0); + + await expect(promise).resolves.toBeUndefined(); + }); + + test("rejects when wizard exits with non-zero code", async () => { + mockBunWhich("/usr/local/bin/npx"); + + const { runWizard } = await import("../../src/lib/wizard.js"); + + const promise = runWizard({}); + + await Bun.sleep(10); + + // Emit close with error code + lastSpawnedProc?.emit("close", 1); + + await expect(promise).rejects.toThrow("Wizard exited with code 1"); + }); + + test("rejects when spawn emits error", async () => { + mockBunWhich("/usr/local/bin/npx"); + + const { runWizard } = await import("../../src/lib/wizard.js"); + + const promise = runWizard({}); + + await Bun.sleep(10); + + // Emit error event + lastSpawnedProc?.emit("error", new Error("ENOENT: command not found")); + + await expect(promise).rejects.toThrow( + "Failed to start wizard: ENOENT: command not found" + ); + }); + + test("passes correct arguments to spawn with no options", async () => { + mockBunWhich("/usr/local/bin/npx"); + + const { runWizard } = await import("../../src/lib/wizard.js"); + + const promise = runWizard({}); + + await Bun.sleep(10); + lastSpawnedProc?.emit("close", 0); + await promise; + + expect(spawnCalls).toHaveLength(1); + expect(spawnCalls[0].command).toBe("/usr/local/bin/npx"); + expect(spawnCalls[0].args).toEqual(["@sentry/wizard@latest"]); + expect(spawnCalls[0].options).toMatchObject({ + stdio: "inherit", + }); + }); + + test("passes correct arguments to spawn with all options", async () => { + mockBunWhich("/usr/bin/npx"); + + const { runWizard } = await import("../../src/lib/wizard.js"); + + const promise = runWizard({ + integration: "nextjs", + org: "my-org", + project: "my-project", + url: "https://sentry.example.com", + debug: true, + uninstall: true, + quiet: true, + skipConnect: true, + saas: true, + signup: true, + disableTelemetry: true, + }); + + await Bun.sleep(10); + lastSpawnedProc?.emit("close", 0); + await promise; + + expect(spawnCalls).toHaveLength(1); + expect(spawnCalls[0].command).toBe("/usr/bin/npx"); + expect(spawnCalls[0].args).toEqual([ + "@sentry/wizard@latest", + "-i", + "nextjs", + "--org", + "my-org", + "--project", + "my-project", + "-u", + "https://sentry.example.com", + "--debug", + "--uninstall", + "--quiet", + "--skip-connect", + "--saas", + "-s", + "--disable-telemetry", + ]); + }); + + test("rejects with specific exit code in error message", async () => { + mockBunWhich("/usr/local/bin/npx"); + + const { runWizard } = await import("../../src/lib/wizard.js"); + + const promise = runWizard({ integration: "flutter" }); + + await Bun.sleep(10); + lastSpawnedProc?.emit("close", 127); + + await expect(promise).rejects.toThrow("Wizard exited with code 127"); + }); +}); From aec648d25ba670edf5f1530030e46033169c0d90 Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Fri, 6 Feb 2026 01:35:40 +0530 Subject: [PATCH 3/9] feat: added support for authenticated session in the wizard --- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 22 +++ src/commands/init.ts | 97 +++++++++- src/lib/wizard.ts | 52 +++++ test/commands/init.test.ts | 177 ++++++++++++++++-- 4 files changed, 328 insertions(+), 20 deletions(-) diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index b9b550fc..b5ccb04f 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -381,6 +381,28 @@ sentry api /organizations/ --include sentry api /projects/my-org/my-project/issues/ --paginate ``` +### Init + +Initialize Sentry in your project + +#### `sentry init` + +Initialize Sentry in your project + +**Flags:** +- `-i, --integration - Integration to setup (nextjs, reactNative, flutter, etc.)` +- `--org - Sentry organization slug` +- `--project - Sentry project slug` +- `-u, --url - Sentry URL (for self-hosted)` +- `--debug - Enable verbose logging` +- `--uninstall - Revert project setup` +- `--quiet - Don't prompt for input` +- `--skip-connect - Skip connecting to Sentry server` +- `--saas - Skip self-hosted/SaaS selection` +- `-s, --signup - Redirect to signup if not logged in` +- `--disable-telemetry - Don't send telemetry to Sentry` +- `--no-auth - Don't pass existing CLI auth to wizard (force browser login)` + ### Cli CLI-related commands diff --git a/src/commands/init.ts b/src/commands/init.ts index fe3e648d..c22b0ec8 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -8,8 +8,67 @@ import { buildCommand } from "@stricli/core"; import type { SentryContext } from "../context.js"; -import { getDefaultOrganization } from "../lib/db/defaults.js"; -import { runWizard, type WizardOptions } from "../lib/wizard.js"; +import { + getOrganization, + getProject, + getProjectKeys, +} from "../lib/api-client.js"; +import { getAuthToken } from "../lib/db/auth.js"; +import { + getDefaultOrganization, + getDefaultProject, +} from "../lib/db/defaults.js"; +import { getSentryBaseUrl, isSentrySaasUrl } from "../lib/sentry-urls.js"; +import { + type PreSelectedProject, + runWizard, + type WizardOptions, +} from "../lib/wizard.js"; + +/** + * Try to build preSelectedProject data from existing CLI auth. + * Returns undefined if not authenticated or data fetch fails. + */ +async function tryBuildPreSelectedProject( + orgSlug: string, + projectSlug: string, + urlOverride?: string +): Promise { + const token = getAuthToken(); + if (!token) { + return; + } + + try { + const [org, project, keys] = await Promise.all([ + getOrganization(orgSlug), + getProject(orgSlug, projectSlug), + getProjectKeys(orgSlug, projectSlug), + ]); + + const dsn = keys[0]?.dsn?.public; + if (!dsn) { + return; + } + + const baseUrl = urlOverride ?? getSentryBaseUrl(); + const selfHosted = !isSentrySaasUrl(baseUrl); + + return { + authToken: token, + selfHosted, + dsn, + id: project.id, + projectSlug: project.slug, + projectName: project.name, + orgId: org.id, + orgName: org.name, + orgSlug: org.slug, + }; + } catch { + return; + } +} type InitFlags = { readonly integration?: string; @@ -23,6 +82,7 @@ type InitFlags = { readonly saas: boolean; readonly signup: boolean; readonly "disable-telemetry": boolean; + readonly "no-auth": boolean; }; export const initCommand = buildCommand({ @@ -103,6 +163,11 @@ export const initCommand = buildCommand({ brief: "Don't send telemetry to Sentry", default: false, }, + "no-auth": { + kind: "boolean", + brief: "Don't pass existing CLI auth to wizard (force browser login)", + default: false, + }, }, aliases: { i: "integration", u: "url", s: "signup" }, }, @@ -124,12 +189,30 @@ export const initCommand = buildCommand({ disableTelemetry: flags["disable-telemetry"], }; - // Auto-populate org from CLI config if not provided and user is authenticated + // Auto-populate org from CLI config if not provided if (!options.org) { - const defaultOrg = await getDefaultOrganization(); - if (defaultOrg) { - options.org = defaultOrg; - stdout.write(`Using organization: ${defaultOrg}\n`); + options.org = (await getDefaultOrganization()) ?? undefined; + } + + // Auto-populate project from CLI config if not provided + const projectSlug = options.project ?? (await getDefaultProject()); + + // Try to share auth with wizard (unless --no-auth is set) + if (!flags["no-auth"] && options.org && projectSlug) { + const preSelected = await tryBuildPreSelectedProject( + options.org, + projectSlug, + flags.url + ); + if (preSelected) { + options.preSelectedProject = preSelected; + stdout.write( + `Using existing Sentry auth for ${preSelected.orgSlug}/${preSelected.projectSlug}\n` + ); + } else if (flags.debug) { + stdout.write( + "Could not fetch project data, wizard will prompt for login\n" + ); } } diff --git a/src/lib/wizard.ts b/src/lib/wizard.ts index 41f10dbc..ed8f1c2e 100644 --- a/src/lib/wizard.ts +++ b/src/lib/wizard.ts @@ -8,6 +8,31 @@ import { spawn } from "node:child_process"; +/** + * Pre-selected project data for non-interactive wizard mode. + * When provided, the wizard skips browser login and project selection. + */ +export type PreSelectedProject = { + /** Valid Sentry auth token */ + authToken: string; + /** Whether the Sentry instance is self-hosted */ + selfHosted: boolean; + /** Project's public DSN */ + dsn: string; + /** Numeric Sentry project ID */ + id: string; + /** Sentry project slug */ + projectSlug: string; + /** Sentry project display name */ + projectName: string; + /** Numeric Sentry organization ID */ + orgId: string; + /** Sentry organization display name */ + orgName: string; + /** Sentry organization slug */ + orgSlug: string; +}; + /** * Options for running the Sentry Wizard. * These map to our CLI's interface, not the wizard's flags directly. @@ -35,6 +60,8 @@ export type WizardOptions = { signup?: boolean; /** Don't send telemetry data to Sentry */ disableTelemetry?: boolean; + /** Pre-selected project data to skip wizard login flow */ + preSelectedProject?: PreSelectedProject; }; /** @@ -78,6 +105,31 @@ function buildWizardArgs(options: WizardOptions): string[] { args.push("--disable-telemetry"); } + // Pre-selected project data bypasses wizard login flow + if (options.preSelectedProject) { + const p = options.preSelectedProject; + args.push( + "--preSelectedProject.authToken", + p.authToken, + "--preSelectedProject.selfHosted", + String(p.selfHosted), + "--preSelectedProject.dsn", + p.dsn, + "--preSelectedProject.id", + p.id, + "--preSelectedProject.projectSlug", + p.projectSlug, + "--preSelectedProject.projectName", + p.projectName, + "--preSelectedProject.orgId", + p.orgId, + "--preSelectedProject.orgName", + p.orgName, + "--preSelectedProject.orgSlug", + p.orgSlug + ); + } + return args; } diff --git a/test/commands/init.test.ts b/test/commands/init.test.ts index 9d98f19e..6888a97f 100644 --- a/test/commands/init.test.ts +++ b/test/commands/init.test.ts @@ -10,10 +10,14 @@ import { _buildWizardArgs, type WizardOptions } from "../../src/lib/wizard.js"; // Track runWizard calls for assertions let runWizardCalls: WizardOptions[] = []; let mockGetDefaultOrg: (() => Promise) | null = null; +let mockGetDefaultProject: (() => Promise) | null = null; +let mockGetAuthToken: (() => string | undefined) | null = null; beforeEach(() => { runWizardCalls = []; mockGetDefaultOrg = null; + mockGetDefaultProject = null; + mockGetAuthToken = null; // Mock runWizard to avoid spawning the wizard process mock.module("../../src/lib/wizard.js", () => ({ @@ -23,7 +27,7 @@ beforeEach(() => { _buildWizardArgs, })); - // Mock getDefaultOrganization + // Mock defaults mock.module("../../src/lib/db/defaults.js", () => ({ getDefaultOrganization: async () => { if (mockGetDefaultOrg) { @@ -31,6 +35,45 @@ beforeEach(() => { } return null; }, + getDefaultProject: async () => { + if (mockGetDefaultProject) { + return mockGetDefaultProject(); + } + return null; + }, + })); + + // Mock auth + mock.module("../../src/lib/db/auth.js", () => ({ + getAuthToken: () => { + if (mockGetAuthToken) { + return mockGetAuthToken(); + } + return; + }, + })); + + // Mock API client - return empty/mock data + mock.module("../../src/lib/api-client.js", () => ({ + getOrganization: async (orgSlug: string) => ({ + id: "123", + slug: orgSlug, + name: "Test Org", + }), + getProject: async (_orgSlug: string, projectSlug: string) => ({ + id: "456", + slug: projectSlug, + name: "Test Project", + }), + getProjectKeys: async () => [ + { dsn: { public: "https://key@o123.ingest.sentry.io/456" } }, + ], + })); + + // Mock sentry-urls + mock.module("../../src/lib/sentry-urls.js", () => ({ + getSentryBaseUrl: () => "https://sentry.io", + isSentrySaasUrl: () => true, })); }); @@ -165,6 +208,43 @@ describe("buildWizardArgs", () => { expect(args).toContain("--disable-telemetry"); expect(args).toHaveLength(7); }); + + test("maps preSelectedProject to wizard flags", () => { + const options: WizardOptions = { + preSelectedProject: { + authToken: "test-token", + selfHosted: false, + dsn: "https://key@o123.ingest.sentry.io/456", + id: "456", + projectSlug: "my-project", + projectName: "My Project", + orgId: "123", + orgName: "My Org", + orgSlug: "my-org", + }, + }; + + const args = _buildWizardArgs(options); + + expect(args).toContain("--preSelectedProject.authToken"); + expect(args).toContain("test-token"); + expect(args).toContain("--preSelectedProject.selfHosted"); + expect(args).toContain("false"); + expect(args).toContain("--preSelectedProject.dsn"); + expect(args).toContain("https://key@o123.ingest.sentry.io/456"); + expect(args).toContain("--preSelectedProject.id"); + expect(args).toContain("456"); + expect(args).toContain("--preSelectedProject.projectSlug"); + expect(args).toContain("my-project"); + expect(args).toContain("--preSelectedProject.projectName"); + expect(args).toContain("My Project"); + expect(args).toContain("--preSelectedProject.orgId"); + expect(args).toContain("123"); + expect(args).toContain("--preSelectedProject.orgName"); + expect(args).toContain("My Org"); + expect(args).toContain("--preSelectedProject.orgSlug"); + expect(args).toContain("my-org"); + }); }); describe("initCommand.func", () => { @@ -190,6 +270,7 @@ describe("initCommand.func", () => { saas: true, signup: false, "disable-telemetry": true, + "no-auth": true, // Skip auth sharing for basic flag mapping test }; await func.call(mockContext, flags); @@ -230,15 +311,13 @@ describe("initCommand.func", () => { saas: false, signup: false, "disable-telemetry": false, + "no-auth": true, // Skip auth sharing }; await func.call(mockContext, flags); expect(runWizardCalls).toHaveLength(1); expect(runWizardCalls[0].org).toBe("my-default-org"); - expect(stdoutWrite).toHaveBeenCalledWith( - "Using organization: my-default-org\n" - ); }); test("does not override org when explicitly provided", async () => { @@ -262,19 +341,16 @@ describe("initCommand.func", () => { saas: false, signup: false, "disable-telemetry": false, + "no-auth": true, // Skip auth sharing }; await func.call(mockContext, flags); expect(runWizardCalls).toHaveLength(1); expect(runWizardCalls[0].org).toBe("explicit-org"); - // Should not write "Using organization" when org is explicitly provided - expect(stdoutWrite).not.toHaveBeenCalledWith( - expect.stringContaining("Using organization:") - ); }); - test("does not write 'Using organization' when no default org", async () => { + test("org is undefined when no default org", async () => { mockGetDefaultOrg = async () => null; const { initCommand } = await import("../../src/commands/init.js"); @@ -294,16 +370,13 @@ describe("initCommand.func", () => { saas: false, signup: false, "disable-telemetry": false, + "no-auth": true, // Skip auth sharing }; await func.call(mockContext, flags); expect(runWizardCalls).toHaveLength(1); expect(runWizardCalls[0].org).toBeUndefined(); - // Should not write "Using organization" when no default org - expect(stdoutWrite).not.toHaveBeenCalledWith( - expect.stringContaining("Using organization:") - ); }); test("writes 'Starting Sentry Wizard...' to stdout", async () => { @@ -324,6 +397,7 @@ describe("initCommand.func", () => { saas: false, signup: false, "disable-telemetry": false, + "no-auth": true, // Skip auth sharing }; await func.call(mockContext, flags); @@ -350,6 +424,7 @@ describe("initCommand.func", () => { saas: true, signup: true, "disable-telemetry": true, + "no-auth": true, // Skip auth sharing }; await func.call(mockContext, flags); @@ -366,4 +441,80 @@ describe("initCommand.func", () => { disableTelemetry: true, }); }); + + test("shares auth with wizard when authenticated with org and project", async () => { + mockGetDefaultOrg = async () => "my-org"; + mockGetDefaultProject = async () => "my-project"; + mockGetAuthToken = () => "test-token-123"; + + const { initCommand } = await import("../../src/commands/init.js"); + const func = await initCommand.loader(); + + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + const flags = { + debug: false, + uninstall: false, + quiet: false, + "skip-connect": false, + saas: false, + signup: false, + "disable-telemetry": false, + "no-auth": false, // Enable auth sharing + }; + + await func.call(mockContext, flags); + + expect(runWizardCalls).toHaveLength(1); + expect(runWizardCalls[0].preSelectedProject).toBeDefined(); + expect(runWizardCalls[0].preSelectedProject?.authToken).toBe( + "test-token-123" + ); + expect(runWizardCalls[0].preSelectedProject?.orgSlug).toBe("my-org"); + expect(runWizardCalls[0].preSelectedProject?.projectSlug).toBe( + "my-project" + ); + expect(runWizardCalls[0].preSelectedProject?.selfHosted).toBe(false); + expect(stdoutWrite).toHaveBeenCalledWith( + "Using existing Sentry auth for my-org/my-project\n" + ); + }); + + test("skips auth sharing when --no-auth flag is set", async () => { + mockGetDefaultOrg = async () => "my-org"; + mockGetDefaultProject = async () => "my-project"; + mockGetAuthToken = () => "test-token-123"; + + const { initCommand } = await import("../../src/commands/init.js"); + const func = await initCommand.loader(); + + const stdoutWrite = mock(() => true); + const mockContext = { + stdout: { write: stdoutWrite }, + stderr: { write: mock(() => true) }, + }; + + const flags = { + debug: false, + uninstall: false, + quiet: false, + "skip-connect": false, + saas: false, + signup: false, + "disable-telemetry": false, + "no-auth": true, // Disable auth sharing + }; + + await func.call(mockContext, flags); + + expect(runWizardCalls).toHaveLength(1); + expect(runWizardCalls[0].preSelectedProject).toBeUndefined(); + expect(stdoutWrite).not.toHaveBeenCalledWith( + expect.stringContaining("Using existing Sentry auth") + ); + }); }); From bfed043c904ee38bfbdb9a0cad93e12ffcd28985 Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Fri, 6 Feb 2026 16:40:44 +0530 Subject: [PATCH 4/9] feat: added sentry-cli --- src/app.ts | 20 +++++++++ src/commands/build/index.ts | 16 +++++++ src/commands/build/upload.ts | 36 ++++++++++++++++ src/commands/debug-files/bundle-jvm.ts | 39 +++++++++++++++++ src/commands/debug-files/bundle-sources.ts | 39 +++++++++++++++++ src/commands/debug-files/check.ts | 39 +++++++++++++++++ src/commands/debug-files/find.ts | 39 +++++++++++++++++ src/commands/debug-files/index.ts | 31 ++++++++++++++ src/commands/debug-files/print-sources.ts | 39 +++++++++++++++++ src/commands/debug-files/upload.ts | 40 +++++++++++++++++ src/commands/deploys/index.ts | 19 ++++++++ src/commands/deploys/list.ts | 39 +++++++++++++++++ src/commands/deploys/new.ts | 39 +++++++++++++++++ src/commands/monitors/index.ts | 19 ++++++++ src/commands/monitors/list.ts | 39 +++++++++++++++++ src/commands/monitors/run.ts | 39 +++++++++++++++++ src/commands/react-native/gradle.ts | 39 +++++++++++++++++ src/commands/react-native/index.ts | 19 ++++++++ src/commands/react-native/xcode.ts | 40 +++++++++++++++++ src/commands/releases/archive.ts | 39 +++++++++++++++++ src/commands/releases/delete.ts | 39 +++++++++++++++++ src/commands/releases/finalize.ts | 39 +++++++++++++++++ src/commands/releases/index.ts | 40 +++++++++++++++++ src/commands/releases/info.ts | 39 +++++++++++++++++ src/commands/releases/list.ts | 40 +++++++++++++++++ src/commands/releases/new.ts | 40 +++++++++++++++++ src/commands/releases/propose-version.ts | 39 +++++++++++++++++ src/commands/releases/restore.ts | 39 +++++++++++++++++ src/commands/releases/set-commits.ts | 40 +++++++++++++++++ src/commands/repos/index.ts | 16 +++++++ src/commands/repos/list.ts | 40 +++++++++++++++++ src/commands/send-envelope.ts | 39 +++++++++++++++++ src/commands/send-event.ts | 41 ++++++++++++++++++ src/commands/sourcemaps/index.ts | 22 ++++++++++ src/commands/sourcemaps/inject.ts | 39 +++++++++++++++++ src/commands/sourcemaps/resolve.ts | 39 +++++++++++++++++ src/commands/sourcemaps/upload.ts | 40 +++++++++++++++++ src/lib/sentry-cli-runner.ts | 50 ++++++++++++++++++++++ 38 files changed, 1350 insertions(+) create mode 100644 src/commands/build/index.ts create mode 100644 src/commands/build/upload.ts create mode 100644 src/commands/debug-files/bundle-jvm.ts create mode 100644 src/commands/debug-files/bundle-sources.ts create mode 100644 src/commands/debug-files/check.ts create mode 100644 src/commands/debug-files/find.ts create mode 100644 src/commands/debug-files/index.ts create mode 100644 src/commands/debug-files/print-sources.ts create mode 100644 src/commands/debug-files/upload.ts create mode 100644 src/commands/deploys/index.ts create mode 100644 src/commands/deploys/list.ts create mode 100644 src/commands/deploys/new.ts create mode 100644 src/commands/monitors/index.ts create mode 100644 src/commands/monitors/list.ts create mode 100644 src/commands/monitors/run.ts create mode 100644 src/commands/react-native/gradle.ts create mode 100644 src/commands/react-native/index.ts create mode 100644 src/commands/react-native/xcode.ts create mode 100644 src/commands/releases/archive.ts create mode 100644 src/commands/releases/delete.ts create mode 100644 src/commands/releases/finalize.ts create mode 100644 src/commands/releases/index.ts create mode 100644 src/commands/releases/info.ts create mode 100644 src/commands/releases/list.ts create mode 100644 src/commands/releases/new.ts create mode 100644 src/commands/releases/propose-version.ts create mode 100644 src/commands/releases/restore.ts create mode 100644 src/commands/releases/set-commits.ts create mode 100644 src/commands/repos/index.ts create mode 100644 src/commands/repos/list.ts create mode 100644 src/commands/send-envelope.ts create mode 100644 src/commands/send-event.ts create mode 100644 src/commands/sourcemaps/index.ts create mode 100644 src/commands/sourcemaps/inject.ts create mode 100644 src/commands/sourcemaps/resolve.ts create mode 100644 src/commands/sourcemaps/upload.ts create mode 100644 src/lib/sentry-cli-runner.ts diff --git a/src/app.ts b/src/app.ts index 4fc666bb..4e605fa3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,13 +8,23 @@ import { } from "@stricli/core"; import { apiCommand } from "./commands/api.js"; import { authRoute } from "./commands/auth/index.js"; +import { buildCommandRoute } from "./commands/build/index.js"; import { cliRoute } from "./commands/cli/index.js"; +import { debugFilesRoute } from "./commands/debug-files/index.js"; +import { deploysRoute } from "./commands/deploys/index.js"; import { eventRoute } from "./commands/event/index.js"; import { helpCommand } from "./commands/help.js"; import { initCommand } from "./commands/init.js"; import { issueRoute } from "./commands/issue/index.js"; +import { monitorsRoute } from "./commands/monitors/index.js"; import { orgRoute } from "./commands/org/index.js"; import { projectRoute } from "./commands/project/index.js"; +import { reactNativeRoute } from "./commands/react-native/index.js"; +import { releasesRoute } from "./commands/releases/index.js"; +import { reposRoute } from "./commands/repos/index.js"; +import { sendEnvelopeCommand } from "./commands/send-envelope.js"; +import { sendEventCommand } from "./commands/send-event.js"; +import { sourcemapsRoute } from "./commands/sourcemaps/index.js"; import { CLI_VERSION } from "./lib/constants.js"; import { CliError, getExitCode } from "./lib/errors.js"; import { error as errorColor } from "./lib/formatters/colors.js"; @@ -31,6 +41,16 @@ export const routes = buildRouteMap({ issue: issueRoute, event: eventRoute, api: apiCommand, + releases: releasesRoute, + sourcemaps: sourcemapsRoute, + "debug-files": debugFilesRoute, + deploys: deploysRoute, + monitors: monitorsRoute, + repos: reposRoute, + build: buildCommandRoute, + "react-native": reactNativeRoute, + "send-event": sendEventCommand, + "send-envelope": sendEnvelopeCommand, }, defaultCommand: "help", docs: { diff --git a/src/commands/build/index.ts b/src/commands/build/index.ts new file mode 100644 index 00000000..87ba7ee6 --- /dev/null +++ b/src/commands/build/index.ts @@ -0,0 +1,16 @@ +import { buildRouteMap } from "@stricli/core"; +import { uploadCommand } from "./upload.js"; + +export const buildCommandRoute = buildRouteMap({ + routes: { + upload: uploadCommand, + }, + docs: { + brief: "Manage builds", + fullDescription: + "Manage builds on Sentry.\n\n" + + "Commands:\n" + + " upload Upload build artifacts", + hideRoute: {}, + }, +}); diff --git a/src/commands/build/upload.ts b/src/commands/build/upload.ts new file mode 100644 index 00000000..83d59105 --- /dev/null +++ b/src/commands/build/upload.ts @@ -0,0 +1,36 @@ +/** + * sentry build upload + * + * Upload build artifacts to Sentry. + * Wraps: sentry-cli build upload + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const uploadCommand = buildCommand({ + docs: { + brief: "Upload build artifacts", + fullDescription: + "Upload build artifacts to Sentry.\n\n" + + "Wraps: sentry-cli build upload\n\n" + + "Note: This command is restricted to Sentry SaaS.\n\n" + + "Examples:\n" + + " sentry build upload", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func(this: SentryContext, _flags: Record, ...args: string[]): Promise { + await runSentryCli(["build", "upload", ...args]); + }, +}); diff --git a/src/commands/debug-files/bundle-jvm.ts b/src/commands/debug-files/bundle-jvm.ts new file mode 100644 index 00000000..3c0ec2f3 --- /dev/null +++ b/src/commands/debug-files/bundle-jvm.ts @@ -0,0 +1,39 @@ +/** + * sentry debug-files bundle-jvm + * + * Bundle JVM debug information files. + * Wraps: sentry-cli debug-files bundle-jvm + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const bundleJvmCommand = buildCommand({ + docs: { + brief: "Bundle JVM debug files", + fullDescription: + "Bundle JVM debug information files.\n\n" + + "Wraps: sentry-cli debug-files bundle-jvm\n\n" + + "Examples:\n" + + " sentry debug-files bundle-jvm ./path/to/classes", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["debug-files", "bundle-jvm", ...args]); + }, +}); diff --git a/src/commands/debug-files/bundle-sources.ts b/src/commands/debug-files/bundle-sources.ts new file mode 100644 index 00000000..3468b2e9 --- /dev/null +++ b/src/commands/debug-files/bundle-sources.ts @@ -0,0 +1,39 @@ +/** + * sentry debug-files bundle-sources + * + * Bundle source files for debug information. + * Wraps: sentry-cli debug-files bundle-sources + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const bundleSourcesCommand = buildCommand({ + docs: { + brief: "Bundle source files", + fullDescription: + "Bundle source files for debug information.\n\n" + + "Wraps: sentry-cli debug-files bundle-sources\n\n" + + "Examples:\n" + + " sentry debug-files bundle-sources ./path/to/debug-file", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["debug-files", "bundle-sources", ...args]); + }, +}); diff --git a/src/commands/debug-files/check.ts b/src/commands/debug-files/check.ts new file mode 100644 index 00000000..443937ff --- /dev/null +++ b/src/commands/debug-files/check.ts @@ -0,0 +1,39 @@ +/** + * sentry debug-files check + * + * Check debug information files for issues. + * Wraps: sentry-cli debug-files check + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const checkCommand = buildCommand({ + docs: { + brief: "Check debug files for issues", + fullDescription: + "Check debug information files for issues.\n\n" + + "Wraps: sentry-cli debug-files check\n\n" + + "Examples:\n" + + " sentry debug-files check ./path/to/file", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["debug-files", "check", ...args]); + }, +}); diff --git a/src/commands/debug-files/find.ts b/src/commands/debug-files/find.ts new file mode 100644 index 00000000..2e20f816 --- /dev/null +++ b/src/commands/debug-files/find.ts @@ -0,0 +1,39 @@ +/** + * sentry debug-files find + * + * Find debug information files. + * Wraps: sentry-cli debug-files find + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const findCommand = buildCommand({ + docs: { + brief: "Find debug information files", + fullDescription: + "Find debug information files in the given path.\n\n" + + "Wraps: sentry-cli debug-files find\n\n" + + "Examples:\n" + + " sentry debug-files find ./path/to/search", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["debug-files", "find", ...args]); + }, +}); diff --git a/src/commands/debug-files/index.ts b/src/commands/debug-files/index.ts new file mode 100644 index 00000000..7c88f9ff --- /dev/null +++ b/src/commands/debug-files/index.ts @@ -0,0 +1,31 @@ +import { buildRouteMap } from "@stricli/core"; +import { bundleJvmCommand } from "./bundle-jvm.js"; +import { bundleSourcesCommand } from "./bundle-sources.js"; +import { checkCommand } from "./check.js"; +import { findCommand } from "./find.js"; +import { printSourcesCommand } from "./print-sources.js"; +import { uploadCommand } from "./upload.js"; + +export const debugFilesRoute = buildRouteMap({ + routes: { + upload: uploadCommand, + check: checkCommand, + find: findCommand, + "bundle-sources": bundleSourcesCommand, + "bundle-jvm": bundleJvmCommand, + "print-sources": printSourcesCommand, + }, + docs: { + brief: "Locate, analyze or upload debug information files", + fullDescription: + "Locate, analyze or upload debug information files.\n\n" + + "Commands:\n" + + " upload Upload debug information files\n" + + " check Check debug files for issues\n" + + " find Find debug information files\n" + + " bundle-sources Bundle source files\n" + + " bundle-jvm Bundle JVM debug files\n" + + " print-sources Print embedded source files", + hideRoute: {}, + }, +}); diff --git a/src/commands/debug-files/print-sources.ts b/src/commands/debug-files/print-sources.ts new file mode 100644 index 00000000..1ae6dbbc --- /dev/null +++ b/src/commands/debug-files/print-sources.ts @@ -0,0 +1,39 @@ +/** + * sentry debug-files print-sources + * + * Print source files embedded in debug information files. + * Wraps: sentry-cli debug-files print-sources + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const printSourcesCommand = buildCommand({ + docs: { + brief: "Print embedded source files", + fullDescription: + "Print source files embedded in debug information files.\n\n" + + "Wraps: sentry-cli debug-files print-sources\n\n" + + "Examples:\n" + + " sentry debug-files print-sources ./path/to/debug-file", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["debug-files", "print-sources", ...args]); + }, +}); diff --git a/src/commands/debug-files/upload.ts b/src/commands/debug-files/upload.ts new file mode 100644 index 00000000..92d95e87 --- /dev/null +++ b/src/commands/debug-files/upload.ts @@ -0,0 +1,40 @@ +/** + * sentry debug-files upload + * + * Upload debug information files to Sentry. + * Wraps: sentry-cli debug-files upload + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const uploadCommand = buildCommand({ + docs: { + brief: "Upload debug information files", + fullDescription: + "Upload debug information files to Sentry.\n\n" + + "Wraps: sentry-cli debug-files upload\n\n" + + "Examples:\n" + + " sentry debug-files upload ./path/to/dsyms\n" + + " sentry debug-files upload --include-sources ./path/to/dsyms", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["debug-files", "upload", ...args]); + }, +}); diff --git a/src/commands/deploys/index.ts b/src/commands/deploys/index.ts new file mode 100644 index 00000000..633dd555 --- /dev/null +++ b/src/commands/deploys/index.ts @@ -0,0 +1,19 @@ +import { buildRouteMap } from "@stricli/core"; +import { listCommand } from "./list.js"; +import { newCommand } from "./new.js"; + +export const deploysRoute = buildRouteMap({ + routes: { + list: listCommand, + new: newCommand, + }, + docs: { + brief: "Manage deployments for Sentry releases", + fullDescription: + "Manage deployments for Sentry releases.\n\n" + + "Commands:\n" + + " list List deployments\n" + + " new Create a new deployment", + hideRoute: {}, + }, +}); diff --git a/src/commands/deploys/list.ts b/src/commands/deploys/list.ts new file mode 100644 index 00000000..78beba20 --- /dev/null +++ b/src/commands/deploys/list.ts @@ -0,0 +1,39 @@ +/** + * sentry deploys list + * + * List deployments for a release. + * Wraps: sentry-cli deploys list + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const listCommand = buildCommand({ + docs: { + brief: "List deployments", + fullDescription: + "List deployments for a Sentry release.\n\n" + + "Wraps: sentry-cli deploys list\n\n" + + "Examples:\n" + + " sentry deploys list --org my-org --project my-project --release 1.0.0", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["deploys", "list", ...args]); + }, +}); diff --git a/src/commands/deploys/new.ts b/src/commands/deploys/new.ts new file mode 100644 index 00000000..259c4bb6 --- /dev/null +++ b/src/commands/deploys/new.ts @@ -0,0 +1,39 @@ +/** + * sentry deploys new + * + * Create a new deployment for a release. + * Wraps: sentry-cli deploys new + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const newCommand = buildCommand({ + docs: { + brief: "Create a new deployment", + fullDescription: + "Create a new deployment for a Sentry release.\n\n" + + "Wraps: sentry-cli deploys new\n\n" + + "Examples:\n" + + " sentry deploys new --env production --release 1.0.0", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["deploys", "new", ...args]); + }, +}); diff --git a/src/commands/monitors/index.ts b/src/commands/monitors/index.ts new file mode 100644 index 00000000..12980cbd --- /dev/null +++ b/src/commands/monitors/index.ts @@ -0,0 +1,19 @@ +import { buildRouteMap } from "@stricli/core"; +import { listCommand } from "./list.js"; +import { runMonitorCommand } from "./run.js"; + +export const monitorsRoute = buildRouteMap({ + routes: { + list: listCommand, + run: runMonitorCommand, + }, + docs: { + brief: "Manage cron monitors on Sentry", + fullDescription: + "Manage cron monitors on Sentry.\n\n" + + "Commands:\n" + + " list List cron monitors\n" + + " run Run a command and report to a cron monitor", + hideRoute: {}, + }, +}); diff --git a/src/commands/monitors/list.ts b/src/commands/monitors/list.ts new file mode 100644 index 00000000..c96e6cf0 --- /dev/null +++ b/src/commands/monitors/list.ts @@ -0,0 +1,39 @@ +/** + * sentry monitors list + * + * List cron monitors in Sentry. + * Wraps: sentry-cli monitors list + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const listCommand = buildCommand({ + docs: { + brief: "List cron monitors", + fullDescription: + "List cron monitors in Sentry.\n\n" + + "Wraps: sentry-cli monitors list\n\n" + + "Examples:\n" + + " sentry monitors list", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["monitors", "list", ...args]); + }, +}); diff --git a/src/commands/monitors/run.ts b/src/commands/monitors/run.ts new file mode 100644 index 00000000..c23ceb0a --- /dev/null +++ b/src/commands/monitors/run.ts @@ -0,0 +1,39 @@ +/** + * sentry monitors run + * + * Run a command and report to a cron monitor. + * Wraps: sentry-cli monitors run + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const runMonitorCommand = buildCommand({ + docs: { + brief: "Run a command and report to a cron monitor", + fullDescription: + "Run a command and report its status to a Sentry cron monitor.\n\n" + + "Wraps: sentry-cli monitors run\n\n" + + "Examples:\n" + + " sentry monitors run my-monitor -- ./my-script.sh", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["monitors", "run", ...args]); + }, +}); diff --git a/src/commands/react-native/gradle.ts b/src/commands/react-native/gradle.ts new file mode 100644 index 00000000..5127cfd0 --- /dev/null +++ b/src/commands/react-native/gradle.ts @@ -0,0 +1,39 @@ +/** + * sentry react-native gradle + * + * Upload React Native Android build artifacts. + * Wraps: sentry-cli react-native gradle + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const gradleCommand = buildCommand({ + docs: { + brief: "Upload React Native Android build artifacts", + fullDescription: + "Upload React Native Android build artifacts to Sentry.\n\n" + + "Wraps: sentry-cli react-native gradle\n\n" + + "Examples:\n" + + " sentry react-native gradle", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["react-native", "gradle", ...args]); + }, +}); diff --git a/src/commands/react-native/index.ts b/src/commands/react-native/index.ts new file mode 100644 index 00000000..696c6e30 --- /dev/null +++ b/src/commands/react-native/index.ts @@ -0,0 +1,19 @@ +import { buildRouteMap } from "@stricli/core"; +import { gradleCommand } from "./gradle.js"; +import { xcodeCommand } from "./xcode.js"; + +export const reactNativeRoute = buildRouteMap({ + routes: { + gradle: gradleCommand, + xcode: xcodeCommand, + }, + docs: { + brief: "Upload build artifacts for React Native projects", + fullDescription: + "Upload build artifacts for React Native projects.\n\n" + + "Commands:\n" + + " gradle Upload Android build artifacts\n" + + " xcode Upload iOS build artifacts (macOS only)", + hideRoute: {}, + }, +}); diff --git a/src/commands/react-native/xcode.ts b/src/commands/react-native/xcode.ts new file mode 100644 index 00000000..516b6572 --- /dev/null +++ b/src/commands/react-native/xcode.ts @@ -0,0 +1,40 @@ +/** + * sentry react-native xcode + * + * Upload React Native iOS build artifacts (macOS only). + * Wraps: sentry-cli react-native xcode + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const xcodeCommand = buildCommand({ + docs: { + brief: "Upload React Native iOS build artifacts", + fullDescription: + "Upload React Native iOS build artifacts to Sentry.\n\n" + + "Wraps: sentry-cli react-native xcode\n\n" + + "Note: This command is only available on macOS.\n\n" + + "Examples:\n" + + " sentry react-native xcode", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["react-native", "xcode", ...args]); + }, +}); diff --git a/src/commands/releases/archive.ts b/src/commands/releases/archive.ts new file mode 100644 index 00000000..7ac9047c --- /dev/null +++ b/src/commands/releases/archive.ts @@ -0,0 +1,39 @@ +/** + * sentry releases archive + * + * Archive a release in Sentry. + * Wraps: sentry-cli releases archive + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const archiveCommand = buildCommand({ + docs: { + brief: "Archive a release", + fullDescription: + "Archive a release in Sentry.\n\n" + + "Wraps: sentry-cli releases archive\n\n" + + "Examples:\n" + + " sentry releases archive 1.0.0", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["releases", "archive", ...args]); + }, +}); diff --git a/src/commands/releases/delete.ts b/src/commands/releases/delete.ts new file mode 100644 index 00000000..49c6f8cf --- /dev/null +++ b/src/commands/releases/delete.ts @@ -0,0 +1,39 @@ +/** + * sentry releases delete + * + * Delete a release in Sentry. + * Wraps: sentry-cli releases delete + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const deleteCommand = buildCommand({ + docs: { + brief: "Delete a release", + fullDescription: + "Delete a release in Sentry.\n\n" + + "Wraps: sentry-cli releases delete\n\n" + + "Examples:\n" + + " sentry releases delete 1.0.0", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["releases", "delete", ...args]); + }, +}); diff --git a/src/commands/releases/finalize.ts b/src/commands/releases/finalize.ts new file mode 100644 index 00000000..65f78492 --- /dev/null +++ b/src/commands/releases/finalize.ts @@ -0,0 +1,39 @@ +/** + * sentry releases finalize + * + * Finalize a release in Sentry. + * Wraps: sentry-cli releases finalize + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const finalizeCommand = buildCommand({ + docs: { + brief: "Finalize a release", + fullDescription: + "Finalize a release in Sentry.\n\n" + + "Wraps: sentry-cli releases finalize\n\n" + + "Examples:\n" + + " sentry releases finalize 1.0.0", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["releases", "finalize", ...args]); + }, +}); diff --git a/src/commands/releases/index.ts b/src/commands/releases/index.ts new file mode 100644 index 00000000..1cbe34f6 --- /dev/null +++ b/src/commands/releases/index.ts @@ -0,0 +1,40 @@ +import { buildRouteMap } from "@stricli/core"; +import { archiveCommand } from "./archive.js"; +import { deleteCommand } from "./delete.js"; +import { finalizeCommand } from "./finalize.js"; +import { infoCommand } from "./info.js"; +import { listCommand } from "./list.js"; +import { newCommand } from "./new.js"; +import { proposeVersionCommand } from "./propose-version.js"; +import { restoreCommand } from "./restore.js"; +import { setCommitsCommand } from "./set-commits.js"; + +export const releasesRoute = buildRouteMap({ + routes: { + new: newCommand, + finalize: finalizeCommand, + list: listCommand, + info: infoCommand, + delete: deleteCommand, + archive: archiveCommand, + restore: restoreCommand, + "set-commits": setCommitsCommand, + "propose-version": proposeVersionCommand, + }, + docs: { + brief: "Manage releases on Sentry", + fullDescription: + "Manage releases on Sentry.\n\n" + + "Commands:\n" + + " new Create a new release\n" + + " finalize Finalize a release\n" + + " list List releases\n" + + " info Show release info\n" + + " delete Delete a release\n" + + " archive Archive a release\n" + + " restore Restore an archived release\n" + + " set-commits Associate commits with a release\n" + + " propose-version Propose a version string", + hideRoute: {}, + }, +}); diff --git a/src/commands/releases/info.ts b/src/commands/releases/info.ts new file mode 100644 index 00000000..3e67ced8 --- /dev/null +++ b/src/commands/releases/info.ts @@ -0,0 +1,39 @@ +/** + * sentry releases info + * + * Show information about a release. + * Wraps: sentry-cli releases info + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const infoCommand = buildCommand({ + docs: { + brief: "Show release info", + fullDescription: + "Show information about a release in Sentry.\n\n" + + "Wraps: sentry-cli releases info\n\n" + + "Examples:\n" + + " sentry releases info 1.0.0", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["releases", "info", ...args]); + }, +}); diff --git a/src/commands/releases/list.ts b/src/commands/releases/list.ts new file mode 100644 index 00000000..677825b0 --- /dev/null +++ b/src/commands/releases/list.ts @@ -0,0 +1,40 @@ +/** + * sentry releases list + * + * List releases in Sentry. + * Wraps: sentry-cli releases list + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const listCommand = buildCommand({ + docs: { + brief: "List releases", + fullDescription: + "List releases in Sentry.\n\n" + + "Wraps: sentry-cli releases list\n\n" + + "Examples:\n" + + " sentry releases list\n" + + " sentry releases list --org my-org --project my-project", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["releases", "list", ...args]); + }, +}); diff --git a/src/commands/releases/new.ts b/src/commands/releases/new.ts new file mode 100644 index 00000000..e3db6b3f --- /dev/null +++ b/src/commands/releases/new.ts @@ -0,0 +1,40 @@ +/** + * sentry releases new + * + * Create a new release in Sentry. + * Wraps: sentry-cli releases new + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const newCommand = buildCommand({ + docs: { + brief: "Create a new release", + fullDescription: + "Create a new release in Sentry.\n\n" + + "Wraps: sentry-cli releases new\n\n" + + "Examples:\n" + + " sentry releases new 1.0.0\n" + + " sentry releases new 1.0.0 --org my-org --project my-project", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["releases", "new", ...args]); + }, +}); diff --git a/src/commands/releases/propose-version.ts b/src/commands/releases/propose-version.ts new file mode 100644 index 00000000..13ff208a --- /dev/null +++ b/src/commands/releases/propose-version.ts @@ -0,0 +1,39 @@ +/** + * sentry releases propose-version + * + * Propose a version string for a new release. + * Wraps: sentry-cli releases propose-version + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const proposeVersionCommand = buildCommand({ + docs: { + brief: "Propose a version string", + fullDescription: + "Propose a version string for a new release based on commit history.\n\n" + + "Wraps: sentry-cli releases propose-version\n\n" + + "Examples:\n" + + " sentry releases propose-version", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["releases", "propose-version", ...args]); + }, +}); diff --git a/src/commands/releases/restore.ts b/src/commands/releases/restore.ts new file mode 100644 index 00000000..ed03b080 --- /dev/null +++ b/src/commands/releases/restore.ts @@ -0,0 +1,39 @@ +/** + * sentry releases restore + * + * Restore an archived release in Sentry. + * Wraps: sentry-cli releases restore + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const restoreCommand = buildCommand({ + docs: { + brief: "Restore an archived release", + fullDescription: + "Restore an archived release in Sentry.\n\n" + + "Wraps: sentry-cli releases restore\n\n" + + "Examples:\n" + + " sentry releases restore 1.0.0", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["releases", "restore", ...args]); + }, +}); diff --git a/src/commands/releases/set-commits.ts b/src/commands/releases/set-commits.ts new file mode 100644 index 00000000..767830d4 --- /dev/null +++ b/src/commands/releases/set-commits.ts @@ -0,0 +1,40 @@ +/** + * sentry releases set-commits + * + * Associate commits with a release. + * Wraps: sentry-cli releases set-commits + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const setCommitsCommand = buildCommand({ + docs: { + brief: "Associate commits with a release", + fullDescription: + "Associate commits with a release in Sentry.\n\n" + + "Wraps: sentry-cli releases set-commits\n\n" + + "Examples:\n" + + " sentry releases set-commits 1.0.0 --auto\n" + + " sentry releases set-commits 1.0.0 --commit 'my-org/my-repo@from..to'", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["releases", "set-commits", ...args]); + }, +}); diff --git a/src/commands/repos/index.ts b/src/commands/repos/index.ts new file mode 100644 index 00000000..0817193f --- /dev/null +++ b/src/commands/repos/index.ts @@ -0,0 +1,16 @@ +import { buildRouteMap } from "@stricli/core"; +import { listCommand } from "./list.js"; + +export const reposRoute = buildRouteMap({ + routes: { + list: listCommand, + }, + docs: { + brief: "Manage repositories on Sentry", + fullDescription: + "Manage repositories configured in Sentry.\n\n" + + "Commands:\n" + + " list List repositories", + hideRoute: {}, + }, +}); diff --git a/src/commands/repos/list.ts b/src/commands/repos/list.ts new file mode 100644 index 00000000..5dca12b8 --- /dev/null +++ b/src/commands/repos/list.ts @@ -0,0 +1,40 @@ +/** + * sentry repos list + * + * List repositories in Sentry. + * Wraps: sentry-cli repos list + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const listCommand = buildCommand({ + docs: { + brief: "List repositories", + fullDescription: + "List repositories configured in Sentry.\n\n" + + "Wraps: sentry-cli repos list\n\n" + + "Examples:\n" + + " sentry repos list\n" + + " sentry repos list --org my-org", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["repos", "list", ...args]); + }, +}); diff --git a/src/commands/send-envelope.ts b/src/commands/send-envelope.ts new file mode 100644 index 00000000..e0adf0f7 --- /dev/null +++ b/src/commands/send-envelope.ts @@ -0,0 +1,39 @@ +/** + * sentry send-envelope + * + * Send a raw envelope to Sentry. + * Wraps: sentry-cli send-envelope + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../context.js"; +import { runSentryCli } from "../lib/sentry-cli-runner.js"; + +export const sendEnvelopeCommand = buildCommand({ + docs: { + brief: "Send an envelope to Sentry", + fullDescription: + "Send a raw envelope to Sentry from the command line.\n\n" + + "Wraps: sentry-cli send-envelope\n\n" + + "Examples:\n" + + " sentry send-envelope ./path/to/envelope", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["send-envelope", ...args]); + }, +}); diff --git a/src/commands/send-event.ts b/src/commands/send-event.ts new file mode 100644 index 00000000..1b3a2eca --- /dev/null +++ b/src/commands/send-event.ts @@ -0,0 +1,41 @@ +/** + * sentry send-event + * + * Send an event to Sentry. + * Wraps: sentry-cli send-event + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../context.js"; +import { runSentryCli } from "../lib/sentry-cli-runner.js"; + +export const sendEventCommand = buildCommand({ + docs: { + brief: "Send an event to Sentry", + fullDescription: + "Send an event to Sentry from the command line.\n\n" + + "Wraps: sentry-cli send-event\n\n" + + "Examples:\n" + + " sentry send-event -m 'Something happened'\n" + + " sentry send-event -m 'Error' --level error\n" + + " sentry send-event -m 'Tagged event' --tag key:value", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["send-event", ...args]); + }, +}); diff --git a/src/commands/sourcemaps/index.ts b/src/commands/sourcemaps/index.ts new file mode 100644 index 00000000..44e1515b --- /dev/null +++ b/src/commands/sourcemaps/index.ts @@ -0,0 +1,22 @@ +import { buildRouteMap } from "@stricli/core"; +import { injectCommand } from "./inject.js"; +import { resolveCommand } from "./resolve.js"; +import { uploadCommand } from "./upload.js"; + +export const sourcemapsRoute = buildRouteMap({ + routes: { + upload: uploadCommand, + inject: injectCommand, + resolve: resolveCommand, + }, + docs: { + brief: "Manage sourcemaps for Sentry releases", + fullDescription: + "Manage sourcemaps for Sentry releases.\n\n" + + "Commands:\n" + + " upload Upload sourcemaps\n" + + " inject Inject debug IDs into source files\n" + + " resolve Resolve minified source locations", + hideRoute: {}, + }, +}); diff --git a/src/commands/sourcemaps/inject.ts b/src/commands/sourcemaps/inject.ts new file mode 100644 index 00000000..6f8d5162 --- /dev/null +++ b/src/commands/sourcemaps/inject.ts @@ -0,0 +1,39 @@ +/** + * sentry sourcemaps inject + * + * Inject debug IDs into source files. + * Wraps: sentry-cli sourcemaps inject + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const injectCommand = buildCommand({ + docs: { + brief: "Inject debug IDs into source files", + fullDescription: + "Inject debug IDs into source files and sourcemaps.\n\n" + + "Wraps: sentry-cli sourcemaps inject\n\n" + + "Examples:\n" + + " sentry sourcemaps inject ./dist", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["sourcemaps", "inject", ...args]); + }, +}); diff --git a/src/commands/sourcemaps/resolve.ts b/src/commands/sourcemaps/resolve.ts new file mode 100644 index 00000000..dd5c497d --- /dev/null +++ b/src/commands/sourcemaps/resolve.ts @@ -0,0 +1,39 @@ +/** + * sentry sourcemaps resolve + * + * Resolve minified source locations using sourcemaps. + * Wraps: sentry-cli sourcemaps resolve + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const resolveCommand = buildCommand({ + docs: { + brief: "Resolve minified source locations", + fullDescription: + "Resolve minified source locations using sourcemaps.\n\n" + + "Wraps: sentry-cli sourcemaps resolve\n\n" + + "Examples:\n" + + " sentry sourcemaps resolve", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["sourcemaps", "resolve", ...args]); + }, +}); diff --git a/src/commands/sourcemaps/upload.ts b/src/commands/sourcemaps/upload.ts new file mode 100644 index 00000000..3dda4f59 --- /dev/null +++ b/src/commands/sourcemaps/upload.ts @@ -0,0 +1,40 @@ +/** + * sentry sourcemaps upload + * + * Upload sourcemaps to Sentry. + * Wraps: sentry-cli sourcemaps upload + */ + +import { buildCommand } from "@stricli/core"; +import type { SentryContext } from "../../context.js"; +import { runSentryCli } from "../../lib/sentry-cli-runner.js"; + +export const uploadCommand = buildCommand({ + docs: { + brief: "Upload sourcemaps", + fullDescription: + "Upload sourcemaps to Sentry for a release.\n\n" + + "Wraps: sentry-cli sourcemaps upload\n\n" + + "Examples:\n" + + " sentry sourcemaps upload ./dist\n" + + " sentry sourcemaps upload --release 1.0.0 ./dist", + }, + parameters: { + flags: {}, + positional: { + kind: "array", + parameter: { + brief: "Arguments to pass to sentry-cli", + parse: String, + placeholder: "args", + }, + }, + }, + async func( + this: SentryContext, + _flags: Record, + ...args: string[] + ): Promise { + await runSentryCli(["sourcemaps", "upload", ...args]); + }, +}); diff --git a/src/lib/sentry-cli-runner.ts b/src/lib/sentry-cli-runner.ts new file mode 100644 index 00000000..88cc51e3 --- /dev/null +++ b/src/lib/sentry-cli-runner.ts @@ -0,0 +1,50 @@ +/** + * Sentry CLI Runner + * + * Wraps the original sentry-cli (Rust-based) for commands not yet + * natively implemented. Uses npx to avoid requiring a separate install. + * This abstraction allows future migration to native implementations + * without changing the public interface. + */ + +import { spawn } from "node:child_process"; + +/** + * Run sentry-cli via npx with the given arguments. + * + * Uses stdio: "inherit" for interactive terminal passthrough, + * matching the wizard.ts pattern. + * + * @param args - Arguments to pass to sentry-cli (e.g., ["releases", "new", "1.0.0"]) + * @throws Error if npx is not found or sentry-cli exits with non-zero code + */ +export function runSentryCli(args: string[]): Promise { + return new Promise((resolve, reject) => { + const npx = Bun.which("npx"); + if (!npx) { + reject( + new Error( + "npx not found. Please install Node.js/npm to use this command." + ) + ); + return; + } + + const proc = spawn(npx, ["@sentry/cli@latest", ...args], { + stdio: "inherit", + env: process.env, + }); + + proc.on("close", (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`sentry-cli exited with code ${code}`)); + } + }); + + proc.on("error", (err) => { + reject(new Error(`Failed to run sentry-cli: ${err.message}`)); + }); + }); +} From 5a547d06f0f79e1e5ec7930358697991859a9759 Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Fri, 6 Feb 2026 16:48:04 +0530 Subject: [PATCH 5/9] fix: sentry-cli install --- src/lib/sentry-cli-runner.ts | 102 ++++++++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 19 deletions(-) diff --git a/src/lib/sentry-cli-runner.ts b/src/lib/sentry-cli-runner.ts index 88cc51e3..c87abfaf 100644 --- a/src/lib/sentry-cli-runner.ts +++ b/src/lib/sentry-cli-runner.ts @@ -2,35 +2,99 @@ * Sentry CLI Runner * * Wraps the original sentry-cli (Rust-based) for commands not yet - * natively implemented. Uses npx to avoid requiring a separate install. - * This abstraction allows future migration to native implementations - * without changing the public interface. + * natively implemented. Detects existing installations before falling + * back to npx. This abstraction allows future migration to native + * implementations without changing the public interface. */ import { spawn } from "node:child_process"; +import { join } from "node:path"; +import { ConfigError } from "./errors.js"; + +/** Resolved binary path and any prefix arguments (e.g., npx package name) */ +type ResolvedBinary = { + command: string; + prefixArgs: string[]; +}; + +/** + * Resolve the sentry-cli binary to use. + * + * Priority: + * 1. Global sentry-cli binary (brew, curl install, scoop, etc.) + * 2. Local node_modules/.bin/sentry-cli (project dependency) + * 3. npx @sentry/cli@latest (auto-download fallback) + * 4. null (not available) + */ +async function resolveSentryCli(): Promise { + // 1. Globally installed sentry-cli (fastest path, no npx overhead) + const globalBin = Bun.which("sentry-cli"); + if (globalBin) { + return { command: globalBin, prefixArgs: [] }; + } + + // 2. Local node_modules install (@sentry/cli as project dependency) + const localBin = join(process.cwd(), "node_modules", ".bin", "sentry-cli"); + if (await Bun.file(localBin).exists()) { + return { command: localBin, prefixArgs: [] }; + } + + // 3. Fall back to npx (auto-downloads on first use) + const npx = Bun.which("npx"); + if (npx) { + return { command: npx, prefixArgs: ["@sentry/cli@latest"] }; + } + + return null; +} /** - * Run sentry-cli via npx with the given arguments. + * Build platform-specific installation instructions. + * Shows the most relevant install methods for the user's OS. + */ +function getInstallInstructions(): string { + const { platform } = process; + const lines = [ + "sentry-cli is required but not installed.\n", + "Install it using one of these methods:\n", + ]; + + if (platform === "darwin") { + lines.push(" brew install getsentry/tools/sentry-cli"); + lines.push(" curl -sL https://sentry.io/get-cli/ | sh"); + } else if (platform === "win32") { + lines.push(" scoop install sentry-cli"); + } else { + lines.push(" curl -sL https://sentry.io/get-cli/ | sh"); + } + + lines.push(" npm install -g @sentry/cli"); + + return lines.join("\n"); +} + +/** + * Run sentry-cli with the given arguments. * - * Uses stdio: "inherit" for interactive terminal passthrough, - * matching the wizard.ts pattern. + * Resolves the binary (global > local > npx), then spawns it + * with stdio: "inherit" for interactive terminal passthrough. * * @param args - Arguments to pass to sentry-cli (e.g., ["releases", "new", "1.0.0"]) - * @throws Error if npx is not found or sentry-cli exits with non-zero code + * @throws ConfigError if sentry-cli is not installed and npx is unavailable + * @throws Error if sentry-cli exits with non-zero code */ -export function runSentryCli(args: string[]): Promise { +export async function runSentryCli(args: string[]): Promise { + const resolved = await resolveSentryCli(); + + if (!resolved) { + throw new ConfigError( + "sentry-cli is not installed", + getInstallInstructions() + ); + } + return new Promise((resolve, reject) => { - const npx = Bun.which("npx"); - if (!npx) { - reject( - new Error( - "npx not found. Please install Node.js/npm to use this command." - ) - ); - return; - } - - const proc = spawn(npx, ["@sentry/cli@latest", ...args], { + const proc = spawn(resolved.command, [...resolved.prefixArgs, ...args], { stdio: "inherit", env: process.env, }); From edc698d8bbe60238bc2d96c3d60897dab0f31ac8 Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Fri, 6 Feb 2026 17:36:01 +0530 Subject: [PATCH 6/9] chore: generate skills --- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index b5ccb04f..3a732b35 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -419,6 +419,158 @@ Update the Sentry CLI to the latest version - `--check - Check for updates without installing` - `--method - Installation method to use (curl, npm, pnpm, bun, yarn)` +### Releases + +Manage releases on Sentry + +#### `sentry releases new ` + +Create a new release + +#### `sentry releases finalize ` + +Finalize a release + +#### `sentry releases list ` + +List releases + +#### `sentry releases info ` + +Show release info + +#### `sentry releases delete ` + +Delete a release + +#### `sentry releases archive ` + +Archive a release + +#### `sentry releases restore ` + +Restore an archived release + +#### `sentry releases set-commits ` + +Associate commits with a release + +#### `sentry releases propose-version ` + +Propose a version string + +### Sourcemaps + +Manage sourcemaps for Sentry releases + +#### `sentry sourcemaps upload ` + +Upload sourcemaps + +#### `sentry sourcemaps inject ` + +Inject debug IDs into source files + +#### `sentry sourcemaps resolve ` + +Resolve minified source locations + +### Debug-files + +Locate, analyze or upload debug information files + +#### `sentry debug-files upload ` + +Upload debug information files + +#### `sentry debug-files check ` + +Check debug files for issues + +#### `sentry debug-files find ` + +Find debug information files + +#### `sentry debug-files bundle-sources ` + +Bundle source files + +#### `sentry debug-files bundle-jvm ` + +Bundle JVM debug files + +#### `sentry debug-files print-sources ` + +Print embedded source files + +### Deploys + +Manage deployments for Sentry releases + +#### `sentry deploys list ` + +List deployments + +#### `sentry deploys new ` + +Create a new deployment + +### Monitors + +Manage cron monitors on Sentry + +#### `sentry monitors list ` + +List cron monitors + +#### `sentry monitors run ` + +Run a command and report to a cron monitor + +### Repos + +Manage repositories on Sentry + +#### `sentry repos list ` + +List repositories + +### Build + +Manage builds + +#### `sentry build upload ` + +Upload build artifacts + +### React-native + +Upload build artifacts for React Native projects + +#### `sentry react-native gradle ` + +Upload React Native Android build artifacts + +#### `sentry react-native xcode ` + +Upload React Native iOS build artifacts + +### Send-event + +Send an event to Sentry + +#### `sentry send-event ` + +Send an event to Sentry + +### Send-envelope + +Send an envelope to Sentry + +#### `sentry send-envelope ` + +Send an envelope to Sentry + ## Output Formats ### JSON Output From bdb9385f3f13cacf1f02f7e965c8286d17d8393b Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Fri, 6 Feb 2026 17:38:44 +0530 Subject: [PATCH 7/9] chore: updated the skills --- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index c4ebf711..7e746687 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -432,6 +432,20 @@ Update the Sentry CLI to the latest version - `--check - Check for updates without installing` - `--method - Installation method to use (curl, npm, pnpm, bun, yarn)` +### Log + +View Sentry logs + +#### `sentry log list ` + +List logs from a project + +**Flags:** +- `-n, --limit - Number of log entries (1-1000) - (default: "100")` +- `-q, --query - Filter query (Sentry search syntax)` +- `-f, --follow - Stream logs (optionally specify poll interval in seconds)` +- `--json - Output as JSON` + ### Releases Manage releases on Sentry @@ -583,19 +597,6 @@ Send an envelope to Sentry #### `sentry send-envelope ` Send an envelope to Sentry -### Log - -View Sentry logs - -#### `sentry log list ` - -List logs from a project - -**Flags:** -- `-n, --limit - Number of log entries (1-1000) - (default: "100")` -- `-q, --query - Filter query (Sentry search syntax)` -- `-f, --follow - Stream logs (optionally specify poll interval in seconds)` -- `--json - Output as JSON` ## Output Formats From e67376869717b143344f729e286e2ce707506f69 Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Fri, 6 Feb 2026 17:41:36 +0530 Subject: [PATCH 8/9] chore: minor changes --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 321c38a1..bf97688a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,8 +16,8 @@ import { eventRoute } from "./commands/event/index.js"; import { helpCommand } from "./commands/help.js"; import { initCommand } from "./commands/init.js"; import { issueRoute } from "./commands/issue/index.js"; -import { monitorsRoute } from "./commands/monitors/index.js"; import { logRoute } from "./commands/log/index.js"; +import { monitorsRoute } from "./commands/monitors/index.js"; import { orgRoute } from "./commands/org/index.js"; import { projectRoute } from "./commands/project/index.js"; import { reactNativeRoute } from "./commands/react-native/index.js"; From 50697176a050e495dcbfeed52ac8581b82fa1c84 Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Fri, 6 Feb 2026 17:44:17 +0530 Subject: [PATCH 9/9] chore: removed the tests --- test/commands/init.test.ts | 520 ------------------------------------- test/lib/wizard.test.ts | 185 ------------- 2 files changed, 705 deletions(-) delete mode 100644 test/commands/init.test.ts delete mode 100644 test/lib/wizard.test.ts diff --git a/test/commands/init.test.ts b/test/commands/init.test.ts deleted file mode 100644 index 6888a97f..00000000 --- a/test/commands/init.test.ts +++ /dev/null @@ -1,520 +0,0 @@ -/** - * Init Command Tests - * - * Tests for the sentry init command and wizard flag mapping. - */ - -import { beforeEach, describe, expect, mock, test } from "bun:test"; -import { _buildWizardArgs, type WizardOptions } from "../../src/lib/wizard.js"; - -// Track runWizard calls for assertions -let runWizardCalls: WizardOptions[] = []; -let mockGetDefaultOrg: (() => Promise) | null = null; -let mockGetDefaultProject: (() => Promise) | null = null; -let mockGetAuthToken: (() => string | undefined) | null = null; - -beforeEach(() => { - runWizardCalls = []; - mockGetDefaultOrg = null; - mockGetDefaultProject = null; - mockGetAuthToken = null; - - // Mock runWizard to avoid spawning the wizard process - mock.module("../../src/lib/wizard.js", () => ({ - runWizard: async (options: WizardOptions) => { - runWizardCalls.push(options); - }, - _buildWizardArgs, - })); - - // Mock defaults - mock.module("../../src/lib/db/defaults.js", () => ({ - getDefaultOrganization: async () => { - if (mockGetDefaultOrg) { - return mockGetDefaultOrg(); - } - return null; - }, - getDefaultProject: async () => { - if (mockGetDefaultProject) { - return mockGetDefaultProject(); - } - return null; - }, - })); - - // Mock auth - mock.module("../../src/lib/db/auth.js", () => ({ - getAuthToken: () => { - if (mockGetAuthToken) { - return mockGetAuthToken(); - } - return; - }, - })); - - // Mock API client - return empty/mock data - mock.module("../../src/lib/api-client.js", () => ({ - getOrganization: async (orgSlug: string) => ({ - id: "123", - slug: orgSlug, - name: "Test Org", - }), - getProject: async (_orgSlug: string, projectSlug: string) => ({ - id: "456", - slug: projectSlug, - name: "Test Project", - }), - getProjectKeys: async () => [ - { dsn: { public: "https://key@o123.ingest.sentry.io/456" } }, - ], - })); - - // Mock sentry-urls - mock.module("../../src/lib/sentry-urls.js", () => ({ - getSentryBaseUrl: () => "https://sentry.io", - isSentrySaasUrl: () => true, - })); -}); - -describe("buildWizardArgs", () => { - test("returns empty array when no options provided", () => { - const args = _buildWizardArgs({}); - expect(args).toEqual([]); - }); - - test("maps integration option to -i flag", () => { - const args = _buildWizardArgs({ integration: "nextjs" }); - expect(args).toEqual(["-i", "nextjs"]); - }); - - test("maps org option to --org flag", () => { - const args = _buildWizardArgs({ org: "my-org" }); - expect(args).toEqual(["--org", "my-org"]); - }); - - test("maps project option to --project flag", () => { - const args = _buildWizardArgs({ project: "my-project" }); - expect(args).toEqual(["--project", "my-project"]); - }); - - test("maps url option to -u flag", () => { - const args = _buildWizardArgs({ url: "https://sentry.example.com" }); - expect(args).toEqual(["-u", "https://sentry.example.com"]); - }); - - test("maps debug option to --debug flag", () => { - const args = _buildWizardArgs({ debug: true }); - expect(args).toEqual(["--debug"]); - }); - - test("does not include --debug when debug is false", () => { - const args = _buildWizardArgs({ debug: false }); - expect(args).not.toContain("--debug"); - }); - - test("maps uninstall option to --uninstall flag", () => { - const args = _buildWizardArgs({ uninstall: true }); - expect(args).toEqual(["--uninstall"]); - }); - - test("maps quiet option to --quiet flag", () => { - const args = _buildWizardArgs({ quiet: true }); - expect(args).toEqual(["--quiet"]); - }); - - test("maps skipConnect option to --skip-connect flag", () => { - const args = _buildWizardArgs({ skipConnect: true }); - expect(args).toEqual(["--skip-connect"]); - }); - - test("maps saas option to --saas flag", () => { - const args = _buildWizardArgs({ saas: true }); - expect(args).toEqual(["--saas"]); - }); - - test("maps signup option to -s flag", () => { - const args = _buildWizardArgs({ signup: true }); - expect(args).toEqual(["-s"]); - }); - - test("maps disableTelemetry option to --disable-telemetry flag", () => { - const args = _buildWizardArgs({ disableTelemetry: true }); - expect(args).toEqual(["--disable-telemetry"]); - }); - - test("combines multiple options correctly", () => { - const options: WizardOptions = { - integration: "reactNative", - org: "test-org", - project: "test-project", - debug: true, - saas: true, - }; - - const args = _buildWizardArgs(options); - - expect(args).toContain("-i"); - expect(args).toContain("reactNative"); - expect(args).toContain("--org"); - expect(args).toContain("test-org"); - expect(args).toContain("--project"); - expect(args).toContain("test-project"); - expect(args).toContain("--debug"); - expect(args).toContain("--saas"); - }); - - test("preserves argument order for predictable output", () => { - const options: WizardOptions = { - integration: "nextjs", - org: "my-org", - url: "https://custom.sentry.io", - debug: true, - }; - - const args = _buildWizardArgs(options); - - // Verify the order matches the buildWizardArgs implementation - expect(args).toEqual([ - "-i", - "nextjs", - "--org", - "my-org", - "-u", - "https://custom.sentry.io", - "--debug", - ]); - }); - - test("handles all boolean flags together", () => { - const options: WizardOptions = { - debug: true, - uninstall: true, - quiet: true, - skipConnect: true, - saas: true, - signup: true, - disableTelemetry: true, - }; - - const args = _buildWizardArgs(options); - - expect(args).toContain("--debug"); - expect(args).toContain("--uninstall"); - expect(args).toContain("--quiet"); - expect(args).toContain("--skip-connect"); - expect(args).toContain("--saas"); - expect(args).toContain("-s"); - expect(args).toContain("--disable-telemetry"); - expect(args).toHaveLength(7); - }); - - test("maps preSelectedProject to wizard flags", () => { - const options: WizardOptions = { - preSelectedProject: { - authToken: "test-token", - selfHosted: false, - dsn: "https://key@o123.ingest.sentry.io/456", - id: "456", - projectSlug: "my-project", - projectName: "My Project", - orgId: "123", - orgName: "My Org", - orgSlug: "my-org", - }, - }; - - const args = _buildWizardArgs(options); - - expect(args).toContain("--preSelectedProject.authToken"); - expect(args).toContain("test-token"); - expect(args).toContain("--preSelectedProject.selfHosted"); - expect(args).toContain("false"); - expect(args).toContain("--preSelectedProject.dsn"); - expect(args).toContain("https://key@o123.ingest.sentry.io/456"); - expect(args).toContain("--preSelectedProject.id"); - expect(args).toContain("456"); - expect(args).toContain("--preSelectedProject.projectSlug"); - expect(args).toContain("my-project"); - expect(args).toContain("--preSelectedProject.projectName"); - expect(args).toContain("My Project"); - expect(args).toContain("--preSelectedProject.orgId"); - expect(args).toContain("123"); - expect(args).toContain("--preSelectedProject.orgName"); - expect(args).toContain("My Org"); - expect(args).toContain("--preSelectedProject.orgSlug"); - expect(args).toContain("my-org"); - }); -}); - -describe("initCommand.func", () => { - test("maps flags to WizardOptions and calls runWizard", async () => { - const { initCommand } = await import("../../src/commands/init.js"); - const func = await initCommand.loader(); - - const stdoutWrite = mock(() => true); - const mockContext = { - stdout: { write: stdoutWrite }, - stderr: { write: mock(() => true) }, - }; - - const flags = { - integration: "nextjs", - org: "test-org", - project: "test-project", - url: "https://sentry.example.com", - debug: true, - uninstall: false, - quiet: false, - "skip-connect": true, - saas: true, - signup: false, - "disable-telemetry": true, - "no-auth": true, // Skip auth sharing for basic flag mapping test - }; - - await func.call(mockContext, flags); - - expect(runWizardCalls).toHaveLength(1); - expect(runWizardCalls[0]).toMatchObject({ - integration: "nextjs", - org: "test-org", - project: "test-project", - url: "https://sentry.example.com", - debug: true, - uninstall: false, - quiet: false, - skipConnect: true, - saas: true, - signup: false, - disableTelemetry: true, - }); - }); - - test("auto-populates org from defaults when --org not provided", async () => { - mockGetDefaultOrg = async () => "my-default-org"; - - const { initCommand } = await import("../../src/commands/init.js"); - const func = await initCommand.loader(); - - const stdoutWrite = mock(() => true); - const mockContext = { - stdout: { write: stdoutWrite }, - stderr: { write: mock(() => true) }, - }; - - const flags = { - debug: false, - uninstall: false, - quiet: false, - "skip-connect": false, - saas: false, - signup: false, - "disable-telemetry": false, - "no-auth": true, // Skip auth sharing - }; - - await func.call(mockContext, flags); - - expect(runWizardCalls).toHaveLength(1); - expect(runWizardCalls[0].org).toBe("my-default-org"); - }); - - test("does not override org when explicitly provided", async () => { - mockGetDefaultOrg = async () => "default-org"; - - const { initCommand } = await import("../../src/commands/init.js"); - const func = await initCommand.loader(); - - const stdoutWrite = mock(() => true); - const mockContext = { - stdout: { write: stdoutWrite }, - stderr: { write: mock(() => true) }, - }; - - const flags = { - org: "explicit-org", - debug: false, - uninstall: false, - quiet: false, - "skip-connect": false, - saas: false, - signup: false, - "disable-telemetry": false, - "no-auth": true, // Skip auth sharing - }; - - await func.call(mockContext, flags); - - expect(runWizardCalls).toHaveLength(1); - expect(runWizardCalls[0].org).toBe("explicit-org"); - }); - - test("org is undefined when no default org", async () => { - mockGetDefaultOrg = async () => null; - - const { initCommand } = await import("../../src/commands/init.js"); - const func = await initCommand.loader(); - - const stdoutWrite = mock(() => true); - const mockContext = { - stdout: { write: stdoutWrite }, - stderr: { write: mock(() => true) }, - }; - - const flags = { - debug: false, - uninstall: false, - quiet: false, - "skip-connect": false, - saas: false, - signup: false, - "disable-telemetry": false, - "no-auth": true, // Skip auth sharing - }; - - await func.call(mockContext, flags); - - expect(runWizardCalls).toHaveLength(1); - expect(runWizardCalls[0].org).toBeUndefined(); - }); - - test("writes 'Starting Sentry Wizard...' to stdout", async () => { - const { initCommand } = await import("../../src/commands/init.js"); - const func = await initCommand.loader(); - - const stdoutWrite = mock(() => true); - const mockContext = { - stdout: { write: stdoutWrite }, - stderr: { write: mock(() => true) }, - }; - - const flags = { - debug: false, - uninstall: false, - quiet: false, - "skip-connect": false, - saas: false, - signup: false, - "disable-telemetry": false, - "no-auth": true, // Skip auth sharing - }; - - await func.call(mockContext, flags); - - expect(stdoutWrite).toHaveBeenCalledWith("Starting Sentry Wizard...\n\n"); - }); - - test("handles all flags correctly including integration", async () => { - const { initCommand } = await import("../../src/commands/init.js"); - const func = await initCommand.loader(); - - const stdoutWrite = mock(() => true); - const mockContext = { - stdout: { write: stdoutWrite }, - stderr: { write: mock(() => true) }, - }; - - const flags = { - integration: "reactNative", - debug: true, - uninstall: true, - quiet: true, - "skip-connect": true, - saas: true, - signup: true, - "disable-telemetry": true, - "no-auth": true, // Skip auth sharing - }; - - await func.call(mockContext, flags); - - expect(runWizardCalls).toHaveLength(1); - expect(runWizardCalls[0]).toMatchObject({ - integration: "reactNative", - debug: true, - uninstall: true, - quiet: true, - skipConnect: true, - saas: true, - signup: true, - disableTelemetry: true, - }); - }); - - test("shares auth with wizard when authenticated with org and project", async () => { - mockGetDefaultOrg = async () => "my-org"; - mockGetDefaultProject = async () => "my-project"; - mockGetAuthToken = () => "test-token-123"; - - const { initCommand } = await import("../../src/commands/init.js"); - const func = await initCommand.loader(); - - const stdoutWrite = mock(() => true); - const mockContext = { - stdout: { write: stdoutWrite }, - stderr: { write: mock(() => true) }, - }; - - const flags = { - debug: false, - uninstall: false, - quiet: false, - "skip-connect": false, - saas: false, - signup: false, - "disable-telemetry": false, - "no-auth": false, // Enable auth sharing - }; - - await func.call(mockContext, flags); - - expect(runWizardCalls).toHaveLength(1); - expect(runWizardCalls[0].preSelectedProject).toBeDefined(); - expect(runWizardCalls[0].preSelectedProject?.authToken).toBe( - "test-token-123" - ); - expect(runWizardCalls[0].preSelectedProject?.orgSlug).toBe("my-org"); - expect(runWizardCalls[0].preSelectedProject?.projectSlug).toBe( - "my-project" - ); - expect(runWizardCalls[0].preSelectedProject?.selfHosted).toBe(false); - expect(stdoutWrite).toHaveBeenCalledWith( - "Using existing Sentry auth for my-org/my-project\n" - ); - }); - - test("skips auth sharing when --no-auth flag is set", async () => { - mockGetDefaultOrg = async () => "my-org"; - mockGetDefaultProject = async () => "my-project"; - mockGetAuthToken = () => "test-token-123"; - - const { initCommand } = await import("../../src/commands/init.js"); - const func = await initCommand.loader(); - - const stdoutWrite = mock(() => true); - const mockContext = { - stdout: { write: stdoutWrite }, - stderr: { write: mock(() => true) }, - }; - - const flags = { - debug: false, - uninstall: false, - quiet: false, - "skip-connect": false, - saas: false, - signup: false, - "disable-telemetry": false, - "no-auth": true, // Disable auth sharing - }; - - await func.call(mockContext, flags); - - expect(runWizardCalls).toHaveLength(1); - expect(runWizardCalls[0].preSelectedProject).toBeUndefined(); - expect(stdoutWrite).not.toHaveBeenCalledWith( - expect.stringContaining("Using existing Sentry auth") - ); - }); -}); diff --git a/test/lib/wizard.test.ts b/test/lib/wizard.test.ts deleted file mode 100644 index a4613991..00000000 --- a/test/lib/wizard.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Wizard Module Tests - * - * Tests for the runWizard function that spawns @sentry/wizard. - */ - -import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; -import { EventEmitter } from "node:events"; - -// Store original Bun.which for restoration -const originalBunWhich = Bun.which; - -// Mock spawn function and captured calls -let mockSpawn: ReturnType; -let lastSpawnedProc: EventEmitter | null = null; -let spawnCalls: Array<{ command: string; args: string[]; options: unknown }> = - []; - -beforeEach(() => { - spawnCalls = []; - lastSpawnedProc = null; - - // Create mock spawn that returns an EventEmitter - mockSpawn = mock((command: string, args: string[], options: unknown) => { - spawnCalls.push({ command, args, options }); - const proc = new EventEmitter(); - lastSpawnedProc = proc; - return proc; - }); - - // Mock node:child_process - mock.module("node:child_process", () => ({ - spawn: mockSpawn, - })); -}); - -afterEach(() => { - // Restore Bun.which - (Bun as { which: typeof Bun.which }).which = originalBunWhich; -}); - -/** - * Helper to mock Bun.which - */ -function mockBunWhich(returnValue: string | null) { - (Bun as { which: typeof Bun.which }).which = () => returnValue; -} - -describe("runWizard", () => { - test("rejects when npx is not found", async () => { - mockBunWhich(null); - - // Import after mocking - const { runWizard } = await import("../../src/lib/wizard.js"); - - await expect(runWizard({})).rejects.toThrow( - "npx not found. Please install Node.js/npm to use the init command." - ); - }); - - test("resolves when wizard exits with code 0", async () => { - mockBunWhich("/usr/local/bin/npx"); - - const { runWizard } = await import("../../src/lib/wizard.js"); - - const promise = runWizard({}); - - // Give the promise time to set up the event listener - await Bun.sleep(10); - - // Emit close with success code - lastSpawnedProc?.emit("close", 0); - - await expect(promise).resolves.toBeUndefined(); - }); - - test("rejects when wizard exits with non-zero code", async () => { - mockBunWhich("/usr/local/bin/npx"); - - const { runWizard } = await import("../../src/lib/wizard.js"); - - const promise = runWizard({}); - - await Bun.sleep(10); - - // Emit close with error code - lastSpawnedProc?.emit("close", 1); - - await expect(promise).rejects.toThrow("Wizard exited with code 1"); - }); - - test("rejects when spawn emits error", async () => { - mockBunWhich("/usr/local/bin/npx"); - - const { runWizard } = await import("../../src/lib/wizard.js"); - - const promise = runWizard({}); - - await Bun.sleep(10); - - // Emit error event - lastSpawnedProc?.emit("error", new Error("ENOENT: command not found")); - - await expect(promise).rejects.toThrow( - "Failed to start wizard: ENOENT: command not found" - ); - }); - - test("passes correct arguments to spawn with no options", async () => { - mockBunWhich("/usr/local/bin/npx"); - - const { runWizard } = await import("../../src/lib/wizard.js"); - - const promise = runWizard({}); - - await Bun.sleep(10); - lastSpawnedProc?.emit("close", 0); - await promise; - - expect(spawnCalls).toHaveLength(1); - expect(spawnCalls[0].command).toBe("/usr/local/bin/npx"); - expect(spawnCalls[0].args).toEqual(["@sentry/wizard@latest"]); - expect(spawnCalls[0].options).toMatchObject({ - stdio: "inherit", - }); - }); - - test("passes correct arguments to spawn with all options", async () => { - mockBunWhich("/usr/bin/npx"); - - const { runWizard } = await import("../../src/lib/wizard.js"); - - const promise = runWizard({ - integration: "nextjs", - org: "my-org", - project: "my-project", - url: "https://sentry.example.com", - debug: true, - uninstall: true, - quiet: true, - skipConnect: true, - saas: true, - signup: true, - disableTelemetry: true, - }); - - await Bun.sleep(10); - lastSpawnedProc?.emit("close", 0); - await promise; - - expect(spawnCalls).toHaveLength(1); - expect(spawnCalls[0].command).toBe("/usr/bin/npx"); - expect(spawnCalls[0].args).toEqual([ - "@sentry/wizard@latest", - "-i", - "nextjs", - "--org", - "my-org", - "--project", - "my-project", - "-u", - "https://sentry.example.com", - "--debug", - "--uninstall", - "--quiet", - "--skip-connect", - "--saas", - "-s", - "--disable-telemetry", - ]); - }); - - test("rejects with specific exit code in error message", async () => { - mockBunWhich("/usr/local/bin/npx"); - - const { runWizard } = await import("../../src/lib/wizard.js"); - - const promise = runWizard({ integration: "flutter" }); - - await Bun.sleep(10); - lastSpawnedProc?.emit("close", 127); - - await expect(promise).rejects.toThrow("Wizard exited with code 127"); - }); -});