netlify-cli 21.3.0 → 21.4.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.
- package/dist/commands/blobs/blobs.js +1 -1
- package/dist/commands/blobs/blobs.js.map +1 -1
- package/dist/commands/build/index.js +1 -1
- package/dist/commands/build/index.js.map +1 -1
- package/dist/commands/clone/index.js +1 -1
- package/dist/commands/clone/index.js.map +1 -1
- package/dist/commands/deploy/deploy.js +6 -6
- package/dist/commands/deploy/deploy.js.map +1 -1
- package/dist/commands/deploy/index.js +1 -1
- package/dist/commands/deploy/index.js.map +1 -1
- package/dist/commands/dev/index.js +1 -1
- package/dist/commands/dev/index.js.map +1 -1
- package/dist/commands/env/env.js +1 -1
- package/dist/commands/env/env.js.map +1 -1
- package/dist/commands/functions/functions.js +1 -1
- package/dist/commands/functions/functions.js.map +1 -1
- package/dist/commands/init/index.js +1 -1
- package/dist/commands/init/index.js.map +1 -1
- package/dist/commands/link/index.js +1 -1
- package/dist/commands/link/index.js.map +1 -1
- package/dist/commands/login/index.js +1 -1
- package/dist/commands/login/index.js.map +1 -1
- package/dist/commands/main.js +3 -3
- package/dist/commands/main.js.map +1 -1
- package/dist/commands/unlink/index.js +1 -1
- package/dist/commands/unlink/index.js.map +1 -1
- package/dist/recipes/ai-context/context.d.ts +28 -2
- package/dist/recipes/ai-context/context.d.ts.map +1 -1
- package/dist/recipes/ai-context/context.js +100 -6
- package/dist/recipes/ai-context/context.js.map +1 -1
- package/dist/recipes/ai-context/index.d.ts +1 -1
- package/dist/recipes/ai-context/index.d.ts.map +1 -1
- package/dist/recipes/ai-context/index.js +69 -81
- package/dist/recipes/ai-context/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utils/command-helpers.d.ts.map +1 -1
- package/dist/utils/command-helpers.js +3 -2
- package/dist/utils/command-helpers.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -1,20 +1,58 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import semver from 'semver';
|
|
4
|
+
import { chalk, logAndThrowError, log, version } from '../../utils/command-helpers.js';
|
|
3
5
|
const ATTRIBUTES_REGEX = /(\S*)="([^\s"]*)"/gim;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
// AI_CONTEXT_BASE_URL is used to help with local testing at non-production
|
|
7
|
+
// versions of the context apis.
|
|
8
|
+
const BASE_URL = new URL(process.env.AI_CONTEXT_BASE_URL ?? 'https://docs.netlify.com/ai-context').toString();
|
|
9
|
+
export const NTL_DEV_MCP_FILE_NAME = 'netlify-development.mdc';
|
|
6
10
|
const MINIMUM_CLI_VERSION_HEADER = 'x-cli-min-ver';
|
|
7
11
|
export const NETLIFY_PROVIDER = 'netlify';
|
|
8
12
|
const PROVIDER_CONTEXT_REGEX = /<providercontext ([^>]*)>(.*)<\/providercontext>/ims;
|
|
9
13
|
const PROVIDER_CONTEXT_OVERRIDES_REGEX = /<providercontextoverrides([^>]*)>(.*)<\/providercontextoverrides>/ims;
|
|
10
14
|
const PROVIDER_CONTEXT_OVERRIDES_TAG = 'ProviderContextOverrides';
|
|
11
|
-
|
|
15
|
+
let contextConsumers = [];
|
|
16
|
+
export const getContextConsumers = async (cliVersion) => {
|
|
17
|
+
if (contextConsumers.length > 0) {
|
|
18
|
+
return contextConsumers;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(`${BASE_URL}/context-consumers`, {
|
|
22
|
+
headers: {
|
|
23
|
+
'user-agent': `NetlifyCLI ${cliVersion}`,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const data = (await res.json());
|
|
30
|
+
contextConsumers = data?.consumers ?? [];
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
return contextConsumers;
|
|
34
|
+
};
|
|
35
|
+
export const downloadFile = async (cliVersion, contextConfig, consumer) => {
|
|
12
36
|
try {
|
|
13
|
-
|
|
37
|
+
if (!contextConfig.endpoint) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const url = new URL(contextConfig.endpoint, BASE_URL);
|
|
41
|
+
url.searchParams.set('consumer', consumer.key);
|
|
42
|
+
if (process.env.AI_CONTEXT_BASE_URL) {
|
|
43
|
+
const overridingUrl = new URL(process.env.AI_CONTEXT_BASE_URL);
|
|
44
|
+
url.host = overridingUrl.host;
|
|
45
|
+
url.port = overridingUrl.port;
|
|
46
|
+
url.protocol = overridingUrl.protocol;
|
|
47
|
+
}
|
|
48
|
+
const res = await fetch(url, {
|
|
14
49
|
headers: {
|
|
15
50
|
'user-agent': `NetlifyCLI ${cliVersion}`,
|
|
16
51
|
},
|
|
17
52
|
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
18
56
|
const contents = await res.text();
|
|
19
57
|
const minimumCLIVersion = res.headers.get(MINIMUM_CLI_VERSION_HEADER) ?? undefined;
|
|
20
58
|
return {
|
|
@@ -70,7 +108,9 @@ export const applyOverrides = (template, overrides) => {
|
|
|
70
108
|
if (!overrides) {
|
|
71
109
|
return template;
|
|
72
110
|
}
|
|
73
|
-
return template
|
|
111
|
+
return template
|
|
112
|
+
.replace(PROVIDER_CONTEXT_OVERRIDES_REGEX, `<${PROVIDER_CONTEXT_OVERRIDES_TAG}>${overrides}</${PROVIDER_CONTEXT_OVERRIDES_TAG}>`)
|
|
113
|
+
.trim();
|
|
74
114
|
};
|
|
75
115
|
/**
|
|
76
116
|
* Reads a file on disk and tries to parse it as a context file.
|
|
@@ -98,4 +138,58 @@ export const writeFile = async (path, contents) => {
|
|
|
98
138
|
await fs.mkdir(directory, { recursive: true });
|
|
99
139
|
await fs.writeFile(path, contents);
|
|
100
140
|
};
|
|
141
|
+
export const deleteFile = async (path) => {
|
|
142
|
+
try {
|
|
143
|
+
// delete file from file system - not just unlinking it
|
|
144
|
+
await fs.rm(path);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// ignore
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
export const downloadAndWriteContextFiles = async (consumer, { command }) => {
|
|
151
|
+
await Promise.allSettled(Object.keys(consumer.contextScopes).map(async (contextKey) => {
|
|
152
|
+
const contextConfig = consumer.contextScopes[contextKey];
|
|
153
|
+
const { contents: downloadedFile, minimumCLIVersion } = (await downloadFile(version, contextConfig, consumer).catch(() => null)) ?? {};
|
|
154
|
+
if (!downloadedFile) {
|
|
155
|
+
return logAndThrowError(`An error occurred when pulling the latest context file for scope ${contextConfig.scope}. Please try again.`);
|
|
156
|
+
}
|
|
157
|
+
if (minimumCLIVersion && semver.lt(version, minimumCLIVersion)) {
|
|
158
|
+
return logAndThrowError(`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.`);
|
|
159
|
+
}
|
|
160
|
+
const absoluteFilePath = resolve(command?.workingDir ?? '', consumer.path, `netlify-${contextKey}.${consumer.ext || 'mdc'}`);
|
|
161
|
+
const existing = await getExistingContext(absoluteFilePath);
|
|
162
|
+
const remote = parseContextFile(downloadedFile);
|
|
163
|
+
let { contents } = remote;
|
|
164
|
+
// Does a file already exist at this path?
|
|
165
|
+
if (existing) {
|
|
166
|
+
// If it's a file we've created, let's check the version and bail if we're
|
|
167
|
+
// already on the latest, otherwise rewrite it with the latest version.
|
|
168
|
+
if (existing.provider?.toLowerCase() === NETLIFY_PROVIDER) {
|
|
169
|
+
if (remote.version === existing.version) {
|
|
170
|
+
log(`You're all up to date! ${chalk.underline(absoluteFilePath)} contains the latest version of the context files.`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// We must preserve any overrides found in the existing file.
|
|
174
|
+
contents = applyOverrides(remote.contents, existing.overrides?.innerContents);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
// Whatever exists in the file goes in the overrides block.
|
|
178
|
+
contents = applyOverrides(remote.contents, existing.contents);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// we don't want to cut off content, but if we _have_ to
|
|
182
|
+
// then we need to do so before writing or the user's
|
|
183
|
+
// context gets in a bad state. Note, this can result in
|
|
184
|
+
// a file that's not parsable next time. This will be
|
|
185
|
+
// fine because the file will simply be replaced. Not ideal
|
|
186
|
+
// but solves the issue of a truncated file in a bad state
|
|
187
|
+
// being updated.
|
|
188
|
+
if (consumer.truncationLimit && contents.length > consumer.truncationLimit) {
|
|
189
|
+
contents = contents.slice(0, consumer.truncationLimit);
|
|
190
|
+
}
|
|
191
|
+
await writeFile(absoluteFilePath, contents);
|
|
192
|
+
log(`${existing ? 'Updated' : 'Created'} context files at ${chalk.underline(absoluteFilePath)}`);
|
|
193
|
+
}));
|
|
194
|
+
};
|
|
101
195
|
//# sourceMappingURL=context.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../../src/recipes/ai-context/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../../src/recipes/ai-context/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAA;AAGtF,MAAM,gBAAgB,GAAG,sBAAsB,CAAA;AAC/C,2EAA2E;AAC3E,gCAAgC;AAChC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,qCAAqC,CAAC,CAAC,QAAQ,EAAE,CAAA;AAC7G,MAAM,CAAC,MAAM,qBAAqB,GAAG,yBAAyB,CAAA;AAC9D,MAAM,0BAA0B,GAAG,eAAe,CAAA;AAClD,MAAM,CAAC,MAAM,gBAAgB,GAAG,SAAS,CAAA;AACzC,MAAM,sBAAsB,GAAG,qDAAqD,CAAA;AACpF,MAAM,gCAAgC,GAAG,sEAAsE,CAAA;AAC/G,MAAM,8BAA8B,GAAG,0BAA0B,CAAA;AA2BjE,IAAI,gBAAgB,GAAqB,EAAE,CAAA;AAC3C,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EAAE,UAAkB,EAAE,EAAE;IAC9D,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,OAAO,gBAAgB,CAAA;IACzB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,oBAAoB,EAAE;YACvD,OAAO,EAAE;gBACP,YAAY,EAAE,cAAc,UAAU,EAAE;aACzC;SACF,CAAC,CAAA;QAEF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,OAAO,EAAE,CAAA;QACX,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAgD,CAAA;QAC9E,gBAAgB,GAAG,IAAI,EAAE,SAAS,IAAI,EAAE,CAAA;IAC1C,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IAEV,OAAO,gBAAgB,CAAA;AACzB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,UAAkB,EAAE,aAA4B,EAAE,QAAwB,EAAE,EAAE;IAC/G,IAAI,CAAC;QACH,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC;YAC5B,OAAO,IAAI,CAAA;QACb,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QACrD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAA;QAE9C,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,CAAC;YACpC,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;YAC9D,GAAG,CAAC,IAAI,GAAG,aAAa,CAAC,IAAI,CAAA;YAC7B,GAAG,CAAC,IAAI,GAAG,aAAa,CAAC,IAAI,CAAA;YAC7B,GAAG,CAAC,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAA;QACvC,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,OAAO,EAAE;gBACP,YAAY,EAAE,cAAc,UAAU,EAAE;aACzC;SACF,CAAC,CAAA;QAEF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,OAAO,IAAI,CAAA;QACb,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;QACjC,MAAM,iBAAiB,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,IAAI,SAAS,CAAA;QAElF,OAAO;YACL,QAAQ;YACR,iBAAiB;SAClB,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,QAAQ;IACV,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAaD;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,QAAgB,EAAE,EAAE;IACnD,MAAM,MAAM,GAAsB;QAChC,QAAQ;KACT,CAAA;IAED,MAAM,eAAe,GAAG,QAAQ,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAA;IAE9D,IAAI,eAAe,EAAE,CAAC;QACpB,MAAM,CAAC,EAAE,UAAU,EAAE,aAAa,CAAC,GAAG,eAAe,CAAA;QAErD,MAAM,CAAC,aAAa,GAAG,aAAa,CAAA;QAEpC,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACpE,QAAQ,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBAC3B,KAAK,UAAU;oBACb,MAAM,CAAC,QAAQ,GAAG,KAAK,CAAA;oBAEvB,MAAK;gBAEP,KAAK,SAAS;oBACZ,MAAM,CAAC,OAAO,GAAG,KAAK,CAAA;oBAEtB,MAAK;gBAEP;oBACE,SAAQ;YACZ,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,gBAAgB,GAAG,QAAQ,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAA;IAEzE,IAAI,gBAAgB,EAAE,CAAC;QACrB,MAAM,CAAC,gBAAgB,EAAE,AAAD,EAAG,aAAa,CAAC,GAAG,gBAAgB,CAAA;QAE5D,MAAM,CAAC,SAAS,GAAG;YACjB,QAAQ,EAAE,gBAAgB;YAC1B,aAAa;SACd,CAAA;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,QAAgB,EAAE,SAAkB,EAAE,EAAE;IACrE,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED,OAAO,QAAQ;SACZ,OAAO,CACN,gCAAgC,EAChC,IAAI,8BAA8B,IAAI,SAAS,KAAK,8BAA8B,GAAG,CACtF;SACA,IAAI,EAAE,CAAA;AACX,CAAC,CAAA;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EAAE,IAAY,EAAE,EAAE;IACvD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAEjC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,iFAAiF,CAAC,CAAA;QAC3G,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAC5C,MAAM,UAAU,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAA;QAEzC,OAAO,UAAU,CAAA;IACnB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,SAAS,GAAG,KAA8B,CAAA;QAEhD,IAAI,SAAS,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,kCAAkC,IAAI,KAAK,SAAS,CAAC,OAAO,EAAE,CAAC,CAAA;QACjF,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAAE,IAAY,EAAE,QAAgB,EAAE,EAAE;IAChE,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE/B,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC9C,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;AACpC,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,IAAY,EAAE,EAAE;IAC/C,IAAI,CAAC;QACH,uDAAuD;QACvD,MAAM,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,4BAA4B,GAAG,KAAK,EAAE,QAAwB,EAAE,EAAE,OAAO,EAAoB,EAAE,EAAE;IAC5G,MAAM,OAAO,CAAC,UAAU,CACtB,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;QAC3D,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC,CAAA;QAExD,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,GACnD,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,aAAa,EAAE,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAA;QAEhF,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,OAAO,gBAAgB,CACrB,oEAAoE,aAAa,CAAC,KAAK,qBAAqB,CAC7G,CAAA;QACH,CAAC;QACD,IAAI,iBAAiB,IAAI,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC,EAAE,CAAC;YAC/D,OAAO,gBAAgB,CACrB,iCAAiC,iBAAiB,0CAA0C,KAAK,CAAC,SAAS,CACzG,4BAA4B,CAC7B,oCAAoC,CACtC,CAAA;QACH,CAAC;QAED,MAAM,gBAAgB,GAAG,OAAO,CAC9B,OAAO,EAAE,UAAU,IAAI,EAAE,EACzB,QAAQ,CAAC,IAAI,EACb,WAAW,UAAU,IAAI,QAAQ,CAAC,GAAG,IAAI,KAAK,EAAE,CACjD,CAAA;QAED,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,gBAAgB,CAAC,CAAA;QAC3D,MAAM,MAAM,GAAG,gBAAgB,CAAC,cAAc,CAAC,CAAA;QAE/C,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAA;QAEzB,0CAA0C;QAC1C,IAAI,QAAQ,EAAE,CAAC;YACb,0EAA0E;YAC1E,uEAAuE;YACvE,IAAI,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,CAAC;gBAC1D,IAAI,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,OAAO,EAAE,CAAC;oBACxC,GAAG,CACD,0BAA0B,KAAK,CAAC,SAAS,CACvC,gBAAgB,CACjB,oDAAoD,CACtD,CAAA;oBACD,OAAM;gBACR,CAAC;gBAED,6DAA6D;gBAC7D,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC,CAAA;YAC/E,CAAC;iBAAM,CAAC;gBACN,2DAA2D;gBAC3D,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAA;YAC/D,CAAC;QACH,CAAC;QAED,wDAAwD;QACxD,qDAAqD;QACrD,wDAAwD;QACxD,qDAAqD;QACrD,2DAA2D;QAC3D,0DAA0D;QAC1D,iBAAiB;QACjB,IAAI,QAAQ,CAAC,eAAe,IAAI,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,eAAe,EAAE,CAAC;YAC3E,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAA;QACxD,CAAC;QAED,MAAM,SAAS,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAA;QAE3C,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,qBAAqB,KAAK,CAAC,SAAS,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;IAClG,CAAC,CAAC,CACH,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { RunRecipeOptions } from '../../commands/recipes/recipes.js';
|
|
2
2
|
export declare const description = "Manage context files for AI tools";
|
|
3
|
-
export declare const run: (
|
|
3
|
+
export declare const run: (runOptions: RunRecipeOptions) => Promise<void>;
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/recipes/ai-context/index.ts"],"names":[],"mappings":"
|
|
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;AAYzE,eAAO,MAAM,WAAW,sCAAsC,CAAA;AAqH9D,eAAO,MAAM,GAAG,GAAU,YAAY,gBAAgB,kBAuCrD,CAAA"}
|
|
@@ -1,30 +1,40 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
|
-
import semver from 'semver';
|
|
4
3
|
import execa from 'execa';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { logAndThrowError, log, version } from '../../utils/command-helpers.js';
|
|
5
|
+
import { getExistingContext, NTL_DEV_MCP_FILE_NAME, getContextConsumers, deleteFile, downloadAndWriteContextFiles, } from './context.js';
|
|
7
6
|
export const description = 'Manage context files for AI tools';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
// context consumers endpoints returns all supported IDE and other consumers
|
|
8
|
+
// that can be used to pull context files. It also includes a catchall consumer
|
|
9
|
+
// for outlining all context that an unspecified consumer would handle.
|
|
10
|
+
const allContextConsumers = (await getContextConsumers(version)).filter((consumer) => !consumer.hideFromCLI);
|
|
11
|
+
const cliContextConsumers = allContextConsumers.filter((consumer) => !consumer.hideFromCLI);
|
|
12
|
+
const rulesForDefaultConsumer = allContextConsumers.find((consumer) => consumer.key === 'catchall-consumer') ?? {
|
|
13
|
+
key: 'catchall-consumer',
|
|
14
|
+
path: './ai-context',
|
|
15
|
+
presentedName: '',
|
|
16
|
+
ext: 'mdc',
|
|
17
|
+
contextScopes: {},
|
|
18
|
+
hideFromCLI: true,
|
|
11
19
|
};
|
|
12
|
-
const presets =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
const presets = cliContextConsumers.map((consumer) => ({
|
|
21
|
+
name: consumer.presentedName,
|
|
22
|
+
value: consumer.key,
|
|
23
|
+
}));
|
|
24
|
+
// always add the custom location option (not preset from API)
|
|
25
|
+
presets.push({ name: 'Custom location', value: rulesForDefaultConsumer.key });
|
|
26
|
+
const promptForContextConsumerSelection = async () => {
|
|
27
|
+
const { consumerKey } = await inquirer.prompt([
|
|
19
28
|
{
|
|
20
|
-
name: '
|
|
29
|
+
name: 'consumerKey',
|
|
21
30
|
message: 'Where should we put the context files?',
|
|
22
31
|
type: 'list',
|
|
23
32
|
choices: presets,
|
|
24
33
|
},
|
|
25
34
|
]);
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
const contextConsumer = consumerKey ? cliContextConsumers.find((consumer) => consumer.key === consumerKey) : null;
|
|
36
|
+
if (contextConsumer) {
|
|
37
|
+
return contextConsumer;
|
|
28
38
|
}
|
|
29
39
|
const { customPath } = await inquirer.prompt([
|
|
30
40
|
{
|
|
@@ -35,31 +45,19 @@ const promptForPath = async () => {
|
|
|
35
45
|
},
|
|
36
46
|
]);
|
|
37
47
|
if (customPath) {
|
|
38
|
-
return customPath;
|
|
48
|
+
return { ...rulesForDefaultConsumer, path: customPath || rulesForDefaultConsumer.path };
|
|
39
49
|
}
|
|
40
50
|
log('You must select a path.');
|
|
41
|
-
return
|
|
51
|
+
return promptForContextConsumerSelection();
|
|
42
52
|
};
|
|
43
|
-
const IDE = [
|
|
44
|
-
{
|
|
45
|
-
name: 'Windsurf',
|
|
46
|
-
command: 'windsurf',
|
|
47
|
-
rulesPath: IDE_RULES_PATH_MAP.windsurf,
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
name: 'Cursor',
|
|
51
|
-
command: 'cursor',
|
|
52
|
-
rulesPath: IDE_RULES_PATH_MAP.cursor,
|
|
53
|
-
},
|
|
54
|
-
];
|
|
55
53
|
/**
|
|
56
54
|
* Checks if a command belongs to a known IDEs by checking if it includes a specific string.
|
|
57
55
|
* For example, the command that starts windsurf looks something like "/applications/windsurf.app/contents/...".
|
|
58
56
|
*/
|
|
59
|
-
const
|
|
57
|
+
const getConsumerKeyFromCommand = (command) => {
|
|
60
58
|
// The actual command is something like "/applications/windsurf.app/contents/...", but we are only looking for windsurf
|
|
61
|
-
const match =
|
|
62
|
-
return match
|
|
59
|
+
const match = cliContextConsumers.find((consumer) => consumer.consumerProcessCmd && command.includes(consumer.consumerProcessCmd));
|
|
60
|
+
return match ? match.key : null;
|
|
63
61
|
};
|
|
64
62
|
/**
|
|
65
63
|
* Receives a process ID (pid) and returns both the command that the process was run with and its parent process ID. If the process is a known IDE, also returns information about that IDE.
|
|
@@ -71,9 +69,9 @@ const getCommandAndParentPID = async (pid) => {
|
|
|
71
69
|
const parentPID = output.substring(0, spaceIndex);
|
|
72
70
|
const command = output.substring(spaceIndex + 1).toLowerCase();
|
|
73
71
|
return {
|
|
74
|
-
parentPID:
|
|
75
|
-
command
|
|
76
|
-
|
|
72
|
+
parentPID: Number(parentPID),
|
|
73
|
+
command,
|
|
74
|
+
consumerKey: getConsumerKeyFromCommand(command),
|
|
77
75
|
};
|
|
78
76
|
};
|
|
79
77
|
const getPathByDetectingIDE = async () => {
|
|
@@ -82,7 +80,7 @@ const getPathByDetectingIDE = async () => {
|
|
|
82
80
|
let result;
|
|
83
81
|
try {
|
|
84
82
|
result = await getCommandAndParentPID(ppid);
|
|
85
|
-
while (result.parentPID !== 1 && !result.
|
|
83
|
+
while (result.parentPID !== 1 && !result.consumerKey) {
|
|
86
84
|
result = await getCommandAndParentPID(result.parentPID);
|
|
87
85
|
}
|
|
88
86
|
}
|
|
@@ -91,54 +89,44 @@ const getPathByDetectingIDE = async () => {
|
|
|
91
89
|
// perhaps we are on a machine that doesn't support it.
|
|
92
90
|
return null;
|
|
93
91
|
}
|
|
94
|
-
|
|
92
|
+
if (result?.consumerKey) {
|
|
93
|
+
const contextConsumer = cliContextConsumers.find((consumer) => consumer.key === result.consumerKey);
|
|
94
|
+
if (contextConsumer) {
|
|
95
|
+
return contextConsumer;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
95
99
|
};
|
|
96
|
-
export const run = async (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const filePath = args[0]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const { contents: downloadedFile, minimumCLIVersion } = (await download) ?? {};
|
|
103
|
-
if (!downloadedFile) {
|
|
104
|
-
return logAndThrowError('An error occurred when pulling the latest context files. Please try again.');
|
|
100
|
+
export const run = async (runOptions) => {
|
|
101
|
+
const { args, command } = runOptions;
|
|
102
|
+
let consumer = null;
|
|
103
|
+
const filePath = args[0];
|
|
104
|
+
if (filePath) {
|
|
105
|
+
consumer = { ...rulesForDefaultConsumer, path: filePath };
|
|
105
106
|
}
|
|
106
|
-
if (
|
|
107
|
-
|
|
107
|
+
if (!consumer && process.env.AI_CONTEXT_SKIP_DETECTION !== 'true') {
|
|
108
|
+
consumer = await getPathByDetectingIDE();
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
// If this is not a file we've created, we can offer to overwrite it and
|
|
127
|
-
// preserve the existing contents by moving it to the overrides slot.
|
|
128
|
-
const { confirm } = await inquirer.prompt({
|
|
129
|
-
type: 'confirm',
|
|
130
|
-
name: 'confirm',
|
|
131
|
-
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?`,
|
|
132
|
-
default: true,
|
|
133
|
-
});
|
|
134
|
-
if (!confirm) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
// Whatever exists in the file goes in the overrides block.
|
|
138
|
-
contents = applyOverrides(remote.contents, existing.contents);
|
|
110
|
+
if (!consumer) {
|
|
111
|
+
consumer = await promptForContextConsumerSelection();
|
|
112
|
+
}
|
|
113
|
+
if (!consumer?.contextScopes) {
|
|
114
|
+
log('No context files found for this consumer. Try again or let us know if this happens again via our support channels.');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
await downloadAndWriteContextFiles(consumer, runOptions);
|
|
119
|
+
// the deprecated MCP file path
|
|
120
|
+
// let's remove that file if it exists.
|
|
121
|
+
const priorContextFilePath = resolve(command?.workingDir ?? '', consumer.path, NTL_DEV_MCP_FILE_NAME);
|
|
122
|
+
const priorExists = await getExistingContext(priorContextFilePath);
|
|
123
|
+
if (priorExists) {
|
|
124
|
+
await deleteFile(priorContextFilePath);
|
|
139
125
|
}
|
|
126
|
+
log('All context files have been added!');
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
logAndThrowError(error);
|
|
140
130
|
}
|
|
141
|
-
await writeFile(absoluteFilePath, contents);
|
|
142
|
-
log(`${existing ? 'Updated' : 'Created'} context files at ${chalk.underline(absoluteFilePath)}`);
|
|
143
131
|
};
|
|
144
132
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/recipes/ai-context/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,QAAQ,MAAM,UAAU,CAAA;AAC/B,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/recipes/ai-context/index.ts"],"names":[],"mappings":"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"}
|