diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 4b44388a..7e746687 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -387,6 +387,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 @@ -424,6 +446,158 @@ List logs from a project - `-f, --follow - Stream logs (optionally specify poll interval in seconds)` - `--json - Output as JSON` +### 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 diff --git a/src/app.ts b/src/app.ts index e885db02..bf97688a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,13 +8,24 @@ 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 { 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"; +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 { AuthError, CliError, getExitCode } from "./lib/errors.js"; import { error as errorColor } from "./lib/formatters/colors.js"; @@ -23,6 +34,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, @@ -31,6 +43,16 @@ export const routes = buildRouteMap({ event: eventRoute, log: logRoute, 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/init.ts b/src/commands/init.ts new file mode 100644 index 00000000..c22b0ec8 --- /dev/null +++ b/src/commands/init.ts @@ -0,0 +1,223 @@ +/** + * 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 { + 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; + 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; + readonly "no-auth": 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, + }, + "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" }, + }, + 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 + if (!options.org) { + 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" + ); + } + } + + stdout.write("Starting Sentry Wizard...\n\n"); + + await runWizard(options); + }, +}); 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..c87abfaf --- /dev/null +++ b/src/lib/sentry-cli-runner.ts @@ -0,0 +1,114 @@ +/** + * Sentry CLI Runner + * + * Wraps the original sentry-cli (Rust-based) for commands not yet + * 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; +} + +/** + * 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. + * + * 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 ConfigError if sentry-cli is not installed and npx is unavailable + * @throws Error if sentry-cli exits with non-zero code + */ +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 proc = spawn(resolved.command, [...resolved.prefixArgs, ...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}`)); + }); + }); +} diff --git a/src/lib/wizard.ts b/src/lib/wizard.ts new file mode 100644 index 00000000..ed8f1c2e --- /dev/null +++ b/src/lib/wizard.ts @@ -0,0 +1,181 @@ +/** + * 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"; + +/** + * 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. + */ +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; + /** Pre-selected project data to skip wizard login flow */ + preSelectedProject?: PreSelectedProject; +}; + +/** + * 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"); + } + + // 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; +} + +/** + * 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 };