UNPKG

netlify

Version:

Netlify command line tool

132 lines 5.53 kB
import { resolve } from 'node:path'; import inquirer from 'inquirer'; import execa from 'execa'; import { logAndThrowError, log, version } from '../../utils/command-helpers.js'; import { getExistingContext, NTL_DEV_MCP_FILE_NAME, getContextConsumers, deleteFile, downloadAndWriteContextFiles, } from './context.js'; export const description = 'Manage context files for AI tools'; // context consumers endpoints returns all supported IDE and other consumers // that can be used to pull context files. It also includes a catchall consumer // for outlining all context that an unspecified consumer would handle. const allContextConsumers = await getContextConsumers(version); const cliContextConsumers = allContextConsumers.filter((consumer) => !consumer.hideFromCLI); const rulesForDefaultConsumer = allContextConsumers.find((consumer) => consumer.key === 'catchall-consumer') ?? { key: 'catchall-consumer', path: './ai-context', presentedName: '', ext: 'mdc', contextScopes: {}, hideFromCLI: true, }; const presets = cliContextConsumers.map((consumer) => ({ name: consumer.presentedName, value: consumer.key, })); // always add the custom location option (not preset from API) presets.push({ name: 'Custom location', value: rulesForDefaultConsumer.key }); const promptForContextConsumerSelection = async () => { const { consumerKey } = await inquirer.prompt([ { name: 'consumerKey', message: 'Where should we put the context files?', type: 'list', choices: presets, }, ]); const contextConsumer = consumerKey ? cliContextConsumers.find((consumer) => consumer.key === consumerKey) : null; if (contextConsumer) { return contextConsumer; } const { customPath } = await inquirer.prompt([ { type: 'input', name: 'customPath', message: 'Enter the path, relative to the project root, where the context files should be placed', default: './ai-context', }, ]); if (customPath) { return { ...rulesForDefaultConsumer, path: customPath || rulesForDefaultConsumer.path }; } log('You must select a path.'); return promptForContextConsumerSelection(); }; /** * Checks if a command belongs to a known IDEs by checking if it includes a specific string. * For example, the command that starts windsurf looks something like "/applications/windsurf.app/contents/...". */ const getConsumerKeyFromCommand = (command) => { // The actual command is something like "/applications/windsurf.app/contents/...", but we are only looking for windsurf const match = cliContextConsumers.find((consumer) => consumer.consumerProcessCmd && command.includes(consumer.consumerProcessCmd)); return match ? match.key : null; }; /** * 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. */ const getCommandAndParentPID = async (pid) => { const { stdout } = await execa('ps', ['-p', String(pid), '-o', 'ppid=,comm=']); const output = stdout.trim(); const spaceIndex = output.indexOf(' '); const parentPID = output.substring(0, spaceIndex); const command = output.substring(spaceIndex + 1).toLowerCase(); return { parentPID: Number(parentPID), command, consumerKey: getConsumerKeyFromCommand(command), }; }; const getPathByDetectingIDE = async () => { // Go up the chain of ancestor process IDs and find if one of their commands matches an IDE. const ppid = process.ppid; let result; try { result = await getCommandAndParentPID(ppid); while (result.parentPID !== 1 && !result.consumerKey) { result = await getCommandAndParentPID(result.parentPID); } } catch { // The command "ps -p {pid} -o ppid=,comm=" didn't work, // perhaps we are on a machine that doesn't support it. return null; } if (result?.consumerKey) { const contextConsumer = cliContextConsumers.find((consumer) => consumer.key === result.consumerKey); if (contextConsumer) { return contextConsumer; } } return null; }; export const run = async (runOptions) => { const { args, command } = runOptions; let consumer = null; const filePath = args[0]; if (filePath) { consumer = { ...rulesForDefaultConsumer, path: filePath }; } if (!consumer && process.env.AI_CONTEXT_SKIP_DETECTION !== 'true') { consumer = await getPathByDetectingIDE(); } if (!consumer) { consumer = await promptForContextConsumerSelection(); } if (!consumer?.contextScopes) { log('No context files found for this consumer. Try again or let us know if this happens again via our support channels.'); return; } try { await downloadAndWriteContextFiles(consumer, runOptions); // the deprecated MCP file path // let's remove that file if it exists. const priorContextFilePath = resolve(command?.workingDir ?? '', consumer.path, NTL_DEV_MCP_FILE_NAME); const priorExists = await getExistingContext(priorContextFilePath); if (priorExists) { await deleteFile(priorContextFilePath); } log('All context files have been added!'); } catch (error) { logAndThrowError(error); } }; //# sourceMappingURL=index.js.map