Skip to content
Open
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
35 changes: 35 additions & 0 deletions docs/commands/capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Netlify CLI capabilities command
sidebar:
label: capabilities
---

# `capabilities`

The `capabilities` command prints a machine-readable JSON manifest describing the CLI itself: every command and its flags, which commands support `--json`, the exit-code dictionary, relevant environment variables, and config file locations. It is intended for scripts and AI agents that need to discover the CLI's surface without scraping `--help` output.

Check warning on line 9 in docs/commands/capabilities.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [smart-marks.smartApostrophes] Use a smart apostrophe (’) instead of a straight single quote mark in 'CLI's' Raw Output: {"message": "[smart-marks.smartApostrophes] Use a smart apostrophe (’) instead of a straight single quote mark in 'CLI's'", "location": {"path": "docs/commands/capabilities.md", "range": {"start": {"line": 9, "column": 309}}}, "severity": "WARNING"}

Check warning on line 9 in docs/commands/capabilities.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'CLI's'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'CLI's'?", "location": {"path": "docs/commands/capabilities.md", "range": {"start": {"line": 9, "column": 309}}}, "severity": "WARNING"}

Check warning on line 9 in docs/commands/capabilities.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'config'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'config'?", "location": {"path": "docs/commands/capabilities.md", "range": {"start": {"line": 9, "column": 219}}}, "severity": "WARNING"}

<!-- AUTO-GENERATED-CONTENT:START (GENERATE_COMMANDS_DOCS) -->
Print a machine-readable manifest of every command, its flags, exit codes, env vars, and config files

Check warning on line 12 in docs/commands/capabilities.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'config'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'config'?", "location": {"path": "docs/commands/capabilities.md", "range": {"start": {"line": 12, "column": 90}}}, "severity": "WARNING"}

Check warning on line 12 in docs/commands/capabilities.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'env'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'env'?", "location": {"path": "docs/commands/capabilities.md", "range": {"start": {"line": 12, "column": 76}}}, "severity": "WARNING"}
Intended for scripts and AI agents. Output is always JSON on stdout.

Check warning on line 13 in docs/commands/capabilities.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'stdout'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'stdout'?", "location": {"path": "docs/commands/capabilities.md", "range": {"start": {"line": 13, "column": 62}}}, "severity": "WARNING"}

**Usage**

```bash
netlify capabilities
```

**Flags**

- `json` (*boolean*) - Output capabilities as JSON (the default; this command always outputs JSON)
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in

**Examples**

```bash
netlify capabilities
netlify capabilities --json
```


<!-- AUTO-GENERATED-CONTENT:END -->
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@

Build on your local machine

### [capabilities](/commands/capabilities)

Print a machine-readable manifest of every command, its flags, exit codes, env vars, and config files

Check warning on line 55 in docs/index.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'config'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'config'?", "location": {"path": "docs/index.md", "range": {"start": {"line": 55, "column": 90}}}, "severity": "WARNING"}

Check warning on line 55 in docs/index.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'env'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'env'?", "location": {"path": "docs/index.md", "range": {"start": {"line": 55, "column": 76}}}, "severity": "WARNING"}

### [claim](/commands/claim)

Claim an anonymously deployed site and link it to your account
Expand Down
2 changes: 1 addition & 1 deletion src/commands/agents/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import requiresSiteInfoWithProject from '../../utils/hooks/requires-site-info-wi
import type BaseCommand from '../base-command.js'

const agents = (_options: OptionValues, command: BaseCommand) => {
command.help()
command.helpOrRejectExtraArgs()
}

export const createAgentsCommand = (program: BaseCommand) => {
Expand Down
27 changes: 26 additions & 1 deletion src/commands/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,39 @@ export const apiCommand = async (apiMethodName: string, options: OptionValues, c

let payload
if (options.data) {
payload = typeof options.data === 'string' ? JSON.parse(options.data) : options.data
if (typeof options.data === 'string') {
try {
payload = JSON.parse(options.data)
} catch {
const received = options.data.length > 80 ? `${options.data.slice(0, 80)}…` : options.data
return logAndThrowError(
`Invalid JSON provided to the ${chalk.cyanBright('--data')} flag.
Received: ${received}
The --data flag expects a JSON object of API parameters, e.g. --data '{"site_id":"123456"}'.
Note: key=value pairs are not accepted; use JSON syntax instead.`,
)
}
} else {
payload = options.data
}
} else {
payload = {}
}
try {
const apiResponse = await apiMethod(payload)
logJson(apiResponse)
} catch (error_) {
if (error_ instanceof Error && error_.message.includes('Missing required path variable')) {
const apiMethods = methods as { operationId: string; parameters: { path?: Record<string, unknown> } }[]
const pathVariables = apiMethods.find((method) => method.operationId === apiMethodName)?.parameters.path ?? {}
const requiredNames = Object.keys(pathVariables).join(', ')
return logAndThrowError(
`${error_.message}
The ${chalk.cyanBright('--data')} flag must include the path variable(s) required by ${apiMethodName}${
requiredNames ? `: ${requiredNames}` : ''
}, e.g. --data '{"site_id":"123456"}'`,
)
}
Comment on lines +65 to +75

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify the type definition of methods from `@netlify/api`

# Check if `@netlify/api` has type definitions
rg -n "export.*methods" node_modules/@netlify/api --type=ts -A 5

# Also check for the type definition in .d.ts files
fd -e d.ts . node_modules/@netlify/api --exec rg -n "methods" -A 10

Repository: netlify/cli

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section around the snippet
sed -n '1,140p' src/commands/api/api.ts | nl -ba | sed -n '40,120p'

# Find how `methods` is imported/typed in this file
rg -n "import .*methods|from ['\"]`@netlify/api`['\"]" src/commands/api/api.ts

# Check other usages of `methods` in the repo (to understand assumed runtime shape)
rg -n "\bmethods\b" src | head -n 200

Repository: netlify/cli

Length of output: 98


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant section around the snippet without `nl`
sed -n '40,120p' src/commands/api/api.ts | sed -n '1,80p'

# Find how `methods` is imported/typed in this file
rg -n "import .*methods|from ['\"]`@netlify/api`['\"]" src/commands/api/api.ts

# Check other usages of `methods` in this repo (limits output)
rg -n "\bmethods\b" src | head -n 200

Repository: netlify/cli

Length of output: 2933


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the code around the snippet and the earlier usage of `methods`
sed -n '1,120p' src/commands/api/api.ts
sed -n '120,220p' src/commands/api/api.ts

# Inspect `@netlify/api` typing for `methods` and related types
sed -n '1,120p' node_modules/@netlify/api/lib/index.d.ts

# Search for more specific types/shapes around methods/operationId/parameters.path
rg -n "operationId|parameters|path\\??" node_modules/@netlify/api/lib/index.d.ts
rg -n "interface .*method|type .*method|declare const methods" node_modules/@netlify/api/lib -S

Repository: netlify/cli

Length of output: 5169


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "\bmethods\.|methods\b.*operationId|as \{ operationId" src
rg -n "from '`@netlify/api`'" src

Repository: netlify/cli

Length of output: 2267


Guard against unchecked methods shape when building the “missing path variable” message

@netlify/api types methods as any[], so the cast to { operationId: string; parameters: { path?: Record<string, unknown> } }[] is unchecked and can fail at runtime if the element shape differs. Add runtime guards/safe access before reading operationId and parameters.path (or extract via a small helper type-guard) so this error-path doesn’t crash.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/api/api.ts` around lines 65 - 75, The current cast of methods to
a specific shape is unsafe and can throw when building the missing-path error
message; add a runtime type guard (e.g., isApiMethod(obj): obj has
operationId:string and optional parameters.path:Record<string,unknown>) and use
it to filter/map methods before calling find by apiMethodName, then safely read
parameters?.path and guard Object.keys with a default empty object; update the
block that computes apiMethods, pathVariables and requiredNames (references:
methods, apiMethods, apiMethodName, parameters.path, logAndThrowError) to use
these checks so the error path never assumes the shape.

return logAndThrowError(error_)
}
}
94 changes: 77 additions & 17 deletions src/commands/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getGlobalConfigStore, LocalState } from '@netlify/dev-utils'
import { isCI } from 'ci-info'
import { Command, CommanderError, Help, Option, type OptionValues } from 'commander'
import debug from 'debug'
import { closest, distance } from 'fastest-levenshtein'
import { findUp } from 'find-up'
import inquirer from 'inquirer'
import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt'
Expand All @@ -19,6 +20,7 @@ import pick from 'lodash/pick.js'

import { getAgent } from '../lib/http-agent.js'
import {
BANG,
NETLIFY_CYAN,
USER_AGENT,
chalk,
Expand All @@ -35,12 +37,14 @@ import {
warn,
logError,
} from '../utils/command-helpers.js'
import { handleOptionError, isOptionError } from '../utils/command-error-handler.js'
import { handleOptionError, isOptionError, suggestUnknownOptionAlternatives } from '../utils/command-error-handler.js'
import { guardGlobalConfigFile, guardLocalStateFile } from '../utils/config-guard.js'
import { EXIT_CODES } from '../utils/exit-codes.js'
import type { FeatureFlags } from '../utils/feature-flags.js'
import { getFrameworksAPIPaths } from '../utils/frameworks-api.js'
import { getSiteByName } from '../utils/get-site.js'
import openBrowser from '../utils/open-browser.js'
import { isInteractive } from '../utils/scripted-commands.js'
import { failOnNonInteractivePrompt, isInteractive } from '../utils/scripted-commands.js'
import { identify, reportError, track } from '../utils/telemetry/index.js'
import type { NetlifyOptions } from './types.js'
import type { CachedConfig } from '../lib/build.js'
Expand Down Expand Up @@ -74,6 +78,7 @@ const HELP_SEPARATOR_WIDTH = 5
*/
const COMMANDS_WITHOUT_WORKSPACE_OPTIONS = new Set([
'api',
'capabilities',
'recipes',
'completion',
'status',
Expand Down Expand Up @@ -138,13 +143,14 @@ async function selectWorkspace(project: Project, filter?: string): Promise<strin
log()
log(chalk.cyan(`We've detected multiple projects inside your repository`))

if (isCI) {
throw new Error(
if (isCI || !isInteractive()) {
return failOnNonInteractivePrompt(
'Select the project you want to work with',
`Projects detected: ${(project.workspace?.packages || [])
.map((pkg) => pkg.name || pkg.path)
.join(
', ',
)}. Configure the project you want to work with and try again. Refer to https://ntl.fyi/configure-site for more information.`,
.join(', ')}. Pass ${chalk.cyanBright('--filter <app>')} or ${chalk.cyanBright(
'--cwd <path>',
)} to configure the project you want to work with and try again. Refer to https://ntl.fyi/configure-site for more information.`,
)
}

Expand Down Expand Up @@ -183,6 +189,7 @@ export type BaseOptionValues = {
debug?: boolean
filter?: string
httpProxy?: string
nonInteractive?: boolean
silent?: string
verbose?: boolean
}
Expand Down Expand Up @@ -246,6 +253,12 @@ export default class BaseCommand extends Command {
const commandName = name || ''
const base = new BaseCommand(commandName)
.addOption(new Option('--silent', 'Silence CLI output').hideHelp(true))
.addOption(
new Option(
'--non-interactive',
'Never open prompts; fail with exit code 4 when input would be required',
).hideHelp(true),
)
.addOption(new Option('--cwd <cwd>').hideHelp(true))
.addOption(
new Option('--auth <token>', 'Netlify auth token - can be used to run this command without logging in'),
Expand Down Expand Up @@ -290,6 +303,7 @@ export default class BaseCommand extends Command {
// brief error message, making it easier for users in CI/CD environments to
// understand what went wrong.
this.exitOverride((error: CommanderError) => {
suggestUnknownOptionAlternatives(this, error)
if (isOptionError(error)) {
handleOptionError(this)
}
Expand All @@ -304,12 +318,56 @@ export default class BaseCommand extends Command {
}

#noBaseOptions = false

get noBaseOptions(): boolean {
return this.#noBaseOptions
}

/** don't show help options on command overview (mostly used on top commands like `addons` where options only apply on children) */
noHelpOptions() {
this.#noBaseOptions = true
return this
}

/**
* Rejects space-form subcommand invocations (e.g. `netlify sites delete`) on namespace
* commands with a colon-form did-you-mean (`netlify sites:delete`) instead of silently
* succeeding. No-op when no positional arguments were given.
*/
rejectSpaceFormSubcommand(): void {
if (this.args.length === 0) {
return
}

const attempted = this.args[0]
const colonForm = `${this.name()}:${attempted}`
const subcommandNames = (this.parent ?? this).commands
.map((cmd) => cmd.name())
.filter((cmdName) => cmdName.startsWith(`${this.name()}:`))
const exactMatch = subcommandNames.find((cmdName) => cmdName === colonForm)
const nearest = exactMatch ?? (subcommandNames.length === 0 ? undefined : closest(colonForm, subcommandNames))

const bang = chalk.red(BANG)
process.stderr.write(` ${bang} Error: 'netlify ${this.name()} ${attempted}' is not a command.\n`)
if (nearest !== undefined && (exactMatch !== undefined || distance(colonForm, nearest) <= 3)) {
const remainingArgs = this.args.slice(1).join(' ')
process.stderr.write(
` ${bang} Did you mean 'netlify ${nearest}${remainingArgs === '' ? '' : ` ${remainingArgs}`}'?\n`,
)
}
process.stderr.write(` ${bang} Run 'netlify ${this.name()} --help' to see available subcommands.\n`)
exit(EXIT_CODES.USAGE_ERROR)
}

/**
* Action for namespace-only parent commands (e.g. `sites`, `env`): shows help when
* called bare, errors with a colon-form suggestion when positional arguments are given.
*/
helpOrRejectExtraArgs(): void {
this.rejectSpaceFormSubcommand()
this.help()
}

/** The examples list for the command (used inside doc generation and help page) */
examples: string[] = []
/** Set examples for the command */
Expand Down Expand Up @@ -353,7 +411,6 @@ export default class BaseCommand extends Command {

/** override the longestOptionTermLength to react on hide options flag */
help.longestOptionTermLength = (command: BaseCommand, helper: Help): number =>
// @ts-expect-error TS(2551) FIXME: Property 'noBaseOptions' does not exist on type 'C... Remove this comment to see the full error message
(command.noBaseOptions === false &&
helper.visibleOptions(command).reduce((max, option) => Math.max(max, helper.optionTerm(option).length), 0)) ||
0
Expand All @@ -367,9 +424,9 @@ export default class BaseCommand extends Command {
const bang = isCommand ? `${HELP_$} ` : ''

if (description) {
const pad = termWidth + HELP_SEPARATOR_WIDTH
const fullText = `${bang}${term.padEnd(pad - (isCommand ? 2 : 0))}${chalk.grey(description)}`
return helper.wrap(fullText, helpWidth - HELP_INDENT_WIDTH, pad)
const pad = Math.max(termWidth + HELP_SEPARATOR_WIDTH - (isCommand ? 2 : 0), term.length + 2)
const fullText = `${bang}${term.padEnd(pad)}${chalk.grey(description)}`
return helper.wrap(fullText, helpWidth - HELP_INDENT_WIDTH, pad + (isCommand ? 2 : 0))
}

return `${bang}${term}`
Expand Down Expand Up @@ -480,12 +537,13 @@ export default class BaseCommand extends Command {
return token
}
if (!isInteractive()) {
return logAndThrowError(
`Authentication required. NETLIFY_AUTH_TOKEN is not set and ${chalk.cyanBright(
'`netlify status`',
)} also informs us that you need to use ${chalk.cyanBright(
return failOnNonInteractivePrompt(
'Logging in to your Netlify account',
`Authentication required. Set the ${chalk.cyanBright(
'NETLIFY_AUTH_TOKEN',
)} environment variable or pass ${chalk.cyanBright('--auth <token>')}, or use ${chalk.cyanBright(
'`netlify login --request <message>`',
)} as a next step.`,
)} to ask a human for credentials.`,
)
}
const accessToken = await this.expensivelyAuthenticate()
Expand Down Expand Up @@ -653,6 +711,8 @@ export default class BaseCommand extends Command {
// ==================================================
// Retrieve Site id and build state from the state.json
// ==================================================
guardGlobalConfigFile()
await guardLocalStateFile(this.workingDir)
const state = new LocalState(this.workingDir)
const [token] = await getToken(flags.auth)

Expand Down Expand Up @@ -884,4 +944,4 @@ export default class BaseCommand extends Command {
}

export const getBaseOptionValues = (options: OptionValues): BaseOptionValues =>
pick(options, ['auth', 'cwd', 'debug', 'filter', 'httpProxy', 'silent', 'verbose'])
pick(options, ['auth', 'cwd', 'debug', 'filter', 'httpProxy', 'nonInteractive', 'silent', 'verbose'])
2 changes: 1 addition & 1 deletion src/commands/blobs/blobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import BaseCommand from '../base-command.js'
* The blobs command
*/
const blobs = (_options: OptionValues, command: BaseCommand) => {
command.help()
command.helpOrRejectExtraArgs()
}

/**
Expand Down
Loading
Loading