netlify-cli 27.12.1 → 27.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,8 +38,8 @@ export const agentsStop = async (id, options, command) => {
38
38
  }
39
39
  // Stop the agent task
40
40
  const stopSpinnerInstance = startSpinner({ text: 'Stopping agent task...' });
41
- const response = await fetch(`${apiOpts.scheme ?? 'https'}://${apiOpts.host ?? api.host}/api/v1/agent_runners/${id}`, {
42
- method: 'DELETE',
41
+ const response = await fetch(`${apiOpts.scheme ?? 'https'}://${apiOpts.host ?? api.host}/api/v1/agent_runners/${id}/stop`, {
42
+ method: 'POST',
43
43
  headers: {
44
44
  Authorization: `Bearer ${api.accessToken ?? ''}`,
45
45
  'User-Agent': apiOpts.userAgent,
@@ -234,6 +234,8 @@ export default class BaseCommand extends Command {
234
234
  const getCommands = (command) => {
235
235
  const parentCommand = this.name() === 'netlify' ? command : command.parent;
236
236
  return (parentCommand?.commands
237
+ // subcommands nested with `.command()` rather than named `parent:child`, e.g. `database connect`
238
+ .concat(this.name() === 'netlify' ? [] : command.commands)
237
239
  .filter((cmd) => {
238
240
  if (cmd._hidden)
239
241
  return false;
@@ -242,11 +244,13 @@ export default class BaseCommand extends Command {
242
244
  // don't include subcommands on the main page
243
245
  return !cmd.name().includes(':');
244
246
  }
245
- return cmd.name().startsWith(`${command.name()}:`);
247
+ return cmd.name().startsWith(`${command.name()}:`) || cmd.parent === command;
246
248
  })
247
249
  .sort((a, b) => a.name().localeCompare(b.name())) || []);
248
250
  };
249
- help.longestSubcommandTermLength = (command) => getCommands(command).reduce((max, cmd) => Math.max(max, cmd.name().length), 0);
251
+ /** nested subcommands are shown by their full path, e.g. `database migrations apply` */
252
+ const getCommandTerm = (cmd) => cmd.parent && cmd.parent.name() !== 'netlify' ? `${getCommandTerm(cmd.parent)} ${cmd.name()}` : cmd.name();
253
+ help.longestSubcommandTermLength = (command) => getCommands(command).reduce((max, cmd) => Math.max(max, getCommandTerm(cmd).length), 0);
250
254
  /** override the longestOptionTermLength to react on hide options flag */
251
255
  help.longestOptionTermLength = (command, helper) => (!command.noBaseOptions &&
252
256
  helper.visibleOptions(command).reduce((max, option) => Math.max(max, helper.optionTerm(option).length), 0)) ||
@@ -313,7 +317,7 @@ export default class BaseCommand extends Command {
313
317
  '',
314
318
  ];
315
319
  }
316
- const commandList = getCommands(command).map((cmd) => formatItem(cmd.name(), helper.subcommandDescription(cmd).split('\n')[0], true));
320
+ const commandList = getCommands(command).map((cmd) => formatItem(getCommandTerm(cmd), helper.subcommandDescription(cmd).split('\n')[0], true));
317
321
  if (commandList.length !== 0) {
318
322
  output = [...output, chalk.bold('COMMANDS'), formatHelpList(commandList), ''];
319
323
  }
@@ -72,7 +72,7 @@ const getDeployFolder = async ({ command, config, options, site, siteData, }) =>
72
72
  ? resolve(command.jsWorkspaceRoot || site.root, options.dir)
73
73
  : resolve(command.workingDir, options.dir);
74
74
  }
75
- else if (config?.build?.publish) {
75
+ else if (config.build.publish) {
76
76
  deployFolder = resolve(site.root, config.build.publish);
77
77
  }
78
78
  else if (siteData?.build_settings?.dir) {
@@ -252,10 +252,7 @@ const generateDeployCommand = (options, availableTeams, command) => {
252
252
  return parts.join(' ');
253
253
  };
254
254
  const prepareProductionDeploy = async ({ api, siteData, options, command, }) => {
255
- if (typeof siteData.published_deploy === 'object' &&
256
- // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- FIXME: a non-object `published_deploy` must not count as locked
257
- siteData.published_deploy !== null &&
258
- siteData.published_deploy.locked) {
255
+ if (siteData.published_deploy?.locked) {
259
256
  log(`\n${NETLIFYDEVERR} Deployments are "locked" for production context of this project\n`);
260
257
  const overrideCommand = generateDeployCommand({ ...options, prodIfUnlocked: true, prod: false }, [], command);
261
258
  if (!isInteractive()) {
@@ -275,6 +272,19 @@ const prepareProductionDeploy = async ({ api, siteData, options, command, }) =>
275
272
  log(`\n${NETLIFYDEVLOG} "Auto publishing" has been enabled for production context\n`);
276
273
  }
277
274
  };
275
+ const createDeploy = async ({ alias, api, command, deployToProduction, options, siteData, siteId, }) => {
276
+ if (deployToProduction) {
277
+ await prepareProductionDeploy({ siteData, api, options, command });
278
+ }
279
+ const draft = options.draft || (!deployToProduction && !alias);
280
+ const createDeployBody = {
281
+ draft,
282
+ branch: alias,
283
+ include_upload_url: options.uploadSourceZip,
284
+ ...getDeploySourceFields(),
285
+ };
286
+ return (await api.createSiteDeploy({ siteId, title: options.message, body: createDeployBody }));
287
+ };
278
288
  const hasErrorMessage = (actual, expected) => {
279
289
  if (typeof actual === 'string') {
280
290
  return actual.includes(expected);
@@ -372,7 +382,7 @@ const uploadDeployBlobs = async ({ cachedConfig, deployId, options, packagePath,
372
382
  phase: 'stop',
373
383
  });
374
384
  };
375
- const runDeploy = async ({ alias, api, command, config, deployFolder, deployTimeout, deployToProduction, functionsConfig, functionsFolder, options, packagePath, silent, site, siteData, siteId, skipFunctionsCache, title, deployId: existingDeployId, }) => {
385
+ const runDeploy = async ({ alias, api, command, config, deployFolder, deployTimeout, deployToProduction, functionsConfig, functionsFolder, options, packagePath, silent, site, siteData, siteId, skipFunctionsCache, deployId: existingDeployId, }) => {
376
386
  let results;
377
387
  let deployId = existingDeployId;
378
388
  let uploadSourceZipResult;
@@ -380,21 +390,15 @@ const runDeploy = async ({ alias, api, command, config, deployFolder, deployTime
380
390
  // We won't have a deploy ID if we run the command with `--no-build`.
381
391
  // In this case, we must create the deploy.
382
392
  if (!deployId) {
383
- if (deployToProduction) {
384
- await prepareProductionDeploy({ siteData, api, options, command });
385
- }
386
- const draft = options.draft || (!deployToProduction && !alias);
387
- const createDeployBody = {
388
- draft,
389
- branch: alias,
390
- include_upload_url: options.uploadSourceZip,
391
- ...getDeploySourceFields(),
392
- };
393
- const createDeployResponse = (await api.createSiteDeploy({
393
+ const createDeployResponse = await createDeploy({
394
+ alias,
395
+ api,
396
+ command,
397
+ deployToProduction,
398
+ options,
399
+ siteData,
394
400
  siteId,
395
- title,
396
- body: createDeployBody,
397
- }));
401
+ });
398
402
  deployId = createDeployResponse.id;
399
403
  if (options.uploadSourceZip &&
400
404
  createDeployResponse.source_zip_upload_url &&
@@ -458,7 +462,7 @@ const runDeploy = async ({ alias, api, command, config, deployFolder, deployTime
458
462
  workingDir: command.workingDir,
459
463
  manifestPath: manifestPath ?? undefined,
460
464
  packagePath,
461
- serverEnabled: Boolean(siteData?.feature_flags?.netlify_build_server_standalone),
465
+ serverEnabled: Boolean(siteData.feature_flags?.netlify_build_server_standalone),
462
466
  serverManifestPath: serverManifestPath ?? undefined,
463
467
  skipFunctionsCache,
464
468
  siteRoot: site.root,
@@ -708,7 +712,6 @@ const prepAndRunDeploy = async ({ api, command, config, deployToProduction, opti
708
712
  siteData,
709
713
  siteId,
710
714
  skipFunctionsCache: options.skipFunctionsCache,
711
- title: options.message,
712
715
  deployId,
713
716
  });
714
717
  return results;
@@ -831,7 +834,7 @@ const anonymousDeploy = async (options, command) => {
831
834
  }
832
835
  };
833
836
  const checkForFunctions = async () => {
834
- const functionsFolder = getFunctionsFolder({ config, options, site, siteData: {}, workingDir });
837
+ const functionsFolder = getFunctionsFolder({ config, options, site, workingDir });
835
838
  const internalFunctionsDir = await getInternalFunctionsDir({ base: site.root });
836
839
  const frameworksFunctionsDir = command.netlify.frameworksAPIPaths.functions.path;
837
840
  const hasFunctions = (await dirHasFiles(functionsFolder)) ||
@@ -861,10 +864,9 @@ const anonymousDeploy = async (options, command) => {
861
864
  }
862
865
  const deployFolder = await getDeployFolder({
863
866
  command,
864
- config: command.netlify.config,
867
+ config,
865
868
  options,
866
869
  site,
867
- siteData: {},
868
870
  });
869
871
  await validateDeployFolder(deployFolder);
870
872
  const edgeFunctionsDistPath = await getEdgeFunctionsDistPathIfExists(workingDir);
@@ -983,22 +985,8 @@ export const deploy = async (options, command) => {
983
985
  const deployToProduction = !options.draft && (options.prod || (options.prodIfUnlocked && !(siteData.published_deploy?.locked ?? false)));
984
986
  let results = {};
985
987
  if (options.build) {
986
- if (deployToProduction) {
987
- await prepareProductionDeploy({ siteData, api, options, command });
988
- }
989
- const draft = options.draft || (!deployToProduction && !alias);
990
- const createDeployBody = {
991
- draft,
992
- branch: alias,
993
- include_upload_url: options.uploadSourceZip,
994
- ...getDeploySourceFields(),
995
- };
996
- const deployMetadata = (await api.createSiteDeploy({
997
- siteId,
998
- title: options.message,
999
- body: createDeployBody,
1000
- }));
1001
- const deployId = deployMetadata.id || '';
988
+ const deployMetadata = await createDeploy({ alias, api, command, deployToProduction, options, siteData, siteId });
989
+ const deployId = deployMetadata.id;
1002
990
  const skewProtectionToken = deployMetadata.skew_protection_token;
1003
991
  let sourceZipFileName;
1004
992
  if (options.uploadSourceZip &&
@@ -5,7 +5,7 @@ import { chalk, logAndThrowError, log, logJson, warn } from '../../utils/command
5
5
  import getRepoData from '../../utils/get-repo-data.js';
6
6
  import { configureRepo } from '../../utils/init/config.js';
7
7
  import { isInteractive } from '../../utils/scripted-commands.js';
8
- import { resolveTeamForNonInteractive } from '../../utils/team.js';
8
+ import { requireTeams, resolveTeamForNonInteractive } from '../../utils/team.js';
9
9
  import { track } from '../../utils/telemetry/index.js';
10
10
  import { MAX_SITE_NAME_LENGTH } from '../../utils/validation.js';
11
11
  import { link } from '../link/link.js';
@@ -29,9 +29,10 @@ export const sitesCreate = async (options, command) => {
29
29
  log(`Using team: ${team.name}`);
30
30
  }
31
31
  else {
32
+ const teams = requireTeams(accounts);
32
33
  accountSlug = await select({
33
34
  message: 'Team:',
34
- choices: accounts.map((account) => ({
35
+ choices: teams.map((account) => ({
35
36
  value: account.slug,
36
37
  name: account.name,
37
38
  })),
@@ -1,4 +1,5 @@
1
1
  import type { MinimalAccount } from './types.js';
2
+ export declare const requireTeams: (accounts: MinimalAccount[] | undefined) => MinimalAccount[];
2
3
  export declare const resolveTeam: (accounts: MinimalAccount[]) => MinimalAccount | undefined;
3
4
  export declare const resolveTeamForNonInteractive: (accounts: MinimalAccount[], commandExample: string) => MinimalAccount;
4
5
  //# sourceMappingURL=team.d.ts.map
@@ -1,4 +1,11 @@
1
1
  import { logAndThrowError } from './command-helpers.js';
2
+ export const requireTeams = (accounts) => {
3
+ const teams = accounts ?? [];
4
+ if (teams.length === 0) {
5
+ return logAndThrowError('No teams available. Please ensure you have access to at least one team.');
6
+ }
7
+ return teams;
8
+ };
2
9
  export const resolveTeam = (accounts) => {
3
10
  if (accounts.length === 1) {
4
11
  return accounts[0];
@@ -6,14 +13,12 @@ export const resolveTeam = (accounts) => {
6
13
  return accounts.find((acc) => acc.default);
7
14
  };
8
15
  export const resolveTeamForNonInteractive = (accounts, commandExample) => {
9
- if (accounts.length === 0) {
10
- return logAndThrowError('No teams available. Please ensure you have access to at least one team.');
11
- }
12
- const team = resolveTeam(accounts);
16
+ const teams = requireTeams(accounts);
17
+ const team = resolveTeam(teams);
13
18
  if (team) {
14
19
  return team;
15
20
  }
16
- const availableTeams = accounts.map((t) => t.slug).join(', ');
21
+ const availableTeams = teams.map((t) => t.slug).join(', ');
17
22
  return logAndThrowError(`Multiple teams available. Please specify which team to use.\n` +
18
23
  `Available teams: ${availableTeams}\n\n` +
19
24
  `Example: ${commandExample}\n\n` +
@@ -3,9 +3,8 @@ import { dirname, join } from 'path';
3
3
  import process, { version as nodejsVersion } from 'process';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { getGlobalConfigStore } from '@netlify/dev-utils';
6
- import { isCI } from 'ci-info';
7
6
  import execa from '../execa.js';
8
- import { cliVersion } from './utils.js';
7
+ import { cliVersion, isTelemetryDisabledByEnv } from './utils.js';
9
8
  const dirPath = dirname(fileURLToPath(import.meta.url));
10
9
  let currentCommand;
11
10
  export const setCommandForErrorReporting = (command) => {
@@ -21,7 +20,7 @@ export const setCommandForErrorReporting = (command) => {
21
20
  */
22
21
  // @ts-expect-error TS(7006) FIXME: Parameter 'error' implicitly has an 'any' type.
23
22
  export const reportError = async function (error, config = {}) {
24
- if (isCI) {
23
+ if (isTelemetryDisabledByEnv()) {
25
24
  return;
26
25
  }
27
26
  // convert a NotifiableError to an error class
@@ -2,10 +2,10 @@ import { dirname, join } from 'path';
2
2
  import process, { version as nodejsVersion } from 'process';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { getGlobalConfigStore } from '@netlify/dev-utils';
5
- import { isCI } from 'ci-info';
5
+ import { id as ciVendor, isPR } from 'ci-info';
6
6
  import { getDrivingAgent } from '../agent-detection.js';
7
7
  import execa from '../execa.js';
8
- import { isTelemetryDisabled, cliVersion } from './utils.js';
8
+ import { isTelemetryDisabled, isTelemetryDisabledByEnv, cliVersion } from './utils.js';
9
9
  import isValidEventName from './validation.js';
10
10
  const dirPath = dirname(fileURLToPath(import.meta.url));
11
11
  function send(type, payload) {
@@ -49,11 +49,15 @@ const getAgentProperties = () => {
49
49
  agent_other_value: agent?.otherValue,
50
50
  };
51
51
  };
52
+ const getCIProperties = () => ({
53
+ ci_vendor: ciVendor ?? undefined,
54
+ ci_is_pr: isPR ?? undefined,
55
+ });
52
56
  /**
53
57
  * Tracks a custom event with the provided payload
54
58
  */
55
59
  export async function track(eventName, payload = {}) {
56
- if (isCI) {
60
+ if (isTelemetryDisabledByEnv()) {
57
61
  return;
58
62
  }
59
63
  const globalConfig = await getGlobalConfigStore();
@@ -78,12 +82,12 @@ export async function track(eventName, payload = {}) {
78
82
  anonymousId: cliId,
79
83
  duration,
80
84
  status,
81
- properties: { ...properties, nodejsVersion, cliVersion, ...getAgentProperties() },
85
+ properties: { ...properties, nodejsVersion, cliVersion, ...getAgentProperties(), ...getCIProperties() },
82
86
  };
83
87
  return send('track', defaultData);
84
88
  }
85
89
  export async function identify(payload) {
86
- if (isCI) {
90
+ if (isTelemetryDisabledByEnv()) {
87
91
  return;
88
92
  }
89
93
  const globalConfig = await getGlobalConfigStore();
@@ -1,3 +1,4 @@
1
1
  export declare const cliVersion: string;
2
2
  export declare const isTelemetryDisabled: (config: any) => any;
3
+ export declare const isTelemetryDisabledByEnv: () => boolean;
3
4
  //# sourceMappingURL=utils.d.ts.map
@@ -1,7 +1,11 @@
1
+ import process from 'process';
1
2
  import getCLIPackageJson from '../get-cli-package-json.js';
2
3
  export const { version: cliVersion } = await getCLIPackageJson();
3
4
  // @ts-expect-error TS(7006) FIXME: Parameter 'config' implicitly has an 'any' type.
4
5
  export const isTelemetryDisabled = function (config) {
5
6
  return config.get('telemetryDisabled');
6
7
  };
8
+ const isEnabledFlag = (value) => value !== undefined && !['', '0', 'false'].includes(value);
9
+ // An env var opt-out matters in CI, where the global config written by `--telemetry-disable` rarely persists.
10
+ export const isTelemetryDisabledByEnv = () => isEnabledFlag(process.env.NETLIFY_TELEMETRY_DISABLED) || isEnabledFlag(process.env.DO_NOT_TRACK);
7
11
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "27.12.1",
3
+ "version": "27.13.0",
4
4
  "description": "Netlify command line tool",
5
5
  "keywords": [
6
6
  "api",
@@ -182,7 +182,7 @@
182
182
  "@types/write-file-atomic": "^4.0.3",
183
183
  "@types/ws": "^8.18.1",
184
184
  "@types/yauzl": "^2.10.3",
185
- "@vitest/coverage-v8": "^4.1.11",
185
+ "@vitest/coverage-v8": "^5.0.0",
186
186
  "@vitest/eslint-plugin": "1.3.13",
187
187
  "c8": "^10.1.3",
188
188
  "cheerio": "^1.1.2",
@@ -210,7 +210,7 @@
210
210
  "typescript-eslint": "~8.65.0",
211
211
  "typescript-native": "npm:typescript@~7.0.2",
212
212
  "verdaccio": "^6.3.2",
213
- "vitest": "^4.1.11"
213
+ "vitest": "^5.0.0"
214
214
  },
215
215
  "engines": {
216
216
  "node": ">=22.13.0"