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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,9 @@ catch (error: any) { }

Commands use a small set of exit codes aligned with oclif defaults and Unix convention.

- **0 - Success**: Command completed normally. Implicit when `run()` returns without throwing. Only use `this.exit(0)` when you need to short-circuit early on a successful path.
- **1 - Runtime error**: Something went wrong during execution that is not the user's fault. API failures, network errors, missing project config, file system errors, unexpected state. Use `this.output.error(message, {exit: 1})`.
- **2 - Usage error**: The user provided invalid input to the CLI itself. Bad arguments, unknown flags, invalid flag values, failing input validation. This is oclif's default for `this.output.error()` and all parse errors, so omitting the `exit` option also gives you 2. Use `this.output.error(message, {exit: 2})` or `this.output.error(message)`.
- **0 - Success**: Command completed normally. Implicit when `run()` returns without throwing. Only use `this.exit(exitCodes.SUCCESS)` when you need to short-circuit early on a successful path.

@shapirodaniel shapirodaniel Jul 22, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huge win for future agentic work! thanks for including these docs ❤️

- **1 - Runtime error**: Something went wrong during execution that is not the user's fault. API failures, network errors, missing project config, file system errors, unexpected state. Use `this.output.error(message, {exit: exitCodes.RUNTIME_ERROR})`.
- **2 - Usage error**: The user provided invalid input to the CLI itself. Bad arguments, unknown flags, invalid flag values, failing input validation. This is oclif's default for `this.output.error()` and all parse errors, so omitting the `exit` option also gives you 2. Use `this.output.error(message, {exit: exitCodes.USAGE_ERROR})` or `this.output.error(message)`.
- **3 - User abort**: The user declined a confirmation prompt or otherwise chose not to proceed. The command didn't fail, but it also didn't complete its intended action. Use `this.exit(exitCodes.USER_ABORT)`. Import `exitCodes` from `@sanity/cli-core`.
- **130 - User abort (signal)**: The user cancelled via Ctrl+C or dismissed a prompt without answering. Handled automatically by `SanityCommand.catch()` - commands should not set this manually.

Expand All @@ -197,7 +197,7 @@ Commands use a small set of exit codes aligned with oclif defaults and Unix conv

### In Practice

- For `this.output.error()`: pass `{exit: 1}` for runtime errors, `{exit: 2}` (or omit) for usage errors.
- For `this.output.error()`: pass `{exit: exitCodes.RUNTIME_ERROR}` for runtime errors, `{exit: exitCodes.USAGE_ERROR}` (or omit) for usage errors.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:chefs-kiss:

- For user-declined prompts: `this.exit(exitCodes.USER_ABORT)` after logging a message like "Deploy cancelled."
- For custom error classes extending `CLIError`: set `exit` in the constructor options.
- For `this.exit()`: only use for early termination (exit 0 for success, exit 1 for programmatic failure like `doctor` checks failing).
Expand Down Expand Up @@ -379,7 +379,7 @@ Sanity CLI commands all extend from the `SanityCommand` class, which provides se
### Basic Command Structure

```typescript
import {getProjectCliClient, SanityCommand, subdebug} from '@sanity/cli-core'
import {exitCodes, getProjectCliClient, SanityCommand, subdebug} from '@sanity/cli-core'
import {Args, Flags, type FlagInput} from '@oclif/core'

const debug = subdebug('namespace:command')
Expand Down Expand Up @@ -430,7 +430,7 @@ export class MyCommand extends SanityCommand<typeof MyCommand> {
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
debug('Operation failed', error)
this.output.error(`Failed: ${message}`, {exit: 1})
this.output.error(`Failed: ${message}`, {exit: exitCodes.RUNTIME_ERROR})
}
}
}
Expand All @@ -439,7 +439,7 @@ export class MyCommand extends SanityCommand<typeof MyCommand> {
### Error Handling Pattern

```typescript
import {subdebug} from '@sanity/cli-core'
import {exitCodes, subdebug} from '@sanity/cli-core'

const debug = subdebug('feature:action')

Expand All @@ -452,7 +452,7 @@ try {

// Show user-friendly message
const message = error instanceof Error ? error.message : 'Unknown error'
this.output.error(`User-facing message: ${message}`, {exit: 1})
this.output.error(`User-facing message: ${message}`, {exit: exitCodes.RUNTIME_ERROR})
}
```

Expand Down
8 changes: 4 additions & 4 deletions packages/@sanity/cli/src/commands/backups/disable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ export class DisableBackupCommand extends SanityCommand<typeof DisableBackupComm
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
disableBackupDebug(`Failed to list datasets: ${message}`, error)
this.error(`Failed to list datasets: ${message}`, {exit: 1})
this.error(`Failed to list datasets: ${message}`, {exit: exitCodes.RUNTIME_ERROR})
}

if (datasets.length === 0) {
this.error('No datasets found in this project.', {exit: 1})
this.error('No datasets found in this project.', {exit: exitCodes.RUNTIME_ERROR})
}

if (dataset) {
Expand All @@ -93,7 +93,7 @@ export class DisableBackupCommand extends SanityCommand<typeof DisableBackupComm
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
disableBackupDebug(`Failed to disable backup for dataset`, error)
this.error(`Disabling dataset backup failed: ${message}`, {exit: 1})
this.error(`Disabling dataset backup failed: ${message}`, {exit: exitCodes.RUNTIME_ERROR})
}
}

Expand All @@ -111,7 +111,7 @@ export class DisableBackupCommand extends SanityCommand<typeof DisableBackupComm
} catch (error) {
const err = error as Error
disableBackupDebug(`Error fetching datasets`, err)
this.error(`Failed to fetch datasets:\n${err.message}`, {exit: 1})
this.error(`Failed to fetch datasets:\n${err.message}`, {exit: exitCodes.RUNTIME_ERROR})
}
}
}
16 changes: 9 additions & 7 deletions packages/@sanity/cli/src/commands/backups/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@ export class DownloadBackupCommand extends SanityCommand<typeof DownloadBackupCo
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
backupDownloadDebug(`Failed to list datasets: ${message}`, error)
this.error(`Failed to list datasets: ${message}`, {exit: 1})
this.error(`Failed to list datasets: ${message}`, {exit: exitCodes.RUNTIME_ERROR})
}

if (datasets.length === 0) {
this.error('No datasets found in this project.', {exit: 1})
this.error('No datasets found in this project.', {exit: exitCodes.RUNTIME_ERROR})
}

if (dataset) {
Expand Down Expand Up @@ -230,7 +230,7 @@ ${styleText('bold', 'backupId')}: ${styleText('cyan', opts.backupId)}`,
progressSpinner.fail()
const message = error instanceof Error ? error.message : String(error)
backupDownloadDebug(`Downloading dataset backup failed: ${message}`, error)
this.error(`Downloading dataset backup failed: ${message}`, {exit: 1})
this.error(`Downloading dataset backup failed: ${message}`, {exit: exitCodes.RUNTIME_ERROR})
}

docOutStream.end()
Expand All @@ -247,7 +247,7 @@ ${styleText('bold', 'backupId')}: ${styleText('cyan', opts.backupId)}`,
progressSpinner.fail()
const message = err instanceof Error ? err.message : String(err)
backupDownloadDebug(`Archiving backup failed: ${message}`, err)
this.error(`Archiving backup failed: ${message}`, {exit: 1})
this.error(`Archiving backup failed: ${message}`, {exit: exitCodes.RUNTIME_ERROR})
}

progressSpinner.set({
Expand Down Expand Up @@ -286,7 +286,7 @@ ${styleText('bold', 'backupId')}: ${styleText('cyan', opts.backupId)}`,
): Promise<DownloadBackupOptions> {
const err = validateDatasetName(datasetName)
if (err) {
this.error(err, {exit: 1})
this.error(err, {exit: exitCodes.RUNTIME_ERROR})
}

const backupId = String(
Expand Down Expand Up @@ -354,7 +354,7 @@ ${styleText('bold', 'backupId')}: ${styleText('cyan', opts.backupId)}`,
})

if (!response?.backups?.length) {
this.error('No backups found', {exit: 1})
this.error('No backups found', {exit: exitCodes.RUNTIME_ERROR})
}

const backupIdChoices = response.backups.map((backup: BackupItem) => ({
Expand All @@ -374,7 +374,9 @@ ${styleText('bold', 'backupId')}: ${styleText('cyan', opts.backupId)}`,
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
backupDownloadDebug(`Failed to fetch backups for dataset ${datasetName}: ${message}`, err)
this.error(`Failed to fetch backups for dataset ${datasetName}: ${message}`, {exit: 1})
this.error(`Failed to fetch backups for dataset ${datasetName}: ${message}`, {
exit: exitCodes.RUNTIME_ERROR,
})
}
}
}
10 changes: 6 additions & 4 deletions packages/@sanity/cli/src/commands/backups/enable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ export class EnableBackupCommand extends SanityCommand<typeof EnableBackupComman
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
enableBackupDebug(`Failed to list datasets: ${message}`, error)
this.error(`Failed to list datasets: ${message}`, {exit: 1})
this.error(`Failed to list datasets: ${message}`, {exit: exitCodes.RUNTIME_ERROR})
}

const hasProduction = datasets.some((dataset) => dataset.name === 'production')

if (datasets.length === 0) {
this.error('No datasets found in this project.', {exit: 1})
this.error('No datasets found in this project.', {exit: exitCodes.RUNTIME_ERROR})
}

if (dataset) {
Expand All @@ -97,7 +97,9 @@ export class EnableBackupCommand extends SanityCommand<typeof EnableBackupComman
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
enableBackupDebug(`Failed to create dataset ${newDatasetName}: ${message}`, error)
this.error(`Failed to create dataset ${newDatasetName}: ${message}`, {exit: 1})
this.error(`Failed to create dataset ${newDatasetName}: ${message}`, {
exit: exitCodes.RUNTIME_ERROR,
})
}
}
}
Expand All @@ -120,7 +122,7 @@ export class EnableBackupCommand extends SanityCommand<typeof EnableBackupComman
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
enableBackupDebug(`Failed to enable backup for dataset`, error)
this.error(`Enabling dataset backup failed: ${message}`, {exit: 1})
this.error(`Enabling dataset backup failed: ${message}`, {exit: exitCodes.RUNTIME_ERROR})
}
}
}
16 changes: 9 additions & 7 deletions packages/@sanity/cli/src/commands/backups/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ export class ListBackupCommand extends SanityCommand<typeof ListBackupCommand> {
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
listBackupDebug(`Failed to list datasets: ${message}`, error)
this.error(`Failed to list datasets: ${message}`, {exit: 1})
this.error(`Failed to list datasets: ${message}`, {exit: exitCodes.RUNTIME_ERROR})
}

if (datasets.length === 0) {
this.error('No datasets found in this project.', {exit: 1})
this.error('No datasets found in this project.', {exit: exitCodes.RUNTIME_ERROR})
}

if (dataset) {
Expand All @@ -115,17 +115,19 @@ export class ListBackupCommand extends SanityCommand<typeof ListBackupCommand> {
const parsedAfter = this.processDateFlag(flags.after, 'after')

if (parsedAfter && parsedBefore && isAfter(parsedAfter, parsedBefore)) {
this.error('--after date must be before --before', {exit: 1})
this.error('--after date must be before --before', {exit: exitCodes.RUNTIME_ERROR})
}
} catch (err) {
this.error(`Parsing date flags: ${err instanceof Error ? err.message : err}`, {exit: 1})
this.error(`Parsing date flags: ${err instanceof Error ? err.message : err}`, {
exit: exitCodes.RUNTIME_ERROR,
})
}
}

// Validate limit flag
if (flags.limit < 1 || flags.limit > Number.MAX_SAFE_INTEGER) {
this.error(`Parsing --limit: must be an integer between 1 and ${Number.MAX_SAFE_INTEGER}`, {
exit: 1,
exit: exitCodes.RUNTIME_ERROR,
})
}

Expand Down Expand Up @@ -180,7 +182,7 @@ export class ListBackupCommand extends SanityCommand<typeof ListBackupCommand> {
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
listBackupDebug(`Failed to list backups for dataset ${dataset}:`, error)
this.error(`List dataset backup failed: ${message}`, {exit: 1})
this.error(`List dataset backup failed: ${message}`, {exit: exitCodes.RUNTIME_ERROR})
}
}

Expand Down Expand Up @@ -208,7 +210,7 @@ export class ListBackupCommand extends SanityCommand<typeof ListBackupCommand> {
} catch (error) {
listBackupDebug(`Error selecting dataset`, error)
this.error(`Failed to select dataset:\n${error instanceof Error ? error.message : error}`, {
exit: 1,
exit: exitCodes.RUNTIME_ERROR,
})
}
}
Expand Down
10 changes: 5 additions & 5 deletions packages/@sanity/cli/src/commands/codemod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {existsSync} from 'node:fs'
import path from 'node:path'

import {Args, Flags} from '@oclif/core'
import {SanityCommand, subdebug} from '@sanity/cli-core'
import {exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'

import {codemods} from '../actions/codemods/index.js'
import {type CodeMod} from '../actions/codemods/types.js'
Expand Down Expand Up @@ -66,7 +66,7 @@ export class CodemodCommand extends SanityCommand<typeof CodemodCommand> {
// Verify that mod exists
const mod = normalizedMods.get(codemodName.toLowerCase())
if (!mod) {
this.error(`Codemod with name "${codemodName}" not found`, {exit: 1})
this.error(`Codemod with name "${codemodName}" not found`, {exit: exitCodes.RUNTIME_ERROR})
}

// Verify if there is any verification defined, and user has not opted out of it
Expand All @@ -76,7 +76,7 @@ export class CodemodCommand extends SanityCommand<typeof CodemodCommand> {
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error))
codemodeDebug('Verification failed: %s', err.message)
this.error(`Verification failed: ${err.message}`, {exit: 1})
this.error(`Verification failed: ${err.message}`, {exit: exitCodes.RUNTIME_ERROR})
}
}

Expand Down Expand Up @@ -133,12 +133,12 @@ export class CodemodCommand extends SanityCommand<typeof CodemodCommand> {
try {
const npxHelp = execSync('npx --help', {encoding: 'utf8'})
if (!npxHelp.includes('npm')) {
this.error('Not the npx we expected', {exit: 1})
this.error('Not the npx we expected', {exit: exitCodes.RUNTIME_ERROR})
}
} catch {
this.error(
`Failed to run "npx" - required to run codemods. Do you have a recent version of npm installed?`,
{exit: 1},
{exit: exitCodes.RUNTIME_ERROR},
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@sanity/cli/src/commands/cors/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export class Add extends SanityCommand<typeof Add> {
const err = error as Error

addCorsDebug(`Error adding CORS origin`, err)
this.error(`CORS origin addition failed:\n${err.message}`, {exit: 1})
this.error(`CORS origin addition failed:\n${err.message}`, {exit: exitCodes.RUNTIME_ERROR})
}
}

Expand Down
8 changes: 4 additions & 4 deletions packages/@sanity/cli/src/commands/cors/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class Delete extends SanityCommand<typeof Delete> {
} catch (error) {
const err = error as Error
deleteCorsDebug(`Error deleting CORS origin ${originId} for project ${projectId}`, err)
this.error(`Origin deletion failed:\n${err.message}`, {exit: 1})
this.error(`Origin deletion failed:\n${err.message}`, {exit: exitCodes.RUNTIME_ERROR})
}
}

Expand All @@ -81,11 +81,11 @@ export class Delete extends SanityCommand<typeof Delete> {
} catch (error) {
const err = error as Error
deleteCorsDebug(`Error fetching CORS origins for project ${projectId}`, err)
this.error(`Failed to fetch CORS origins:\n${err.message}`, {exit: 1})
this.error(`Failed to fetch CORS origins:\n${err.message}`, {exit: exitCodes.RUNTIME_ERROR})
}

if (origins.length === 0) {
this.error('No CORS origins configured for this project.', {exit: 1})
this.error('No CORS origins configured for this project.', {exit: exitCodes.RUNTIME_ERROR})
}

// If origin is specified, find it in the list
Expand All @@ -96,7 +96,7 @@ export class Delete extends SanityCommand<typeof Delete> {
)

if (!selectedOrigin) {
this.error(`Origin "${specifiedOrigin}" not found`, {exit: 1})
this.error(`Origin "${specifiedOrigin}" not found`, {exit: exitCodes.RUNTIME_ERROR})
}

return selectedOrigin.id
Expand Down
6 changes: 4 additions & 2 deletions packages/@sanity/cli/src/commands/cors/list.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {SanityCommand, subdebug} from '@sanity/cli-core'
import {exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'

import {promptForProject} from '../../prompts/promptForProject.js'
import {type CorsOrigin, listCorsOrigins} from '../../services/cors.js'
Expand Down Expand Up @@ -43,7 +43,9 @@ export class List extends SanityCommand<typeof List> {
const err = error as Error

listCorsDebug(`Error fetching CORS origins for project ${projectId}`, err)
this.error(`CORS origins list retrieval failed:\n${err.message}`, {exit: 1})
this.error(`CORS origins list retrieval failed:\n${err.message}`, {
exit: exitCodes.RUNTIME_ERROR,
})
}

if (origins.length === 0) {
Expand Down
12 changes: 7 additions & 5 deletions packages/@sanity/cli/src/commands/datasets/alias/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ export class CreateAliasCommand extends SanityCommand<typeof CreateAliasCommand>
canCreateAlias = features.includes('advancedDatasetManagement')
} catch (error) {
createAliasDebug(`Error getting project features`, error)
this.error('Failed to get project features', {exit: 1})
this.error('Failed to get project features', {exit: exitCodes.RUNTIME_ERROR})
}
if (!canCreateAlias) {
this.error('This project cannot create a dataset alias - see https://www.sanity.io/pricing', {
exit: 1,
exit: exitCodes.RUNTIME_ERROR,
})
}

Expand All @@ -122,7 +122,9 @@ export class CreateAliasCommand extends SanityCommand<typeof CreateAliasCommand>
}

if (existingAliases.includes(aliasName)) {
this.error(`Dataset alias "${aliasOutputName}" already exists`, {exit: 1})
this.error(`Dataset alias "${aliasOutputName}" already exists`, {
exit: exitCodes.RUNTIME_ERROR,
})
}

const targetDataset =
Expand All @@ -132,7 +134,7 @@ export class CreateAliasCommand extends SanityCommand<typeof CreateAliasCommand>
if (targetDataset && !datasets.includes(targetDataset)) {
this.error(
`Dataset "${targetDataset}" does not exist. Available datasets: ${datasets.join(', ')}`,
{exit: 1},
{exit: exitCodes.RUNTIME_ERROR},
)
}

Expand All @@ -145,7 +147,7 @@ export class CreateAliasCommand extends SanityCommand<typeof CreateAliasCommand>
createAliasDebug(`Error creating dataset alias`, error)
this.error(
`Dataset alias creation failed: ${error instanceof Error ? error.message : String(error)}`,
{exit: 1},
{exit: exitCodes.RUNTIME_ERROR},
)
}
}
Expand Down
Loading
Loading