netlify-cli 27.11.2 → 27.12.1

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.
Files changed (62) hide show
  1. package/dist/commands/base-command.js +2 -2
  2. package/dist/commands/deploy/deploy.js +28 -59
  3. package/dist/commands/dev/dev.js +1 -2
  4. package/dist/commands/env/env-clone.js +1 -1
  5. package/dist/commands/env/env-set.js +1 -1
  6. package/dist/commands/env/env-unset.js +1 -1
  7. package/dist/commands/functions/functions-create.js +3 -93
  8. package/dist/commands/init/index.js +6 -1
  9. package/dist/commands/init/init.d.ts +3 -1
  10. package/dist/commands/init/init.js +22 -2
  11. package/dist/commands/serve/serve.js +1 -2
  12. package/dist/commands/sites/sites-list.js +1 -1
  13. package/dist/commands/unlink/unlink.js +1 -1
  14. package/dist/lib/edge-functions/registry.js +0 -1
  15. package/dist/lib/exec-fetcher.js +1 -1
  16. package/dist/lib/functions/form-submissions-handler.js +2 -1
  17. package/dist/lib/spinner.d.ts +2 -2
  18. package/dist/lib/spinner.js +1 -1
  19. package/dist/recipes/ai-context/context.js +2 -2
  20. package/dist/utils/deploy/deploy-site.d.ts +32 -12
  21. package/dist/utils/deploy/deploy-site.js +18 -40
  22. package/dist/utils/deploy/drop-api.d.ts +7 -1
  23. package/dist/utils/deploy/drop-api.js +3 -4
  24. package/dist/utils/deploy/file.d.ts +4 -2
  25. package/dist/utils/deploy/hash-config.d.ts +5 -6
  26. package/dist/utils/deploy/hash-config.js +1 -4
  27. package/dist/utils/deploy/hash-files.d.ts +9 -8
  28. package/dist/utils/deploy/hash-fns.d.ts +45 -10
  29. package/dist/utils/deploy/hasher-segments.d.ts +10 -6
  30. package/dist/utils/deploy/hasher-segments.js +0 -1
  31. package/dist/utils/deploy/process-files.d.ts +1 -1
  32. package/dist/utils/deploy/process-files.js +1 -0
  33. package/dist/utils/deploy/types.d.ts +17 -0
  34. package/dist/utils/deploy/types.js +2 -0
  35. package/dist/utils/deploy/upload-files.d.ts +8 -3
  36. package/dist/utils/deploy/upload-files.js +4 -3
  37. package/dist/utils/deploy/upload-source-zip.js +4 -7
  38. package/dist/utils/deploy/util.d.ts +6 -3
  39. package/dist/utils/deploy/util.js +4 -16
  40. package/dist/utils/dev.d.ts +0 -1
  41. package/dist/utils/dev.js +1 -27
  42. package/dist/utils/env/index.js +1 -3
  43. package/dist/utils/get-repo-data.js +1 -1
  44. package/dist/utils/init/agent-skills.d.ts +96 -0
  45. package/dist/utils/init/agent-skills.js +653 -0
  46. package/dist/utils/parse-raw-flags.js +1 -1
  47. package/dist/utils/proxy-server.d.ts +1 -2
  48. package/dist/utils/proxy-server.js +1 -2
  49. package/dist/utils/proxy.d.ts +1 -1
  50. package/dist/utils/proxy.js +7 -35
  51. package/dist/utils/types.d.ts +1 -1
  52. package/functions-templates/javascript/scheduled-function/package.json +1 -1
  53. package/functions-templates/typescript/database/package-lock.json +8 -8
  54. package/functions-templates/typescript/database/package.json +1 -1
  55. package/functions-templates/typescript/database-drizzle/package-lock.json +8 -8
  56. package/functions-templates/typescript/database-drizzle/package.json +1 -1
  57. package/functions-templates/typescript/hello-world/package-lock.json +15 -17
  58. package/functions-templates/typescript/hello-world/package.json +1 -1
  59. package/functions-templates/typescript/scheduled-function/package.json +1 -1
  60. package/package.json +10 -11
  61. package/dist/utils/addons/prepare.d.ts +0 -13
  62. package/dist/utils/addons/prepare.js +0 -26
@@ -248,7 +248,7 @@ export default class BaseCommand extends Command {
248
248
  };
249
249
  help.longestSubcommandTermLength = (command) => getCommands(command).reduce((max, cmd) => Math.max(max, cmd.name().length), 0);
250
250
  /** override the longestOptionTermLength to react on hide options flag */
251
- help.longestOptionTermLength = (command, helper) => (command.noBaseOptions === false &&
251
+ help.longestOptionTermLength = (command, helper) => (!command.noBaseOptions &&
252
252
  helper.visibleOptions(command).reduce((max, option) => Math.max(max, helper.optionTerm(option).length), 0)) ||
253
253
  0;
254
254
  help.formatHelp = (command, helper) => {
@@ -284,7 +284,7 @@ export default class BaseCommand extends Command {
284
284
  if (argumentList.length !== 0) {
285
285
  output = [...output, chalk.bold('ARGUMENTS'), formatHelpList(argumentList), ''];
286
286
  }
287
- if (command.#noBaseOptions === false) {
287
+ if (!command.#noBaseOptions) {
288
288
  // Options
289
289
  const optionList = helper
290
290
  .visibleOptions(command)
@@ -166,13 +166,6 @@ const validateFolders = async ({ deployFolder, functionsFolder, }) => {
166
166
  const functionsFolderStat = await validateFunctionsFolder(functionsFolder);
167
167
  return { deployFolderStat, functionsFolderStat };
168
168
  };
169
- /**
170
- * @param {object} config
171
- * @param {string} config.deployFolder
172
- * @param {*} config.site
173
- * @returns
174
- */
175
- // @ts-expect-error TS(7031) FIXME: Binding element 'deployFolder' implicitly has an '... Remove this comment to see the full error message
176
169
  const getDeployFilesFilter = ({ deployFolder, site }) => {
177
170
  // site.root === deployFolder can happen when users run `netlify deploy --dir .`
178
171
  // in that specific case we don't want to publish the repo node_modules
@@ -258,9 +251,9 @@ const generateDeployCommand = (options, availableTeams, command) => {
258
251
  }
259
252
  return parts.join(' ');
260
253
  };
261
- // @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message
262
- const prepareProductionDeploy = async ({ api, siteData, options, command }) => {
254
+ const prepareProductionDeploy = async ({ api, siteData, options, command, }) => {
263
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
264
257
  siteData.published_deploy !== null &&
265
258
  siteData.published_deploy.locked) {
266
259
  log(`\n${NETLIFYDEVERR} Deployments are "locked" for production context of this project\n`);
@@ -337,6 +330,7 @@ const deployProgressCb = function () {
337
330
  return;
338
331
  case 'stop':
339
332
  default: {
333
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- FIXME: a `stop` event is assumed to follow a `start` event of the same type
340
334
  spinnersByType[event.type].success(event.msg);
341
335
  delete spinnersByType[event.type];
342
336
  }
@@ -357,7 +351,7 @@ const uploadDeployBlobs = async ({ cachedConfig, deployId, options, packagePath,
357
351
  // We log our own progress so we don't want this as well. Plus, this logs much of the same
358
352
  // information as the build that (likely) came before this as part of the deploy build.
359
353
  quiet: options.debug ?? true,
360
- // @ts-expect-error(serhalp) -- Untyped in `@netlify/build`
354
+ // @ts-expect-error FIXME(@netlify/build): the `cachedConfig` flag is typed `Record<string, unknown>`, rejecting `CachedConfig`
361
355
  cachedConfig,
362
356
  packagePath,
363
357
  deployId,
@@ -378,37 +372,7 @@ const uploadDeployBlobs = async ({ cachedConfig, deployId, options, packagePath,
378
372
  phase: 'stop',
379
373
  });
380
374
  };
381
- const runDeploy = async ({
382
- // @ts-expect-error TS(7031) FIXME: Binding element 'alias' implicitly has an 'any' ty... Remove this comment to see the full error message
383
- alias,
384
- // @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message
385
- api, command,
386
- // @ts-expect-error TS(7031) FIXME: Binding element 'config' implicitly has an 'any' t... Remove this comment to see the full error message
387
- config,
388
- // @ts-expect-error TS(7031) FIXME: Binding element 'deployFolder' implicitly has an '... Remove this comment to see the full error message
389
- deployFolder,
390
- // @ts-expect-error TS(7031) FIXME: Binding element 'deployTimeout' implicitly has an ... Remove this comment to see the full error message
391
- deployTimeout,
392
- // @ts-expect-error TS(7031) FIXME: Binding element 'deployToProduction' implicitly ha... Remove this comment to see the full error message
393
- deployToProduction,
394
- // @ts-expect-error TS(7031) FIXME: Binding element 'functionsConfig' implicitly has a... Remove this comment to see the full error message
395
- functionsConfig, functionsFolder,
396
- // @ts-expect-error TS(7031) FIXME: Binding element 'options' implicitly has an 'a... Remove this comment to see the full error message
397
- options,
398
- // @ts-expect-error TS(7031) FIXME: Binding element 'packagePath' implicitly has an 'a... Remove this comment to see the full error message
399
- packagePath,
400
- // @ts-expect-error TS(7031) FIXME: Binding element 'silent' implicitly has an 'any' t... Remove this comment to see the full error message
401
- silent,
402
- // @ts-expect-error TS(7031) FIXME: Binding element 'site' implicitly has an 'any' typ... Remove this comment to see the full error message
403
- site,
404
- // @ts-expect-error TS(7031) FIXME: Binding element 'siteData' implicitly has an 'any'... Remove this comment to see the full error message
405
- siteData,
406
- // @ts-expect-error TS(7031) FIXME: Binding element 'siteId' implicitly has an 'any' t... Remove this comment to see the full error message
407
- siteId,
408
- // @ts-expect-error TS(7031) FIXME: Binding element 'skipFunctionsCache' implicitly ha... Remove this comment to see the full error message
409
- skipFunctionsCache,
410
- // @ts-expect-error TS(7031) FIXME: Binding element 'title' implicitly has an 'any' ty... Remove this comment to see the full error message
411
- title, deployId: existingDeployId, }) => {
375
+ const runDeploy = async ({ alias, api, command, config, deployFolder, deployTimeout, deployToProduction, functionsConfig, functionsFolder, options, packagePath, silent, site, siteData, siteId, skipFunctionsCache, title, deployId: existingDeployId, }) => {
412
376
  let results;
413
377
  let deployId = existingDeployId;
414
378
  let uploadSourceZipResult;
@@ -426,7 +390,11 @@ title, deployId: existingDeployId, }) => {
426
390
  include_upload_url: options.uploadSourceZip,
427
391
  ...getDeploySourceFields(),
428
392
  };
429
- const createDeployResponse = await api.createSiteDeploy({ siteId, title, body: createDeployBody });
393
+ const createDeployResponse = (await api.createSiteDeploy({
394
+ siteId,
395
+ title,
396
+ body: createDeployBody,
397
+ }));
430
398
  deployId = createDeployResponse.id;
431
399
  if (options.uploadSourceZip &&
432
400
  createDeployResponse.source_zip_upload_url &&
@@ -449,17 +417,21 @@ title, deployId: existingDeployId, }) => {
449
417
  command.netlify.frameworksAPIPaths.functions.path,
450
418
  functionsFolder,
451
419
  ].filter((folder) => Boolean(folder));
452
- const manifestPath = skipFunctionsCache ? null : await getFunctionsManifestPath({ base: site.root, packagePath });
420
+ const manifestPath = skipFunctionsCache
421
+ ? undefined
422
+ : await getFunctionsManifestPath({ base: site.root, packagePath });
453
423
  const serverManifestPath = await getServerManifestPath({ base: site.root, packagePath });
454
424
  const redirectsPath = `${deployFolder}/_redirects`;
455
425
  const headersPath = `${deployFolder}/_headers`;
456
426
  const { redirects } = await parseAllRedirects({
427
+ // @ts-expect-error FIXME(@netlify/redirect-parser): `configRedirects` is typed `string[]` but takes redirect objects
457
428
  configRedirects: config.redirects,
458
429
  redirectsFiles: [redirectsPath],
459
430
  minimal: true,
460
431
  });
461
432
  config.redirects = redirects;
462
433
  const { headers } = await parseAllHeaders({
434
+ // @ts-expect-error FIXME(@netlify/headers-parser): `MinimalHeader` rejects the array values `NetlifyConfig['headers']` allows
463
435
  configHeaders: config.headers,
464
436
  headersFiles: [headersPath],
465
437
  minimal: true,
@@ -474,7 +446,6 @@ title, deployId: existingDeployId, }) => {
474
446
  packagePath: command.workspacePackage,
475
447
  });
476
448
  results = await deploySite(command, api, siteId, deployFolder, {
477
- // @ts-expect-error FIXME
478
449
  config,
479
450
  fnDir: functionDirectories,
480
451
  functionsConfig,
@@ -485,7 +456,7 @@ title, deployId: existingDeployId, }) => {
485
456
  deployId,
486
457
  filter: getDeployFilesFilter({ site, deployFolder }),
487
458
  workingDir: command.workingDir,
488
- manifestPath,
459
+ manifestPath: manifestPath ?? undefined,
489
460
  packagePath,
490
461
  serverEnabled: Boolean(siteData?.feature_flags?.netlify_build_server_standalone),
491
462
  serverManifestPath: serverManifestPath ?? undefined,
@@ -581,10 +552,8 @@ const bundleEdgeFunctions = async (options, command) => {
581
552
  // We log our own progress so we don't want this as well. Plus, this logs much of the same
582
553
  // information as the build that (likely) came before this as part of the deploy build.
583
554
  quiet: options.debug ?? true,
584
- // (cachedConfig type error hides this one, but it still is valid) @ts-expect-error FIXME(serhalp): This is missing from the `runCoreSteps` type in @netlify/build
585
555
  edgeFunctionsBootstrapURL: await getBootstrapURL(),
586
- // @ts-expect-error 'CachedConfig' is not assignable to type 'Record<string, unknown>'.
587
- // Index signature for type 'string' is missing in type 'CachedConfig'.
556
+ // @ts-expect-error FIXME(@netlify/build): `cachedConfig` is typed `Record<string, unknown>` and `edgeFunctionsBootstrapURL` is missing from the flags
588
557
  cachedConfig: command.netlify.cachedConfig,
589
558
  });
590
559
  if (!success) {
@@ -722,7 +691,6 @@ const prepAndRunDeploy = async ({ api, command, config, deployToProduction, opti
722
691
  siteEnv,
723
692
  });
724
693
  const results = await runDeploy({
725
- // @ts-expect-error FIXME
726
694
  alias,
727
695
  api,
728
696
  command,
@@ -735,7 +703,7 @@ const prepAndRunDeploy = async ({ api, command, config, deployToProduction, opti
735
703
  functionsFolder: functionsFolderStat && functionsFolder,
736
704
  options,
737
705
  packagePath: command.workspacePackage,
738
- silent: options.json || options.silent,
706
+ silent: options.json || Boolean(options.silent),
739
707
  site,
740
708
  siteData,
741
709
  siteId,
@@ -768,10 +736,11 @@ const createSiteWithFlags = async (options, command, site) => {
768
736
  throw new Error('Team must be specified to create a site');
769
737
  }
770
738
  try {
771
- const siteData = await api.createSiteInTeam({
739
+ // FIXME(@netlify/api): the `site` response type makes every field optional, unlike `SiteInfo`
740
+ const siteData = (await api.createSiteInTeam({
772
741
  accountSlug: options.team,
773
742
  body,
774
- });
743
+ }));
775
744
  site.id = siteData.id;
776
745
  return siteData;
777
746
  }
@@ -781,10 +750,11 @@ const createSiteWithFlags = async (options, command, site) => {
781
750
  const suffixedName = `${siteName.trim()}-${suffix}`;
782
751
  log(`Site name "${siteName}" is taken. Retrying with "${suffixedName}"...`);
783
752
  try {
784
- const siteData = await api.createSiteInTeam({
753
+ // FIXME(@netlify/api): the `site` response type makes every field optional, unlike `SiteInfo`
754
+ const siteData = (await api.createSiteInTeam({
785
755
  accountSlug: options.team,
786
756
  body: { name: suffixedName },
787
- });
757
+ }));
788
758
  site.id = siteData.id;
789
759
  return siteData;
790
760
  }
@@ -901,7 +871,7 @@ const anonymousDeploy = async (options, command) => {
901
871
  const filter = getDeployFilesFilter({ site, deployFolder });
902
872
  const { files, filesShaMap } = await hashFiles({
903
873
  concurrentHash: DEFAULT_CONCURRENT_HASH,
904
- directories: [deployFolder, edgeFunctionsDistPath].filter(Boolean),
874
+ directories: [deployFolder, edgeFunctionsDistPath].filter((dir) => Boolean(dir)),
905
875
  filter,
906
876
  normalizer: deployFileNormalizer.bind(null, workingDir),
907
877
  statusCb: options.json ? () => { } : deployProgressCb(),
@@ -941,7 +911,7 @@ const anonymousDeploy = async (options, command) => {
941
911
  }
942
912
  const deploy = await waitForDropDeploy(dropApiOptions, deployInfo.id, deployInfo.deploy_id, options.timeout ? options.timeout * 1000 : DEFAULT_DEPLOY_TIMEOUT);
943
913
  site.id = deployInfo.id;
944
- const siteUrl = (deploy.ssl_url || deploy.url || `https://${deployInfo.subdomain}.netlify.app`);
914
+ const siteUrl = deploy.ssl_url || deploy.url || `https://${deployInfo.subdomain}.netlify.app`;
945
915
  const isPasswordProtected = !options.createdVia || options.createdVia === 'drop';
946
916
  const claimUrl = `https://app.netlify.com/drop/${deployInfo.subdomain}#drop_token=${dropToken}`;
947
917
  if (options.json) {
@@ -1023,7 +993,6 @@ export const deploy = async (options, command) => {
1023
993
  include_upload_url: options.uploadSourceZip,
1024
994
  ...getDeploySourceFields(),
1025
995
  };
1026
- // TODO: Type this properly in `@netlify/api`.
1027
996
  const deployMetadata = (await api.createSiteDeploy({
1028
997
  siteId,
1029
998
  title: options.message,
@@ -1058,7 +1027,7 @@ export const deploy = async (options, command) => {
1058
1027
  options,
1059
1028
  workingDir,
1060
1029
  api,
1061
- site,
1030
+ site: site,
1062
1031
  config: netlifyConfig,
1063
1032
  siteData,
1064
1033
  siteId,
@@ -1087,7 +1056,7 @@ export const deploy = async (options, command) => {
1087
1056
  options,
1088
1057
  workingDir,
1089
1058
  api,
1090
- site,
1059
+ site: site,
1091
1060
  config: command.netlify.config,
1092
1061
  siteData,
1093
1062
  siteId,
@@ -91,7 +91,7 @@ export const dev = async (options, command) => {
91
91
  log(`${NETLIFYDEVLOG} Injecting environment variable values for ${chalk.yellow('all scopes')}`);
92
92
  }
93
93
  env = await getDotEnvVariables({ devConfig, env, site });
94
- const { accountId, addonsUrls, capabilities, siteUrl, timeouts } = await getSiteInformation({
94
+ const { accountId, capabilities, siteUrl, timeouts } = await getSiteInformation({
95
95
  // inherited from base command --offline
96
96
  offline: options.offline,
97
97
  api,
@@ -217,7 +217,6 @@ export const dev = async (options, command) => {
217
217
  };
218
218
  const inspectSettings = generateInspectSettings(options.edgeInspect, options.edgeInspectBrk);
219
219
  await startProxyServer({
220
- addonsUrls,
221
220
  aiGatewayContext,
222
221
  api,
223
222
  blobsContext,
@@ -30,7 +30,7 @@ const cloneEnvVars = async ({ api, force, siteFrom, siteTo }) => {
30
30
  const siteId = siteTo.id;
31
31
  // @ts-expect-error TS(7031) FIXME: Binding element 'key' implicitly has an 'any' type... Remove this comment to see the full error message
32
32
  const envVarsToDelete = envelopeTo.filter(({ key }) => keysFrom.includes(key));
33
- if (envVarsToDelete.length !== 0 && Boolean(force) === false) {
33
+ if (envVarsToDelete.length !== 0 && !force) {
34
34
  await promptEnvCloneOverwrite(siteTo.id, envVarsToDelete);
35
35
  }
36
36
  // delete marked env vars in parallel
@@ -34,7 +34,7 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo
34
34
  // @ts-expect-error TS(7006) FIXME: Parameter 'envVar' implicitly has an 'any' type.
35
35
  const existing = envelopeVariables.find((envVar) => envVar.key === key);
36
36
  // Checks if --force is passed and if it is an existing variaible, then we need to prompt the user
37
- if (Boolean(force) === false && existing) {
37
+ if (!force && existing) {
38
38
  await promptOverwriteEnvVariable(key);
39
39
  }
40
40
  const params = { accountId, siteId, key };
@@ -21,7 +21,7 @@ const unsetInEnvelope = async ({ api, context, force, key, siteInfo }) => {
21
21
  // if not, no need to call delete; return early
22
22
  return env;
23
23
  }
24
- if (Boolean(force) === false) {
24
+ if (!force) {
25
25
  await promptOverwriteEnvVariable(key);
26
26
  }
27
27
  const params = { accountId, siteId, key };
@@ -5,13 +5,12 @@ import { createRequire } from 'module';
5
5
  import path, { dirname, join, relative } from 'path';
6
6
  import process from 'process';
7
7
  import { fileURLToPath, pathToFileURL } from 'url';
8
- import { confirm, input, search, select, Separator } from '@inquirer/prompts';
8
+ import { input, search, select, Separator } from '@inquirer/prompts';
9
9
  import { findUp } from 'find-up';
10
10
  import fuzzy from 'fuzzy';
11
11
  import fetch from 'node-fetch';
12
12
  import { createSpinner } from 'nanospinner';
13
13
  import { fileExistsAsync } from '../../lib/fs.js';
14
- import { getAddons, getCurrentAddon, getSiteData } from '../../utils/addons/prepare.js';
15
14
  import { NETLIFYDEVERR, NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, logAndThrowError, log, } from '../../utils/command-helpers.js';
16
15
  import { copyTemplateDir } from '../../utils/copy-template-dir/copy-template-dir.js';
17
16
  import { getDotEnvVariables, injectEnvVariables } from '../../utils/dev.js';
@@ -313,8 +312,7 @@ const downloadFromURL = async function (command, options, argumentName, function
313
312
  // read, execute, and delete function template file if exists
314
313
  const fnTemplateFile = path.join(fnFolder, '.netlify-function-template.mjs');
315
314
  if (await fileExistsAsync(fnTemplateFile)) {
316
- const { default: { addons = [], onComplete }, } = await import(pathToFileURL(fnTemplateFile).href);
317
- await installAddons(command, addons, path.resolve(fnFolder));
315
+ const { default: { onComplete }, } = await import(pathToFileURL(fnTemplateFile).href);
318
316
  await handleOnComplete({ command, onComplete });
319
317
  // delete
320
318
  await unlink(fnTemplateFile);
@@ -400,7 +398,7 @@ const scaffoldFromTemplate = async function (command, options, argumentName, fun
400
398
  log(`${NETLIFYDEVLOG} Open in browser: https://github.com/netlify/cli/issues/new`);
401
399
  }
402
400
  else {
403
- const { addons = [], lang, name: templateName, onComplete } = chosenTemplate;
401
+ const { lang, name: templateName, onComplete } = chosenTemplate;
404
402
  const pathToTemplate = path.join(templatesDir, lang, templateName);
405
403
  if (!fs.existsSync(pathToTemplate)) {
406
404
  throw new Error(`There isn't a corresponding directory to the selected name. Template '${templateName}' is misconfigured`);
@@ -435,33 +433,12 @@ const scaffoldFromTemplate = async function (command, options, argumentName, fun
435
433
  if (funcType === 'edge') {
436
434
  await registerEFInToml(name, command.netlify);
437
435
  }
438
- await installAddons(command, addons, path.resolve(functionPath));
439
436
  await handleOnComplete({ command, onComplete });
440
437
  log();
441
438
  log(chalk.greenBright(`Function created!`));
442
439
  }
443
440
  };
444
441
  const TEMPLATE_PERMISSIONS = 0o777;
445
- // @ts-expect-error TS(7031) FIXME: Binding element 'addonName' implicitly has an 'any... Remove this comment to see the full error message
446
- const createFunctionAddon = async function ({ addonName, addons, api, siteData, siteId }) {
447
- try {
448
- const addon = getCurrentAddon({ addons, addonName });
449
- if (addon && addon.id) {
450
- log(`The "${addonName} add-on" already exists for ${siteData.name}`);
451
- return false;
452
- }
453
- await api.createServiceInstance({
454
- siteId,
455
- addon: addonName,
456
- body: { config: {} },
457
- });
458
- log(`Add-on "${addonName}" created for ${siteData.name}`);
459
- return true;
460
- }
461
- catch (error_) {
462
- return logAndThrowError(error_.message);
463
- }
464
- };
465
442
  /**
466
443
  *
467
444
  * @param {object} config
@@ -481,73 +458,6 @@ const handleOnComplete = async ({ command, onComplete }) => {
481
458
  await onComplete.call(command);
482
459
  }
483
460
  };
484
- /**
485
- *
486
- * @param {object} config
487
- * @param {*} config.addonCreated
488
- * @param {*} config.addonDidInstall
489
- * @param {import('../base-command.js').default} config.command
490
- * @param {string} config.fnPath
491
- */
492
- // @ts-expect-error TS(7031) FIXME: Binding element 'addonCreated' implicitly has an '... Remove this comment to see the full error message
493
- const handleAddonDidInstall = async ({ addonCreated, addonDidInstall, command, fnPath }) => {
494
- const { config } = command.netlify;
495
- if (!addonCreated || !addonDidInstall) {
496
- return;
497
- }
498
- const confirmPostInstall = await confirm({
499
- message: `This template has an optional setup script that runs after addon install. This can be helpful for first time users to try out templates. Run the script?`,
500
- default: false,
501
- });
502
- if (!confirmPostInstall) {
503
- return;
504
- }
505
- await injectEnvVariables({
506
- devConfig: { ...config.dev },
507
- env: command.netlify.cachedConfig.env,
508
- site: command.netlify.site,
509
- });
510
- addonDidInstall(fnPath);
511
- };
512
- /**
513
- *
514
- * @param {import('../base-command.js').default} command
515
- * @param {*} functionAddons
516
- * @param {*} fnPath
517
- * @returns
518
- */
519
- // @ts-expect-error TS(7006) FIXME: Parameter 'command' implicitly has an 'any' type.
520
- const installAddons = async function (command, functionAddons, fnPath) {
521
- if (functionAddons.length === 0) {
522
- return;
523
- }
524
- const { api, site } = command.netlify;
525
- const siteId = site.id;
526
- if (!siteId) {
527
- log('No project id found, please run inside a project directory or `netlify link`');
528
- return false;
529
- }
530
- log(`${NETLIFYDEVLOG} checking Netlify APIs...`);
531
- const [siteData, siteAddons] = await Promise.all([getSiteData({ api, siteId }), getAddons({ api, siteId })]);
532
- // @ts-expect-error TS(7031) FIXME: Binding element 'addonDidInstall' implicitly has a... Remove this comment to see the full error message
533
- const arr = functionAddons.map(async ({ addonDidInstall, addonName }) => {
534
- log(`${NETLIFYDEVLOG} installing addon: ${chalk.yellow.inverse(addonName)}`);
535
- try {
536
- const addonCreated = await createFunctionAddon({
537
- api,
538
- addons: siteAddons,
539
- siteId,
540
- addonName,
541
- siteData,
542
- });
543
- await handleAddonDidInstall({ addonCreated, addonDidInstall, command, fnPath });
544
- }
545
- catch (error_) {
546
- return logAndThrowError(`${NETLIFYDEVERR} Error installing addon: ${error_}`);
547
- }
548
- });
549
- return Promise.all(arr);
550
- };
551
461
  /**
552
462
  *
553
463
  * @param {string} funcName
@@ -4,6 +4,8 @@ export const createInitCommand = (program) => program
4
4
  .description('Configure continuous deployment for a new or existing project. To create a new project without continuous deployment, use `netlify sites:create`')
5
5
  .option('-m, --manual', 'Manually configure a git remote for CI')
6
6
  .option('--git-remote-name <name>', 'Name of Git remote to use. e.g. "origin"')
7
+ .option('--skip-agent-setup', 'Skip installing Netlify skills for AI coding agents into the project')
8
+ .option('--reset-context', 'Replace locally edited Netlify skills with the latest release, and migrate or delete edited copies under renamed or deprecated names')
7
9
  .addHelpText('after', () => {
8
10
  const docsUrl = 'https://docs.netlify.com/cli/get-started/';
9
11
  return `
@@ -12,6 +14,9 @@ For more information about getting started with Netlify CLI, see ${terminalLink(
12
14
  })
13
15
  .action(async (options, command) => {
14
16
  const { init } = await import('./init.js');
15
- await init(options, command);
17
+ await init(options, command, {
18
+ setupAgentSkills: !options.skipAgentSetup,
19
+ resetContext: Boolean(options.resetContext),
20
+ });
16
21
  });
17
22
  //# sourceMappingURL=index.js.map
@@ -6,7 +6,9 @@ type InitExitMessageCustomizer = (code: InitExitCode, defaultMessage: string) =>
6
6
  type InitExtraOptions = {
7
7
  customizeExitMessage?: InitExitMessageCustomizer | undefined;
8
8
  exitAfterConfiguringRepo?: boolean | undefined;
9
+ setupAgentSkills?: boolean | undefined;
10
+ resetContext?: boolean | undefined;
9
11
  };
10
- export declare const init: (options: OptionValues, command: BaseCommand, { customizeExitMessage, exitAfterConfiguringRepo }?: InitExtraOptions) => Promise<SiteInfo>;
12
+ export declare const init: (options: OptionValues, command: BaseCommand, { customizeExitMessage, exitAfterConfiguringRepo, setupAgentSkills: shouldSetupAgentSkills, resetContext, }?: InitExtraOptions) => Promise<SiteInfo>;
11
13
  export {};
12
14
  //# sourceMappingURL=init.d.ts.map
@@ -7,6 +7,7 @@ import { configureRepo } from '../../utils/init/config.js';
7
7
  import { track } from '../../utils/telemetry/index.js';
8
8
  import { link } from '../link/link.js';
9
9
  import { sitesCreate } from '../sites/sites-create.js';
10
+ import { setupAgentSkills } from '../../utils/init/agent-skills.js';
10
11
  import { getBuildSettings, saveNetlifyToml } from '../../utils/init/utils.js';
11
12
  import { LINKED_EXISTING_SITE_EXIT_CODE, LINKED_NEW_SITE_EXIT_CODE } from './constants.js';
12
13
  const persistState = ({ siteInfo, state }) => {
@@ -147,14 +148,33 @@ const logExistingRepoSetupAndExit = ({ repoUrl, siteName, customizeExitMessage,
147
148
  // TODO add support for changing GitHub repo in site:config command
148
149
  exit();
149
150
  };
150
- export const init = async (options, command, { customizeExitMessage, exitAfterConfiguringRepo = false } = {}) => {
151
- command.setAnalyticsPayload({ manual: options.manual, force: options.force });
151
+ const installAgentSkills = async (command, reset) => {
152
+ log();
153
+ const result = await setupAgentSkills({ workingDir: command.netlify.repositoryRoot, reset });
154
+ await track('sites_agentSkillsSetup', {
155
+ installed: result.installed,
156
+ directories: result.directories,
157
+ skillsVersion: result.skillsVersion,
158
+ resetContext: reset,
159
+ ...result.summary,
160
+ });
161
+ };
162
+ export const init = async (options, command, { customizeExitMessage, exitAfterConfiguringRepo = false, setupAgentSkills: shouldSetupAgentSkills = false, resetContext = false, } = {}) => {
163
+ command.setAnalyticsPayload({
164
+ manual: options.manual,
165
+ force: options.force,
166
+ skipAgentSetup: options.skipAgentSetup,
167
+ resetContext: options.resetContext,
168
+ });
152
169
  const { repositoryRoot, state } = command.netlify;
153
170
  const { siteInfo: existingSiteInfo } = command.netlify;
154
171
  // Check logged in status
155
172
  await command.authenticate();
156
173
  // Add .netlify to .gitignore file
157
174
  await ensureNetlifyIgnore(repositoryRoot);
175
+ if (shouldSetupAgentSkills) {
176
+ await installAgentSkills(command, resetContext);
177
+ }
158
178
  const repoUrl = getRepoUrl(existingSiteInfo);
159
179
  if (repoUrl && !options.force) {
160
180
  logExistingAndExit({ siteInfo: existingSiteInfo });
@@ -33,7 +33,7 @@ export const serve = async (options, command) => {
33
33
  log(`${NETLIFYDEVLOG} Injecting environment variable values for ${chalk.yellow('all scopes')}`);
34
34
  }
35
35
  env = await getDotEnvVariables({ devConfig, env, site });
36
- const { accountId, addonsUrls, capabilities, siteUrl, timeouts } = await getSiteInformation({
36
+ const { accountId, capabilities, siteUrl, timeouts } = await getSiteInformation({
37
37
  // inherited from base command --offline
38
38
  offline: options.offline,
39
39
  api,
@@ -145,7 +145,6 @@ export const serve = async (options, command) => {
145
145
  };
146
146
  const inspectSettings = generateInspectSettings(options.edgeInspect, options.edgeInspectBrk);
147
147
  const url = await startProxyServer({
148
- addonsUrls,
149
148
  blobsContext: runtimeBlobsContext,
150
149
  command,
151
150
  config: mergedConfig,
@@ -20,7 +20,7 @@ export const sitesList = async (options, command) => {
20
20
  ssl_url: site.ssl_url,
21
21
  account_name: site.account_name,
22
22
  };
23
- if (site.build_settings && site.build_settings.repo_url) {
23
+ if (site.build_settings?.repo_url) {
24
24
  siteInfo.repo_url = site.build_settings.repo_url;
25
25
  }
26
26
  return siteInfo;
@@ -13,7 +13,7 @@ export const unlink = async (_options, command) => {
13
13
  await track('sites_unlinked', {
14
14
  siteId: siteData.id || siteId,
15
15
  });
16
- if (site && site.configPath) {
16
+ if (site?.configPath) {
17
17
  log(`Unlinked ${site.configPath} from ${siteData ? siteData.name : siteId}`);
18
18
  }
19
19
  else {
@@ -235,7 +235,6 @@ export class EdgeFunctionsRegistryImpl {
235
235
  Object.entries(envConfig).forEach(([key, variable]) => {
236
236
  if (variable.sources.includes('ui') ||
237
237
  variable.sources.includes('account') ||
238
- variable.sources.includes('addons') ||
239
238
  variable.sources.includes('internal') ||
240
239
  variable.sources.some((source) => source.startsWith('.env'))) {
241
240
  env[key] = variable.value;
@@ -105,7 +105,7 @@ export const shouldFetchLatestVersion = async ({ binPath, execArgs, execName, la
105
105
  if (!stdout) {
106
106
  return false;
107
107
  }
108
- const match = stdout.match(new RegExp(pattern));
108
+ const match = new RegExp(pattern).exec(stdout);
109
109
  if (!match) {
110
110
  return false;
111
111
  }
@@ -46,7 +46,8 @@ export const createFormSubmissionHandler = function ({ functionsRegistry, siteUr
46
46
  }
47
47
  const originalUrl = new URL(req.url, 'http://localhost');
48
48
  req.url = `/.netlify/functions/${handlerName}${originalUrl.search}`;
49
- const ct = parseContentType(req);
49
+ // A missing header parses to an empty type and takes the unsupported-type branch below.
50
+ const ct = parseContentType(req.headers['content-type'] ?? '');
50
51
  let fields = {};
51
52
  let files = {};
52
53
  if (ct.type.endsWith('/x-www-form-urlencoded')) {
@@ -8,9 +8,9 @@ export declare const startSpinner: ({ text }: {
8
8
  /**
9
9
  * Stops the spinner with the following text
10
10
  */
11
- export declare const stopSpinner: ({ error, spinner, text }: {
11
+ export declare const stopSpinner: ({ error, spinner, text, }: {
12
12
  error?: boolean;
13
- spinner: Spinner;
13
+ spinner: Spinner | undefined;
14
14
  text?: string;
15
15
  }) => void;
16
16
  /**
@@ -33,7 +33,7 @@ export const startSpinner = ({ text }) => {
33
33
  /**
34
34
  * Stops the spinner with the following text
35
35
  */
36
- export const stopSpinner = ({ error, spinner, text }) => {
36
+ export const stopSpinner = ({ error, spinner, text, }) => {
37
37
  if (!spinner) {
38
38
  return;
39
39
  }
@@ -73,7 +73,7 @@ export const parseContextFile = (contents) => {
73
73
  const result = {
74
74
  contents,
75
75
  };
76
- const providerContext = contents.match(PROVIDER_CONTEXT_REGEX);
76
+ const providerContext = PROVIDER_CONTEXT_REGEX.exec(contents);
77
77
  if (providerContext) {
78
78
  const [, attributes, innerContents] = providerContext;
79
79
  result.innerContents = innerContents;
@@ -90,7 +90,7 @@ export const parseContextFile = (contents) => {
90
90
  }
91
91
  }
92
92
  }
93
- const contextOverrides = contents.match(PROVIDER_CONTEXT_OVERRIDES_REGEX);
93
+ const contextOverrides = PROVIDER_CONTEXT_OVERRIDES_REGEX.exec(contents);
94
94
  if (contextOverrides) {
95
95
  const [overrideContents, , innerContents] = contextOverrides;
96
96
  result.overrides = {