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
20 changes: 12 additions & 8 deletions src/commands/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import {
log,
NETLIFY_CYAN,
USER_AGENT,
warn,
logError,
} from '../utils/command-helpers.js'
import execa from '../utils/execa.js'
import { EXIT_CODES } from '../utils/exit-codes.js'
import getCLIPackageJson from '../utils/get-cli-package-json.js'
import { didEnableCompileCache } from '../utils/nodejs-compile-cache.js'
import { handleOptionError, isOptionError } from '../utils/command-error-handler.js'
Expand Down Expand Up @@ -197,21 +197,23 @@ const mainCommand = async function (options, command) {
command.help()
}

warn(`${chalk.yellow(command.args[0])} is not a ${command.name()} command.`)
process.stderr.write(
` ${chalk.yellow(BANG)} Warning: ${chalk.yellow(command.args[0])} is not a ${command.name()} command.\n`,
)

// @ts-expect-error TS(7006) FIXME: Parameter 'cmd' implicitly has an 'any' type.
const allCommands = command.commands.map((cmd) => cmd.name())
const suggestion = closest(command.args[0], allCommands)

// In non-interactive environments (CI/CD, scripts), show the suggestion
// without prompting, and display full help for available commands
// without prompting, and display full help for available commands.
// Diagnostics belong on stderr so stdout stays clean for machine consumers.
if (!isInteractive()) {
log(`\nDid you mean ${chalk.blue(suggestion)}?`)
log()
process.stderr.write(`\nDid you mean ${chalk.blue(suggestion)}?\n\n`)
command.outputHelp({ error: true })
log()
process.stderr.write('\n')
logError(`Run ${NETLIFY_CYAN(`${command.name()} help`)} for a list of available commands.`)
exit(1)
exit(EXIT_CODES.USAGE_ERROR)
}

const applySuggestion = await new Promise((resolve) => {
Expand All @@ -237,7 +239,7 @@ const mainCommand = async function (options, command) {

if (!applySuggestion) {
logError(`Run ${NETLIFY_CYAN(`${command.name()} help`)} for a list of available commands.`)
exit(1)
exit(EXIT_CODES.USAGE_ERROR)
}

await execa(process.argv[0], [process.argv[1], suggestion], { stdio: 'inherit' })
Expand Down Expand Up @@ -299,6 +301,8 @@ export const createMainCommand = (): BaseCommand => {
return `To get started run: ${NETLIFY_CYAN('netlify login')}
To ask a human for credentials: ${NETLIFY_CYAN('netlify login --request <msg>')}

Exit codes: 0 ok, 1 error, 2 usage, 4 needs-input

→ For more help with the CLI, visit ${NETLIFY_CYAN(
terminalLink(cliDocsEntrypointUrl, cliDocsEntrypointUrl, { fallback: false }),
)}
Expand Down
10 changes: 10 additions & 0 deletions src/utils/command-error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ const OPTION_ERROR_CODES = new Set([
'commander.excessArguments',
])

/** Every Commander error code caused by bad user input; these exit with `EXIT_CODES.USAGE_ERROR` */
export const USAGE_ERROR_CODES = new Set([
...OPTION_ERROR_CODES,
'commander.unknownCommand',
'commander.optionMissingArgument',
'commander.missingMandatoryOptionValue',
'commander.invalidArgument',
'commander.conflictingOption',
])

export const isOptionError = (error: CommanderError): boolean => OPTION_ERROR_CODES.has(error.code)

export const handleOptionError = (command: { outputHelp: (context?: HelpContext) => void }): void => {
Expand Down
25 changes: 25 additions & 0 deletions src/utils/exit-codes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* The CLI's exit code dictionary.
*
* Contract:
* - `1` is the legacy catch-all: any unclassified failure exits 1. Scripts must treat
* any non-zero exit as failure and may use the specific codes below for diagnosis.
* - New, more specific codes are only ever adopted on paths that previously failed
* (exited non-zero). A previously succeeding invocation never starts failing, and a
* previously failing invocation never starts succeeding, because a code was added here.
* - `netlify build` additionally passes through `@netlify/build` severity codes.
*
* The dictionary is surfaced in the root `netlify --help` epilogue.
*/
export const EXIT_CODES = {
/** Command completed successfully */
SUCCESS: 0,
/** Legacy/unclassified failure (the historical catch-all) */
GENERAL_ERROR: 1,
/** Usage error: unknown command, unknown option, or bad arguments */
USAGE_ERROR: 2,
/** An interactive prompt was required but the session is non-interactive (CI or `--non-interactive`) */
NON_INTERACTIVE_PROMPT: 4,
} as const

export type ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES]
5 changes: 5 additions & 0 deletions src/utils/run-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { CommanderError } from 'commander'
import { injectForceFlagIfScripted } from './scripted-commands.js'
import { BaseCommand } from '../commands/index.js'
import { CI_FORCED_COMMANDS } from '../commands/main.js'
import { USAGE_ERROR_CODES } from './command-error-handler.js'
import { exit } from './command-helpers.js'
import { EXIT_CODES } from './exit-codes.js'

export const runProgram = async (program: BaseCommand, argv: string[]) => {
const cmdName = argv[2]
Expand All @@ -18,6 +20,9 @@ export const runProgram = async (program: BaseCommand, argv: string[]) => {
await program.parseAsync(argv)
} catch (error) {
if (error instanceof CommanderError) {
if (error.exitCode !== 0 && USAGE_ERROR_CODES.has(error.code)) {
exit(EXIT_CODES.USAGE_ERROR)
}
exit(error.exitCode)
}
throw error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,247 @@
exports[`suggests closest matching command on typo 1`] = `
"› Warning: sta is not a netlify command.

Did you mean api?"
Did you mean api?


⬥ Netlify CLI

VERSION
netlify-cli/test-version test-os test-node-version

USAGE
$ netlify [COMMAND]

COMMANDS
$ agents Manage Netlify AI agent tasks
$ api Run any Netlify API method
$ blobs Manage objects in Netlify Blobs
$ build Build on your local machine
$ claim Claim an anonymously deployed site and link it to your account
$ clone Clone a remote repository and link it to an existing project
on Netlify
$ completion Generate shell completion script
$ create Create a new Netlify project using an AI agent
$ database Provision a production ready Postgres database with a single
command
$ deploy Deploy your project to Netlify
$ dev Local dev server
$ env Control environment variables for the current project
$ functions Manage netlify functions
$ init Configure continuous deployment for a new or existing project.
To create a new project without continuous deployment, use
\`netlify sites:create\`
$ link Link a local repo or project folder to an existing project on
Netlify
$ login Login to your Netlify account
$ logs View logs from your project
$ open Open settings for the project linked to the current folder
$ recipes Create and modify files in a project using pre-defined recipes
$ serve Build the project for production and serve locally. This does
not watch the code for changes, so if you need to rebuild your
project then you must exit and run \`serve\` again.
$ sites Handle various project operations
$ status Print status information
$ switch Switch your active Netlify account
$ teams Handle various team operations
$ unlink Unlink a local folder from a Netlify project
$ watch Watch for project deploy to finish

To get started run: netlify login
To ask a human for credentials: netlify login --request <msg>

Exit codes: 0 ok, 1 error, 2 usage, 4 needs-input

→ For more help with the CLI, visit https://developers.netlify.com/cli
→ For help with Netlify, visit https://docs.netlify.com
→ To report a CLI bug, visit https://github.com/netlify/cli/issues


› Error: Run netlify help for a list of available commands."
`;

exports[`suggests closest matching command on typo 2`] = `
"› Warning: opeen is not a netlify command.

Did you mean open?"
Did you mean open?


⬥ Netlify CLI

VERSION
netlify-cli/test-version test-os test-node-version

USAGE
$ netlify [COMMAND]

COMMANDS
$ agents Manage Netlify AI agent tasks
$ api Run any Netlify API method
$ blobs Manage objects in Netlify Blobs
$ build Build on your local machine
$ claim Claim an anonymously deployed site and link it to your account
$ clone Clone a remote repository and link it to an existing project
on Netlify
$ completion Generate shell completion script
$ create Create a new Netlify project using an AI agent
$ database Provision a production ready Postgres database with a single
command
$ deploy Deploy your project to Netlify
$ dev Local dev server
$ env Control environment variables for the current project
$ functions Manage netlify functions
$ init Configure continuous deployment for a new or existing project.
To create a new project without continuous deployment, use
\`netlify sites:create\`
$ link Link a local repo or project folder to an existing project on
Netlify
$ login Login to your Netlify account
$ logs View logs from your project
$ open Open settings for the project linked to the current folder
$ recipes Create and modify files in a project using pre-defined recipes
$ serve Build the project for production and serve locally. This does
not watch the code for changes, so if you need to rebuild your
project then you must exit and run \`serve\` again.
$ sites Handle various project operations
$ status Print status information
$ switch Switch your active Netlify account
$ teams Handle various team operations
$ unlink Unlink a local folder from a Netlify project
$ watch Watch for project deploy to finish

To get started run: netlify login
To ask a human for credentials: netlify login --request <msg>

Exit codes: 0 ok, 1 error, 2 usage, 4 needs-input

→ For more help with the CLI, visit https://developers.netlify.com/cli
→ For help with Netlify, visit https://docs.netlify.com
→ To report a CLI bug, visit https://github.com/netlify/cli/issues


› Error: Run netlify help for a list of available commands."
`;

exports[`suggests closest matching command on typo 3`] = `
"› Warning: hel is not a netlify command.

Did you mean dev?"
Did you mean dev?


⬥ Netlify CLI

VERSION
netlify-cli/test-version test-os test-node-version

USAGE
$ netlify [COMMAND]

COMMANDS
$ agents Manage Netlify AI agent tasks
$ api Run any Netlify API method
$ blobs Manage objects in Netlify Blobs
$ build Build on your local machine
$ claim Claim an anonymously deployed site and link it to your account
$ clone Clone a remote repository and link it to an existing project
on Netlify
$ completion Generate shell completion script
$ create Create a new Netlify project using an AI agent
$ database Provision a production ready Postgres database with a single
command
$ deploy Deploy your project to Netlify
$ dev Local dev server
$ env Control environment variables for the current project
$ functions Manage netlify functions
$ init Configure continuous deployment for a new or existing project.
To create a new project without continuous deployment, use
\`netlify sites:create\`
$ link Link a local repo or project folder to an existing project on
Netlify
$ login Login to your Netlify account
$ logs View logs from your project
$ open Open settings for the project linked to the current folder
$ recipes Create and modify files in a project using pre-defined recipes
$ serve Build the project for production and serve locally. This does
not watch the code for changes, so if you need to rebuild your
project then you must exit and run \`serve\` again.
$ sites Handle various project operations
$ status Print status information
$ switch Switch your active Netlify account
$ teams Handle various team operations
$ unlink Unlink a local folder from a Netlify project
$ watch Watch for project deploy to finish

To get started run: netlify login
To ask a human for credentials: netlify login --request <msg>

Exit codes: 0 ok, 1 error, 2 usage, 4 needs-input

→ For more help with the CLI, visit https://developers.netlify.com/cli
→ For help with Netlify, visit https://docs.netlify.com
→ To report a CLI bug, visit https://github.com/netlify/cli/issues


› Error: Run netlify help for a list of available commands."
`;

exports[`suggests closest matching command on typo 4`] = `
"› Warning: versio is not a netlify command.

Did you mean serve?"
Did you mean serve?


⬥ Netlify CLI

VERSION
netlify-cli/test-version test-os test-node-version

USAGE
$ netlify [COMMAND]

COMMANDS
$ agents Manage Netlify AI agent tasks
$ api Run any Netlify API method
$ blobs Manage objects in Netlify Blobs
$ build Build on your local machine
$ claim Claim an anonymously deployed site and link it to your account
$ clone Clone a remote repository and link it to an existing project
on Netlify
$ completion Generate shell completion script
$ create Create a new Netlify project using an AI agent
$ database Provision a production ready Postgres database with a single
command
$ deploy Deploy your project to Netlify
$ dev Local dev server
$ env Control environment variables for the current project
$ functions Manage netlify functions
$ init Configure continuous deployment for a new or existing project.
To create a new project without continuous deployment, use
\`netlify sites:create\`
$ link Link a local repo or project folder to an existing project on
Netlify
$ login Login to your Netlify account
$ logs View logs from your project
$ open Open settings for the project linked to the current folder
$ recipes Create and modify files in a project using pre-defined recipes
$ serve Build the project for production and serve locally. This does
not watch the code for changes, so if you need to rebuild your
project then you must exit and run \`serve\` again.
$ sites Handle various project operations
$ status Print status information
$ switch Switch your active Netlify account
$ teams Handle various team operations
$ unlink Unlink a local folder from a Netlify project
$ watch Watch for project deploy to finish

To get started run: netlify login
To ask a human for credentials: netlify login --request <msg>

Exit codes: 0 ok, 1 error, 2 usage, 4 needs-input

→ For more help with the CLI, visit https://developers.netlify.com/cli
→ For help with Netlify, visit https://docs.netlify.com
→ To report a CLI bug, visit https://github.com/netlify/cli/issues


› Error: Run netlify help for a list of available commands."
`;
4 changes: 2 additions & 2 deletions tests/integration/commands/didyoumean/didyoumean.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ test('suggests closest matching command on typo', async (t) => {

for (const error of errors) {
t.expect(error.status).toEqual('rejected')
t.expect(error).toHaveProperty('reason.stdout', t.expect.any(String))
t.expect(error).toHaveProperty('reason.stderr', t.expect.any(String))
t.expect(
normalize((error as { reason: { stdout: string } }).reason.stdout, { duration: true, filePath: true }),
normalize((error as { reason: { stderr: string } }).reason.stderr, { duration: true, filePath: true }),
).toMatchSnapshot()
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ COMMANDS
To get started run: netlify login
To ask a human for credentials: netlify login --request <msg>

Exit codes: 0 ok, 1 error, 2 usage, 4 needs-input

→ For more help with the CLI, visit https://developers.netlify.com/cli
→ For help with Netlify, visit https://docs.netlify.com
→ To report a CLI bug, visit https://github.com/netlify/cli/issues"
Expand Down
Loading
Loading