UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

400 lines (399 loc) • 17.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setupAiAgentsGenerator = setupAiAgentsGenerator; exports.setupAiAgentsGeneratorImpl = setupAiAgentsGeneratorImpl; const fs_1 = require("fs"); const os_1 = require("os"); const path_1 = require("path"); const semver_1 = require("semver"); const format_changed_files_with_prettier_if_available_1 = require("../../generators/internal-utils/format-changed-files-with-prettier-if-available"); const generate_files_1 = require("../../generators/utils/generate-files"); const json_1 = require("../../generators/utils/json"); const native_1 = require("../../native"); const package_json_1 = require("../../utils/package-json"); const provenance_1 = require("../../utils/provenance"); const workspace_root_1 = require("../../utils/workspace-root"); const constants_1 = require("../constants"); const clone_ai_config_repo_1 = require("../clone-ai-config-repo"); const utils_1 = require("../utils"); const constants_2 = require("../constants"); /** * Get the installed Nx version, with fallback to workspace package.json or default version. */ function getNxVersion() { try { // Try to read from node_modules first const { packageJson: { version }, } = (0, package_json_1.readModulePackageJson)('nx'); return version; } catch { try { // Fallback: try to read from workspace package.json const workspacePackageJson = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(workspace_root_1.workspaceRoot, 'package.json'), 'utf-8')); // Check devDependencies first, then dependencies const nxVersion = workspacePackageJson.devDependencies?.nx || workspacePackageJson.dependencies?.nx; if (nxVersion) { // Remove any semver range characters (^, ~, >=, etc.) return nxVersion.replace(/^[\^~>=<]+/, ''); } throw new Error('Nx not found in package.json'); } catch { // If we can't determine the version, default to the newer format // This handles cases where nx might not be installed or is globally installed return '22.0.0'; } } } async function setupAiAgentsGenerator(tree, options, inner = false) { const normalizedOptions = normalizeOptions(options); // Use environment variable to force local execution if (process.env.NX_AI_FILES_USE_LOCAL === 'true' || inner) { return await setupAiAgentsGeneratorImpl(tree, normalizedOptions); } try { await (0, provenance_1.ensurePackageHasProvenance)('nx', normalizedOptions.packageVersion); const { tempDir, cleanup } = (0, package_json_1.installPackageToTmp)('nx', normalizedOptions.packageVersion); let modulePath = (0, path_1.join)(tempDir, 'node_modules', 'nx', 'src/ai/set-up-ai-agents/set-up-ai-agents.js'); const module = await Promise.resolve(`${modulePath}`).then(s => require(s)); const setupAiAgentsGeneratorResult = await module.setupAiAgentsGenerator(tree, normalizedOptions, true); cleanup(); return setupAiAgentsGeneratorResult; } catch (error) { return await setupAiAgentsGeneratorImpl(tree, normalizedOptions); } } function normalizeOptions(options) { return { directory: options.directory, writeNxCloudRules: options.writeNxCloudRules ?? false, packageVersion: options.packageVersion ?? 'latest', agents: options.agents ?? [...utils_1.supportedAgents], }; } async function setupAiAgentsGeneratorImpl(tree, options) { const hasAgent = (agent) => options.agents.includes(agent); const nxVersion = getNxVersion(); const agentsMd = (0, constants_1.agentsMdPath)(options.directory); // write AGENTS.md for most agents if (hasAgent('cursor') || hasAgent('copilot') || hasAgent('codex') || hasAgent('opencode')) { writeAgentRules(tree, agentsMd, options.writeNxCloudRules); } if (hasAgent('claude')) { const claudePath = (0, path_1.join)(options.directory, 'CLAUDE.md'); writeAgentRules(tree, claudePath, options.writeNxCloudRules); // Configure Claude plugin via marketplace (plugin includes MCP server) const claudeSettingsPath = (0, path_1.join)(options.directory, '.claude', 'settings.json'); if (!tree.exists(claudeSettingsPath)) { (0, json_1.writeJson)(tree, claudeSettingsPath, {}); } (0, json_1.updateJson)(tree, claudeSettingsPath, (json) => ({ ...json, extraKnownMarketplaces: { ...json.extraKnownMarketplaces, 'nx-claude-plugins': { source: { source: 'github', repo: 'nrwl/nx-ai-agents-config', }, }, }, enabledPlugins: { ...json.enabledPlugins, 'nx@nx-claude-plugins': true, }, })); // Clean up .mcp.json (nx-mcp now handled by plugin) const mcpJsonPath = (0, constants_1.claudeMcpJsonPath)(options.directory); if (tree.exists(mcpJsonPath)) { try { const mcpJsonContents = (0, json_1.readJson)(tree, mcpJsonPath); if (mcpJsonContents?.mcpServers?.['nx-mcp']) { const serverKeys = Object.keys(mcpJsonContents.mcpServers || {}); if (serverKeys.length === 1 && serverKeys[0] === 'nx-mcp') { // nx-mcp is the only server, delete the file tree.delete(mcpJsonPath); } else { // Other servers exist, just remove nx-mcp entry delete mcpJsonContents.mcpServers['nx-mcp']; (0, json_1.writeJson)(tree, mcpJsonPath, mcpJsonContents); } } } catch { // Ignore errors reading .mcp.json } } } if (hasAgent('opencode')) { const opencodeMcpJsonPath = (0, constants_1.opencodeMcpPath)(options.directory); if (!tree.exists(opencodeMcpJsonPath)) { (0, json_1.writeJson)(tree, opencodeMcpJsonPath, {}); } (0, json_1.updateJson)(tree, opencodeMcpJsonPath, (json) => opencodeMcpConfigUpdater(json, nxVersion)); } if (hasAgent('gemini')) { const geminiSettingsPath = (0, path_1.join)(options.directory, '.gemini', 'settings.json'); if (!tree.exists(geminiSettingsPath)) { (0, json_1.writeJson)(tree, geminiSettingsPath, {}); } (0, json_1.updateJson)(tree, geminiSettingsPath, (json) => mcpConfigUpdater(json, nxVersion)); const contextFileName = (0, json_1.readJson)(tree, geminiSettingsPath).contextFileName; const geminiMd = (0, constants_1.geminiMdPath)(options.directory); // Only set contextFileName to AGENTS.md if GEMINI.md doesn't exist already to preserve existing setups if (!contextFileName && !tree.exists(geminiMd)) { writeAgentRules(tree, agentsMd, options.writeNxCloudRules); (0, json_1.updateJson)(tree, geminiSettingsPath, (json) => ({ ...json, contextFileName: 'AGENTS.md', })); } else { writeAgentRules(tree, contextFileName ?? geminiMd, options.writeNxCloudRules); } } // Copy extensibility artifacts (commands, skills, subagents) for non-Claude agents if (hasAgent('opencode') || hasAgent('copilot') || hasAgent('cursor') || hasAgent('codex') || hasAgent('gemini')) { const repoPath = (0, clone_ai_config_repo_1.getAiConfigRepoPath)(); const agentDirs = [ { agent: 'opencode', src: 'generated/.opencode', dest: '.opencode' }, { agent: 'copilot', src: 'generated/.github', dest: '.github' }, { agent: 'cursor', src: 'generated/.cursor', dest: '.cursor' }, { agent: 'codex', src: 'generated/.codex', dest: '.codex' }, { agent: 'gemini', src: 'generated/.gemini', dest: '.gemini' }, ]; for (const { agent, src, dest } of agentDirs) { if (hasAgent(agent)) { const srcPath = (0, path_1.join)(repoPath, src); if ((0, fs_1.existsSync)(srcPath)) { (0, generate_files_1.generateFiles)(tree, srcPath, (0, path_1.join)(options.directory, dest), {}); } } } } await (0, format_changed_files_with_prettier_if_available_1.formatChangedFilesWithPrettierIfAvailable)(tree); // we use the check variable to determine if we should actually make changes or just report what would be changed return async (check = false) => { const messages = []; const errors = []; if (hasAgent('codex')) { if ((0, fs_1.existsSync)(constants_1.codexConfigTomlPath)) { const tomlContents = (0, fs_1.readFileSync)(constants_1.codexConfigTomlPath, 'utf-8'); if (!tomlContents.includes(constants_2.nxMcpTomlHeader)) { if (!check) { (0, fs_1.appendFileSync)(constants_1.codexConfigTomlPath, `\n${(0, constants_2.getNxMcpTomlConfig)(nxVersion)}`); } messages.push({ title: `Updated ${constants_1.codexConfigTomlPath} with nx-mcp server`, }); } } else { if (!check) { (0, fs_1.mkdirSync)((0, path_1.join)((0, os_1.homedir)(), '.codex'), { recursive: true }); (0, fs_1.writeFileSync)(constants_1.codexConfigTomlPath, (0, constants_2.getNxMcpTomlConfig)(nxVersion)); } messages.push({ title: `Created ${constants_1.codexConfigTomlPath} with nx-mcp server`, }); } } if (hasAgent('copilot')) { try { if ((0, native_1.isEditorInstalled)(0 /* SupportedEditor.VSCode */) && (0, native_1.canInstallNxConsoleForEditor)(0 /* SupportedEditor.VSCode */)) { if (!check) { (0, native_1.installNxConsoleForEditor)(0 /* SupportedEditor.VSCode */); } messages.push({ title: `Installed Nx Console for VSCode`, }); } } catch (e) { errors.push({ title: `Failed to install Nx Console for VSCode. Please install it manually.`, bodyLines: [e.message], }); } try { if ((0, native_1.isEditorInstalled)(1 /* SupportedEditor.VSCodeInsiders */) && (0, native_1.canInstallNxConsoleForEditor)(1 /* SupportedEditor.VSCodeInsiders */)) { if (!check) { (0, native_1.installNxConsoleForEditor)(1 /* SupportedEditor.VSCodeInsiders */); } messages.push({ title: `Installed Nx Console for VSCode Insiders`, }); } } catch (e) { errors.push({ title: `Failed to install Nx Console for VSCode Insiders. Please install it manually.`, bodyLines: [e.message], }); } } if (hasAgent('cursor')) { try { if ((0, native_1.isEditorInstalled)(2 /* SupportedEditor.Cursor */) && (0, native_1.canInstallNxConsoleForEditor)(2 /* SupportedEditor.Cursor */)) { if (!check) { (0, native_1.installNxConsoleForEditor)(2 /* SupportedEditor.Cursor */); } messages.push({ title: `Installed Nx Console for Cursor`, }); } } catch (e) { errors.push({ title: `Failed to install Nx Console for Cursor. Please install it manually.`, bodyLines: [e.message], }); } } return { messages, errors, }; }; } function writeAgentRules(tree, path, writeNxCloudRules) { if (!tree.exists(path)) { // File doesn't exist - create with h1 header (standalone content) const expectedRules = (0, constants_2.getAgentRulesWrapped)({ writeNxCloudRules, useH1: true, }); tree.write(path, expectedRules); return; } const existing = tree.read(path, 'utf-8'); const regex = constants_2.rulesRegex; const existingNxConfiguration = existing.match(regex); if (existingNxConfiguration) { // Check the rest of the file (outside nx block) for an h1 header // to ensure only one h1 exists in the document const contentWithoutNxBlock = existing.replace(regex, ''); const hasExternalH1 = /^# /m.test(contentWithoutNxBlock); const expectedRules = (0, constants_2.getAgentRulesWrapped)({ writeNxCloudRules, useH1: !hasExternalH1, }); const contentOnly = (str) => str .replace(constants_2.nxRulesMarkerCommentStart, '') .replace(constants_2.nxRulesMarkerCommentEnd, '') .replace(constants_2.nxRulesMarkerCommentDescription, '') .replace(/\s/g, ''); // we don't want to make updates on whitespace-only changes if (contentOnly(existingNxConfiguration[0]) === contentOnly(expectedRules)) { return; } // otherwise replace the existing configuration const updatedContent = existing.replace(regex, expectedRules); tree.write(path, updatedContent); } else { // Appending to existing content - use h2 only if the file already has an h1 header // This prevents unnecessary changes when users add content without their own h1 const hasExistingH1 = /^# /m.test(existing); const expectedRules = (0, constants_2.getAgentRulesWrapped)({ writeNxCloudRules, useH1: !hasExistingH1, }); tree.write(path, existing + '\n\n' + expectedRules); } } /** * Extract user-added extra args/flags from an existing MCP config args array * by stripping the known base command prefix. * * Known base patterns (matched in order, first wins): * ['nx', 'mcp'] or ['nx-mcp'] (possibly with @version suffix like nx-mcp@latest) * For opencode the caller prepends 'npx' to these patterns. */ function getExtraMcpArgs(existingArgs, knownBasePatterns) { if (!Array.isArray(existingArgs) || existingArgs.length === 0) return []; for (const pattern of knownBasePatterns) { if (existingArgs.length < pattern.length) continue; const matches = pattern.every((baseArg, i) => { if (baseArg === 'nx-mcp') { // Also match versioned variants like nx-mcp@latest return (existingArgs[i] === 'nx-mcp' || existingArgs[i].startsWith('nx-mcp@')); } return existingArgs[i] === baseArg; }); if (matches) { return existingArgs.slice(pattern.length); } } return []; } function mcpConfigUpdater(existing, nxVersion) { const majorVersion = (0, semver_1.major)(nxVersion); const mcpArgs = majorVersion >= 22 ? ['nx', 'mcp'] : ['nx-mcp']; // Preserve any extra args (e.g. --experimental-polygraph, --transport http) from existing config const extraArgs = getExtraMcpArgs(existing.mcpServers?.['nx-mcp']?.args, [ ['nx', 'mcp'], ['nx-mcp'], ]); mcpArgs.push(...extraArgs); if (existing.mcpServers) { existing.mcpServers['nx-mcp'] = { type: 'stdio', command: 'npx', args: mcpArgs, }; } else { existing.mcpServers = { 'nx-mcp': { type: 'stdio', command: 'npx', args: mcpArgs, }, }; } return existing; } function opencodeMcpConfigUpdater(existing, nxVersion) { const majorVersion = (0, semver_1.major)(nxVersion); const mcpCommand = majorVersion >= 22 ? ['npx', 'nx', 'mcp'] : ['npx', 'nx-mcp']; // Preserve any extra args (e.g. --experimental-polygraph, --transport http) from existing config const extraArgs = getExtraMcpArgs(existing.mcp?.['nx-mcp']?.command, [ ['npx', 'nx', 'mcp'], ['npx', 'nx-mcp'], ]); mcpCommand.push(...extraArgs); if (existing.mcp) { existing.mcp['nx-mcp'] = { type: 'local', command: mcpCommand, enabled: true, }; } else { existing.mcp = { 'nx-mcp': { type: 'local', command: mcpCommand, enabled: true, }, }; } return existing; } exports.default = setupAiAgentsGenerator;