-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat(cli): add sentry cli defaults command for persistent settings
#721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,343 @@ | ||
| /** | ||
| * sentry cli defaults | ||
| * | ||
| * View and manage persistent CLI default settings. | ||
| * | ||
| * Supports four defaults: | ||
| * - `org` / `organization` — default organization slug | ||
| * - `project` — default project slug | ||
| * - `telemetry` — telemetry preference (on/off) | ||
| * - `url` — Sentry instance URL (for self-hosted) | ||
| */ | ||
|
|
||
| import type { SentryContext } from "../../context.js"; | ||
| import { buildCommand } from "../../lib/command.js"; | ||
| import { normalizeUrl } from "../../lib/constants.js"; | ||
| import { | ||
| clearAllDefaults, | ||
| type DefaultsState, | ||
| getAllDefaults, | ||
| getDefaultOrganization, | ||
| getDefaultProject, | ||
| getDefaultUrl, | ||
| getTelemetryPreference, | ||
| setDefaultOrganization, | ||
| setDefaultProject, | ||
| setDefaultUrl, | ||
| setTelemetryPreference, | ||
| } from "../../lib/db/defaults.js"; | ||
| import { ValidationError } from "../../lib/errors.js"; | ||
| import { formatDefaultsResult } from "../../lib/formatters/human.js"; | ||
| import { CommandOutput } from "../../lib/formatters/output.js"; | ||
| import { logger } from "../../lib/logger.js"; | ||
| import { | ||
| FORCE_FLAG, | ||
| guardNonInteractive, | ||
| isConfirmationBypassed, | ||
| YES_FLAG, | ||
| } from "../../lib/mutate-command.js"; | ||
| import { parseBoolValue } from "../../lib/parse-bool.js"; | ||
| import { computeTelemetryEffective } from "../../lib/telemetry.js"; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Defaults registry — maps canonical keys to get/set/clear handlers | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** Canonical key names matching DefaultsState fields */ | ||
| type DefaultKey = "organization" | "project" | "telemetry" | "url"; | ||
|
|
||
| /** Handler for reading, writing, and clearing a single default */ | ||
| type DefaultHandler = { | ||
| /** Get current value for display / change tracking */ | ||
| get: () => string | boolean | null; | ||
| /** Validate and store a new value */ | ||
| set: (value: string) => void; | ||
| /** Clear the stored value */ | ||
| clear: () => void; | ||
| }; | ||
|
|
||
| /** Validate that a slug value is non-empty after trimming whitespace. */ | ||
| function validateSlug(value: string, label: string): string { | ||
| const trimmed = value.trim(); | ||
| if (!trimmed) { | ||
| throw new ValidationError(`${label} cannot be empty.`, label.toLowerCase()); | ||
| } | ||
| return trimmed; | ||
| } | ||
|
|
||
| /** Registry of all supported defaults with their handlers */ | ||
| const DEFAULTS_REGISTRY: Record<DefaultKey, DefaultHandler> = { | ||
| organization: { | ||
| get: getDefaultOrganization, | ||
| set: (value) => setDefaultOrganization(validateSlug(value, "Organization")), | ||
| clear: () => setDefaultOrganization(null), | ||
| }, | ||
| project: { | ||
| get: getDefaultProject, | ||
| set: (value) => setDefaultProject(validateSlug(value, "Project")), | ||
| clear: () => setDefaultProject(null), | ||
| }, | ||
| telemetry: { | ||
| get: () => { | ||
| const pref = getTelemetryPreference(); | ||
| if (pref === true) { | ||
| return "on"; | ||
| } | ||
| if (pref === false) { | ||
| return "off"; | ||
| } | ||
| return null; | ||
| }, | ||
| set: (value) => { | ||
| const parsed = parseBoolValue(value); | ||
| if (parsed === null) { | ||
| throw new ValidationError( | ||
| `Invalid telemetry value: '${value}'. Use on/off, yes/no, true/false, or 1/0.`, | ||
| "telemetry" | ||
| ); | ||
| } | ||
| setTelemetryPreference(parsed); | ||
| }, | ||
| clear: () => setTelemetryPreference(null), | ||
| }, | ||
| url: { | ||
| get: getDefaultUrl, | ||
| set: (value) => { | ||
| const normalized = normalizeUrl(value); | ||
| if (!normalized) { | ||
| throw new ValidationError("URL cannot be empty.", "url"); | ||
| } | ||
| try { | ||
| new URL(normalized); | ||
| } catch { | ||
| throw new ValidationError( | ||
| `Invalid URL: '${value}'. Provide a valid URL (e.g., https://sentry.example.com).`, | ||
| "url" | ||
| ); | ||
| } | ||
| setDefaultUrl(normalized); | ||
| }, | ||
| clear: () => setDefaultUrl(null), | ||
| }, | ||
| }; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Key aliases — maps shorthand names to canonical DefaultKey | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** Shorthand aliases for canonical keys (e.g., "org" → "organization") */ | ||
| const KEY_ALIASES: Partial<Record<string, DefaultKey>> = { | ||
| org: "organization", | ||
| }; | ||
|
|
||
| /** Resolve a user-provided key string to a canonical key, or null if unknown */ | ||
| function normalizeKey(key: string): DefaultKey | null { | ||
| const lower = key.toLowerCase(); | ||
| return ( | ||
| KEY_ALIASES[lower] ?? | ||
| (lower in DEFAULTS_REGISTRY ? (lower as DefaultKey) : null) | ||
| ); | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Result type + telemetry effective state | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** Result data for the defaults command */ | ||
| export type DefaultsResult = { | ||
| /** The operation performed */ | ||
| action: "show" | "set" | "clear" | "clear-all"; | ||
| /** Current state of all defaults after the operation */ | ||
| defaults: DefaultsState; | ||
| /** Effective telemetry state considering env var overrides (display-only) */ | ||
| telemetryEffective?: { | ||
| enabled: boolean; | ||
| source: string; | ||
| }; | ||
| /** What was changed (for set/clear actions) */ | ||
| changed?: { | ||
| key: string; | ||
| previousValue: string | boolean | null; | ||
| newValue: string | boolean | null; | ||
| }; | ||
| }; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Command | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| const log = logger.withTag("defaults"); | ||
|
|
||
| export const defaultsCommand = buildCommand({ | ||
| auth: false, | ||
| docs: { | ||
| brief: "View and manage default settings", | ||
| fullDescription: | ||
| "View and manage persistent CLI default settings.\n\n" + | ||
| "With no arguments, shows all current defaults. Pass a key and value\n" + | ||
| "to set a default, or use `--clear` to remove defaults.\n\n" + | ||
| "## Examples\n\n" + | ||
| "```\n" + | ||
| "sentry cli defaults # Show all defaults\n" + | ||
| "sentry cli defaults org my-org # Set default organization\n" + | ||
| "sentry cli defaults project my-proj # Set default project\n" + | ||
| "sentry cli defaults telemetry off # Disable telemetry\n" + | ||
| "sentry cli defaults url https://... # Set Sentry URL (self-hosted)\n" + | ||
| "sentry cli defaults org --clear # Clear a specific default\n" + | ||
| "sentry cli defaults --clear --yes # Clear all defaults\n" + | ||
| "```\n\n" + | ||
| "## Recognized keys\n\n" + | ||
| "| Key | Description |\n" + | ||
| "|-----|------------|\n" + | ||
| "| `org` | Default organization slug |\n" + | ||
| "| `project` | Default project slug |\n" + | ||
| "| `telemetry` | Telemetry preference (on/off, yes/no, true/false, 1/0) |\n" + | ||
| "| `url` | Sentry instance URL (for self-hosted installations) |", | ||
| }, | ||
| output: { | ||
| human: formatDefaultsResult, | ||
| jsonExclude: ["telemetryEffective"], | ||
| }, | ||
| parameters: { | ||
| positional: { | ||
| kind: "array", | ||
| parameter: { | ||
| brief: "Setting key and optional value", | ||
| parse: String, | ||
| placeholder: "key value", | ||
| }, | ||
| }, | ||
| flags: { | ||
| clear: { | ||
| kind: "boolean", | ||
| brief: | ||
| "Clear the specified default, or all defaults if no key is given", | ||
| default: false, | ||
| }, | ||
| yes: YES_FLAG, | ||
| force: FORCE_FLAG, | ||
| }, | ||
| aliases: { y: "yes", f: "force" }, | ||
| }, | ||
| // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: sequential command dispatch | ||
| async *func( | ||
| this: SentryContext, | ||
| flags: { | ||
| readonly clear: boolean; | ||
| readonly yes: boolean; | ||
| readonly force: boolean; | ||
| }, | ||
| ...args: string[] | ||
| ) { | ||
| const [keyArg, valueArg, ...rest] = args; | ||
|
|
||
| // Too many arguments | ||
| if (rest.length > 0) { | ||
| throw new ValidationError( | ||
| "Too many arguments. Usage: sentry cli defaults [<key> [<value>]]", | ||
| "args" | ||
| ); | ||
| } | ||
|
|
||
| // No key specified — show all or clear all | ||
| if (!keyArg) { | ||
| if (flags.clear) { | ||
| // Clear all defaults (with confirmation) | ||
| guardNonInteractive(flags); | ||
| if (!isConfirmationBypassed(flags)) { | ||
| const confirmed = await log.prompt( | ||
| "This will clear all defaults (organization, project, telemetry, URL). Continue?", | ||
| { type: "confirm" } | ||
| ); | ||
| if (confirmed !== true) { | ||
| return { hint: "Cancelled." }; | ||
| } | ||
| } | ||
| clearAllDefaults(); | ||
| yield new CommandOutput({ | ||
| action: "clear-all" as const, | ||
| defaults: getAllDefaults(), | ||
| }); | ||
| return { hint: "All defaults have been cleared." }; | ||
| } | ||
|
|
||
| // Show all defaults | ||
| yield new CommandOutput({ | ||
| action: "show" as const, | ||
| defaults: getAllDefaults(), | ||
| telemetryEffective: computeTelemetryEffective(), | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| // Validate key | ||
| const canonical = normalizeKey(keyArg); | ||
| if (!canonical) { | ||
| const validKeys = [ | ||
| ...Object.keys(DEFAULTS_REGISTRY), | ||
| ...Object.keys(KEY_ALIASES), | ||
| ]; | ||
| throw new ValidationError( | ||
| `Unknown default '${keyArg}'. Valid keys: ${validKeys.join(", ")}`, | ||
| "key" | ||
| ); | ||
| } | ||
|
|
||
| const handler = DEFAULTS_REGISTRY[canonical]; | ||
|
|
||
| // Key + --clear → clear specific default | ||
| if (flags.clear) { | ||
| if (valueArg !== undefined) { | ||
| throw new ValidationError( | ||
| `Cannot use --clear with a value. Use either 'sentry cli defaults ${keyArg} --clear' or 'sentry cli defaults ${keyArg} <value>'.`, | ||
| "args" | ||
| ); | ||
| } | ||
| const previous = handler.get(); | ||
| handler.clear(); | ||
| yield new CommandOutput({ | ||
| action: "clear" as const, | ||
| defaults: getAllDefaults(), | ||
| changed: { key: canonical, previousValue: previous, newValue: null }, | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| // Key only, no value → show specific default | ||
| if (valueArg === undefined) { | ||
| yield new CommandOutput({ | ||
| action: "show" as const, | ||
| defaults: getAllDefaults(), | ||
| telemetryEffective: | ||
| canonical === "telemetry" ? computeTelemetryEffective() : undefined, | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| // Key + value → set default | ||
| const previous = handler.get(); | ||
| handler.set(valueArg); | ||
| const newValue = handler.get(); | ||
|
|
||
| yield new CommandOutput({ | ||
| action: "set" as const, | ||
| defaults: getAllDefaults(), | ||
| changed: { key: canonical, previousValue: previous, newValue }, | ||
| }); | ||
|
|
||
| // Show telemetry override warning when setting telemetry preference | ||
| if (canonical === "telemetry") { | ||
| const effective = computeTelemetryEffective(); | ||
| if ( | ||
| effective?.source.startsWith("env:") && | ||
| effective.enabled !== (newValue === "on") | ||
| ) { | ||
| log.warn( | ||
| `Note: ${effective.source.slice("env:".length)} environment variable overrides this preference.` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return; | ||
| }, | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.