netlify-cli 18.0.4 → 18.1.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.
@@ -1,6 +1,6 @@
1
1
  import { OptionValues } from 'commander';
2
2
  import BaseCommand from '../base-command.js';
3
- interface RunRecipeOptions {
3
+ export interface RunRecipeOptions {
4
4
  args: string[];
5
5
  command?: BaseCommand;
6
6
  config: unknown;
@@ -9,5 +9,4 @@ interface RunRecipeOptions {
9
9
  }
10
10
  export declare const runRecipe: ({ args, command, config, recipeName, repositoryRoot }: RunRecipeOptions) => Promise<any>;
11
11
  export declare const recipesCommand: (recipeName: string, options: OptionValues, command: BaseCommand) => Promise<any>;
12
- export {};
13
12
  //# sourceMappingURL=recipes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../../../src/commands/recipes/recipes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAKxC,OAAO,WAAW,MAAM,oBAAoB,CAAA;AAM5C,UAAU,gBAAgB;IACxB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,MAAM,EAAE,OAAO,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,SAAS,0DAAiE,gBAAgB,iBAItG,CAAA;AAED,eAAO,MAAM,cAAc,eAAsB,MAAM,WAAW,YAAY,WAAW,WAAW,KAAG,OAAO,CAAC,GAAG,CAgDjH,CAAA"}
1
+ {"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../../../src/commands/recipes/recipes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAKxC,OAAO,WAAW,MAAM,oBAAoB,CAAA;AAM5C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,MAAM,EAAE,OAAO,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,SAAS,0DAAiE,gBAAgB,iBAItG,CAAA;AAED,eAAO,MAAM,cAAc,eAAsB,MAAM,WAAW,YAAY,WAAW,WAAW,KAAG,OAAO,CAAC,GAAG,CAgDjH,CAAA"}
@@ -0,0 +1,33 @@
1
+ export declare const FILE_NAME = "netlify-development.mdc";
2
+ export declare const NETLIFY_PROVIDER = "netlify";
3
+ export declare const downloadFile: (cliVersion: string) => Promise<{
4
+ contents: string;
5
+ minimumCLIVersion: string | undefined;
6
+ } | null>;
7
+ interface ParsedContextFile {
8
+ contents: string;
9
+ innerContents?: string;
10
+ overrides?: {
11
+ contents?: string;
12
+ innerContents?: string;
13
+ };
14
+ provider?: string;
15
+ version?: string;
16
+ }
17
+ /**
18
+ * Parses the `<ProviderContext>` and `<ProviderContextOverrides>` blocks in
19
+ * a context file.
20
+ */
21
+ export declare const parseContextFile: (contents: string) => ParsedContextFile;
22
+ /**
23
+ * Takes a context file (a template) and injects a string in an overrides block
24
+ * if one is found. Returns the resulting context file.
25
+ */
26
+ export declare const applyOverrides: (template: string, overrides?: string) => string;
27
+ /**
28
+ * Reads a file on disk and tries to parse it as a context file.
29
+ */
30
+ export declare const getExistingContext: (path: string) => Promise<ParsedContextFile | null>;
31
+ export declare const writeFile: (path: string, contents: string) => Promise<void>;
32
+ export {};
33
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/recipes/ai-context/context.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,SAAS,4BAA4B,CAAA;AAElD,eAAO,MAAM,gBAAgB,YAAY,CAAA;AAKzC,eAAO,MAAM,YAAY,eAAsB,MAAM;;;SAmBpD,CAAA;AAED,UAAU,iBAAiB;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE;QACV,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,aAAa,CAAC,EAAE,MAAM,CAAA;KACvB,CAAA;IACD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,aAAc,MAAM,sBA0ChD,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,aAAc,MAAM,cAAc,MAAM,WASlE,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,SAAgB,MAAM,sCAqBpD,CAAA;AAED,eAAO,MAAM,SAAS,SAAgB,MAAM,YAAY,MAAM,kBAK7D,CAAA"}
@@ -0,0 +1,100 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ const ATTRIBUTES_REGEX = /(\S*)="([^\s"]*)"/gim;
4
+ const BASE_URL = 'https://docs.netlify.com/ai-context';
5
+ export const FILE_NAME = 'netlify-development.mdc';
6
+ const MINIMUM_CLI_VERSION_HEADER = 'x-cli-min-ver';
7
+ export const NETLIFY_PROVIDER = 'netlify';
8
+ const PROVIDER_CONTEXT_REGEX = /<providercontext ([^>]*)>(.*)<\/providercontext>/ims;
9
+ const PROVIDER_CONTEXT_OVERRIDES_REGEX = /<providercontextoverrides([^>]*)>(.*)<\/providercontextoverrides>/ims;
10
+ const PROVIDER_CONTEXT_OVERRIDES_TAG = 'ProviderContextOverrides';
11
+ export const downloadFile = async (cliVersion) => {
12
+ try {
13
+ const res = await fetch(`${BASE_URL}/${FILE_NAME}`, {
14
+ headers: {
15
+ 'user-agent': `NetlifyCLI ${cliVersion}`,
16
+ },
17
+ });
18
+ const contents = await res.text();
19
+ const minimumCLIVersion = res.headers.get(MINIMUM_CLI_VERSION_HEADER) ?? undefined;
20
+ return {
21
+ contents,
22
+ minimumCLIVersion,
23
+ };
24
+ }
25
+ catch {
26
+ // no-op
27
+ }
28
+ return null;
29
+ };
30
+ /**
31
+ * Parses the `<ProviderContext>` and `<ProviderContextOverrides>` blocks in
32
+ * a context file.
33
+ */
34
+ export const parseContextFile = (contents) => {
35
+ const result = {
36
+ contents,
37
+ };
38
+ const providerContext = contents.match(PROVIDER_CONTEXT_REGEX);
39
+ if (providerContext) {
40
+ const [, attributes, innerContents] = providerContext;
41
+ result.innerContents = innerContents;
42
+ for (const [, name, value] of attributes.matchAll(ATTRIBUTES_REGEX)) {
43
+ switch (name.toLowerCase()) {
44
+ case 'provider':
45
+ result.provider = value;
46
+ break;
47
+ case 'version':
48
+ result.version = value;
49
+ break;
50
+ default:
51
+ continue;
52
+ }
53
+ }
54
+ }
55
+ const contextOverrides = contents.match(PROVIDER_CONTEXT_OVERRIDES_REGEX);
56
+ if (contextOverrides) {
57
+ const [overrideContents, , innerContents] = contextOverrides;
58
+ result.overrides = {
59
+ contents: overrideContents,
60
+ innerContents,
61
+ };
62
+ }
63
+ return result;
64
+ };
65
+ /**
66
+ * Takes a context file (a template) and injects a string in an overrides block
67
+ * if one is found. Returns the resulting context file.
68
+ */
69
+ export const applyOverrides = (template, overrides) => {
70
+ if (!overrides) {
71
+ return template;
72
+ }
73
+ return template.replace(PROVIDER_CONTEXT_OVERRIDES_REGEX, `<${PROVIDER_CONTEXT_OVERRIDES_TAG}>${overrides}</${PROVIDER_CONTEXT_OVERRIDES_TAG}>`);
74
+ };
75
+ /**
76
+ * Reads a file on disk and tries to parse it as a context file.
77
+ */
78
+ export const getExistingContext = async (path) => {
79
+ try {
80
+ const stats = await fs.stat(path);
81
+ if (!stats.isFile()) {
82
+ throw new Error(`${path} already exists but is not a file. Please remove it or rename it and try again.`);
83
+ }
84
+ const file = await fs.readFile(path, 'utf8');
85
+ const parsedFile = parseContextFile(file);
86
+ return parsedFile;
87
+ }
88
+ catch (error) {
89
+ const exception = error;
90
+ if (exception.code !== 'ENOENT') {
91
+ throw new Error(`Could not open context file at ${path}: ${exception.message}`);
92
+ }
93
+ return null;
94
+ }
95
+ };
96
+ export const writeFile = async (path, contents) => {
97
+ const directory = dirname(path);
98
+ await fs.mkdir(directory, { recursive: true });
99
+ await fs.writeFile(path, contents);
100
+ };
@@ -0,0 +1,4 @@
1
+ import type { RunRecipeOptions } from '../../commands/recipes/recipes.js';
2
+ export declare const description = "Manage context files for AI tools";
3
+ export declare const run: ({ args, command }: RunRecipeOptions) => Promise<void>;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/recipes/ai-context/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AAazE,eAAO,MAAM,WAAW,sCAAsC,CAAA;AAuC9D,eAAO,MAAM,GAAG,sBAA6B,gBAAgB,kBAuE5D,CAAA"}
@@ -0,0 +1,85 @@
1
+ import { resolve } from 'node:path';
2
+ import inquirer from 'inquirer';
3
+ import semver from 'semver';
4
+ import { chalk, error, log, version } from '../../utils/command-helpers.js';
5
+ import { applyOverrides, downloadFile, getExistingContext, parseContextFile, writeFile, FILE_NAME, NETLIFY_PROVIDER, } from './context.js';
6
+ export const description = 'Manage context files for AI tools';
7
+ const presets = [
8
+ { name: 'Cursor rules (.cursor/rules/)', value: '.cursor/rules' },
9
+ { name: 'Custom location', value: '' },
10
+ ];
11
+ const promptForPath = async () => {
12
+ const { presetPath } = await inquirer.prompt([
13
+ {
14
+ name: 'presetPath',
15
+ message: 'Where should we put the context files?',
16
+ type: 'list',
17
+ choices: presets,
18
+ },
19
+ ]);
20
+ if (presetPath) {
21
+ return presetPath;
22
+ }
23
+ const { customPath } = await inquirer.prompt([
24
+ {
25
+ type: 'input',
26
+ name: 'customPath',
27
+ message: 'Enter the path, relative to the project root, where the context files should be placed',
28
+ default: './ai-context',
29
+ },
30
+ ]);
31
+ if (customPath) {
32
+ return customPath;
33
+ }
34
+ log('You must select a path.');
35
+ return promptForPath();
36
+ };
37
+ export const run = async ({ args, command }) => {
38
+ // Start the download in the background while we wait for the prompts.
39
+ // eslint-disable-next-line promise/prefer-await-to-then
40
+ const download = downloadFile(version).catch(() => null);
41
+ const filePath = args[0] || (await promptForPath());
42
+ const { contents: downloadedFile, minimumCLIVersion } = (await download) ?? {};
43
+ if (!downloadedFile) {
44
+ error('An error occurred when pulling the latest context files. Please try again.');
45
+ return;
46
+ }
47
+ if (minimumCLIVersion && semver.lt(version, minimumCLIVersion)) {
48
+ error(`This command requires version ${minimumCLIVersion} or above of the Netlify CLI. Refer to ${chalk.underline('https://ntl.fyi/update-cli')} for information on how to update.`);
49
+ return;
50
+ }
51
+ const absoluteFilePath = resolve(command?.workingDir ?? '', filePath, FILE_NAME);
52
+ const existing = await getExistingContext(absoluteFilePath);
53
+ const remote = parseContextFile(downloadedFile);
54
+ let { contents } = remote;
55
+ // Does a file already exist at this path?
56
+ if (existing) {
57
+ // If it's a file we've created, let's check the version and bail if we're
58
+ // already on the latest, otherwise rewrite it with the latest version.
59
+ if (existing.provider?.toLowerCase() === NETLIFY_PROVIDER) {
60
+ if (remote?.version === existing.version) {
61
+ log(`You're all up to date! ${chalk.underline(absoluteFilePath)} contains the latest version of the context files.`);
62
+ return;
63
+ }
64
+ // We must preserve any overrides found in the existing file.
65
+ contents = applyOverrides(remote.contents, existing.overrides?.innerContents);
66
+ }
67
+ else {
68
+ // If this is not a file we've created, we can offer to overwrite it and
69
+ // preserve the existing contents by moving it to the overrides slot.
70
+ const { confirm } = await inquirer.prompt({
71
+ type: 'confirm',
72
+ name: 'confirm',
73
+ message: `A context file already exists at ${chalk.underline(absoluteFilePath)}. It has not been created by the Netlify CLI, but we can update it while preserving its existing content. Can we proceed?`,
74
+ default: true,
75
+ });
76
+ if (!confirm) {
77
+ return;
78
+ }
79
+ // Whatever exists in the file goes in the overrides block.
80
+ contents = applyOverrides(remote.contents, existing.contents);
81
+ }
82
+ }
83
+ await writeFile(absoluteFilePath, contents);
84
+ log(`${existing ? 'Updated' : 'Created'} context files at ${chalk.underline(absoluteFilePath)}`);
85
+ };