UNPKG

nx

Version:

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

426 lines (425 loc) 18.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.configureAiAgentsHandler = configureAiAgentsHandler; exports.configureAiAgentsHandlerImpl = configureAiAgentsHandlerImpl; const tslib_1 = require("tslib"); const enquirer_1 = require("enquirer"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const pc = tslib_1.__importStar(require("picocolors")); const constants_1 = require("../../ai/constants"); const detect_ai_agent_1 = require("../../ai/detect-ai-agent"); const utils_1 = require("../../ai/utils"); const client_1 = require("../../daemon/client/client"); const devkit_internals_1 = require("../../devkit-internals"); const output_1 = require("../../utils/output"); const package_manager_1 = require("../../utils/package-manager"); const provenance_1 = require("../../utils/provenance"); const versions_1 = require("../../utils/versions"); const workspace_root_1 = require("../../utils/workspace-root"); const handle_import_1 = require("../../utils/handle-import"); const ora = require("ora"); async function configureAiAgentsHandler(args, inner = false) { // When called as inner from the tmp install, just run the impl directly if (inner) { return await configureAiAgentsHandlerImpl(args); } // Use environment variable to force local execution if (process.env.NX_USE_LOCAL === 'true' || process.env.NX_AI_FILES_USE_LOCAL === 'true') { await configureAiAgentsHandlerImpl(args); await resetDaemonAgentStatus(); return; } // Skip downloading latest if the current version is already the latest try { const latestVersion = await (0, package_manager_1.resolvePackageVersionUsingRegistry)('nx', 'latest'); if (latestVersion === versions_1.nxVersion) { return await configureAiAgentsHandlerImpl(args); } } catch { // If we can't check, proceed with download } let cleanup; try { await (0, provenance_1.ensurePackageHasProvenance)('nx', 'latest'); const packageInstallResults = (0, devkit_internals_1.installPackageToTmp)('nx', 'latest', (0, package_manager_1.detectPackageManager)(workspace_root_1.workspaceRoot)); cleanup = packageInstallResults.cleanup; let modulePath = require.resolve('nx/src/command-line/configure-ai-agents/configure-ai-agents.js', { paths: [packageInstallResults.tempDir] }); const module = await (0, handle_import_1.handleImport)(modulePath); await module.configureAiAgentsHandler(args, true); cleanup(); } catch (error) { if (cleanup) { cleanup(); } // Fall back to local implementation await configureAiAgentsHandlerImpl(args); } // Reset daemon cache using the local daemon client (the inner handler's // client belongs to the tmp install and isn't connected to our daemon) await resetDaemonAgentStatus(); } async function configureAiAgentsHandlerImpl(options) { // Node 24 has stricter readline behavior, and enquirer is not checking for closed state // when invoking operations, thus you get an ERR_USE_AFTER_CLOSE error. process.on('uncaughtException', (error) => { if (error && typeof error === 'object' && 'code' in error && error['code'] === 'ERR_USE_AFTER_CLOSE') return; throw error; }); const normalizedOptions = normalizeOptions(options); const { nonConfiguredAgents, partiallyConfiguredAgents, fullyConfiguredAgents, disabledAgents, } = await (0, utils_1.getAgentConfigurations)(normalizedOptions.agents, workspace_root_1.workspaceRoot); if (disabledAgents.length > 0) { const commandNames = disabledAgents.map((a) => { if (a.name === 'cursor') return '"cursor"'; if (a.name === 'copilot') return '"code"/"code-insiders"'; return a; }); const title = commandNames.length === 1 ? `${commandNames[0]} command not available.` : `CLI commands ${commandNames .map((c) => `${c}`) .join('/')} not available.`; output_1.output.log({ title, bodyLines: [ pc.dim('To manually configure the Nx MCP in your editor, install Nx Console (https://nx.dev/getting-started/editor-setup)'), ], }); } if (normalizedOptions.agents.filter((agentName) => !disabledAgents.find((a) => a.name === agentName)).length === 0) { output_1.output.error({ title: 'Please select at least one AI agent to configure.', }); process.exit(1); } // important for wording const usingAllAgents = normalizedOptions.agents.length === utils_1.supportedAgents.length; if (normalizedOptions.check) { const outOfDateAgents = fullyConfiguredAgents.filter((a) => a?.outdated); // only error if something is fully configured but outdated if (normalizedOptions.check === 'outdated') { if (fullyConfiguredAgents.length === 0) { output_1.output.log({ title: 'No AI agents are configured', bodyLines: [ 'You can configure AI agents by running `nx configure-ai-agents`.', ], }); process.exit(0); } if (outOfDateAgents.length === 0) { output_1.output.success({ title: 'All configured AI agents are up to date', bodyLines: fullyConfiguredAgents.map((a) => `- ${a.displayName}`), }); process.exit(0); } else { output_1.output.log({ title: 'The following AI agents are out of date:', bodyLines: [ ...outOfDateAgents.map((a) => { const rulesPath = a.rulesPath; const displayPath = rulesPath.startsWith(workspace_root_1.workspaceRoot) ? (0, node_path_1.relative)(workspace_root_1.workspaceRoot, rulesPath) : rulesPath; return `- ${a.displayName} (${displayPath})`; }), '', 'You can update them by running `nx configure-ai-agents`.', ], }); process.exit(1); } // error on any partial, outdated or non-configured agent } else if (normalizedOptions.check === 'all') { if (partiallyConfiguredAgents.length === 0 && outOfDateAgents.length === 0 && nonConfiguredAgents.length === 0) { output_1.output.success({ title: `All ${!usingAllAgents ? 'selected' : 'supported'} AI agents are fully configured and up to date`, bodyLines: fullyConfiguredAgents.map((a) => `- ${a.displayName}`), }); process.exit(0); } output_1.output.error({ title: 'The following agents are not fully configured or up to date:', bodyLines: [ ...partiallyConfiguredAgents, ...outOfDateAgents, ...nonConfiguredAgents, ].map((a) => getAgentChoiceForPrompt(a).message), }); process.exit(1); } } // Automatic mode (no explicit --agents): update outdated agents and report // non-configured ones. When an AI agent is detected, also configure the // detected agent itself (even if non-configured or partial). const detectedAgent = (0, detect_ai_agent_1.detectAiAgent)(); const agentsExplicitlyPassed = options.agents !== undefined; const isAutoMode = !agentsExplicitlyPassed && (options.interactive === false || detectedAgent); if (isAutoMode) { const agentsToConfig = []; const allConfigs = [ ...nonConfiguredAgents, ...partiallyConfiguredAgents, ...fullyConfiguredAgents, ]; // When an AI agent is detected, configure it if it needs it if (detectedAgent) { const detectedNeedsConfig = nonConfiguredAgents.some((a) => a.name === detectedAgent) || partiallyConfiguredAgents.some((a) => a.name === detectedAgent) || fullyConfiguredAgents.some((a) => a.name === detectedAgent && a.outdated); if (detectedNeedsConfig) { agentsToConfig.push(detectedAgent); } } // Update any other outdated agents for (const a of fullyConfiguredAgents) { if (a.outdated && !agentsToConfig.includes(a.name)) { agentsToConfig.push(a.name); } } const stillNonConfigured = nonConfiguredAgents.filter((a) => !agentsToConfig.includes(a.name)); const nothingToDoMessage = detectedAgent ? `${utils_1.agentDisplayMap[detectedAgent] ?? detectedAgent} configuration is up to date` : 'All configured AI agents are up to date'; if (agentsToConfig.length > 0) { const configSpinner = ora(`Configuring agent(s)...`).start(); try { await (0, utils_1.configureAgents)(agentsToConfig, workspace_root_1.workspaceRoot, false); configSpinner.stop(); output_1.output.success({ title: 'AI agents configured successfully', bodyLines: agentsToConfig.map((name) => { const config = allConfigs.find((a) => a.name === name); return config ? `${config.displayName}: ${getAgentConfiguredDescription(config)}` : `- ${name}`; }), }); } catch (e) { configSpinner.fail('Failed to configure AI agents'); output_1.output.error({ title: 'Error details:', bodyLines: [e.message], }); process.exit(1); } } else { output_1.output.success({ title: nothingToDoMessage, }); } if (stillNonConfigured.length > 0) { const agentNames = stillNonConfigured.map((a) => a.name); output_1.output.log({ title: 'The following agents are not yet configured:', bodyLines: [ ...stillNonConfigured.map((a) => `- ${a.displayName}`), '', `Run: nx configure-ai-agents --agents ${agentNames.join(' ')}`, ], }); } return; } // Interactive mode (or non-interactive with explicit --agents) const allAgentChoices = []; const preselectedIndices = []; let currentIndex = 0; // Partially configured agents first (highest priority) partiallyConfiguredAgents.forEach((a) => { allAgentChoices.push(getAgentChoiceForPrompt(a)); preselectedIndices.push(currentIndex); currentIndex++; }); // Outdated agents second for (const a of fullyConfiguredAgents) { if (a.outdated) { allAgentChoices.push(getAgentChoiceForPrompt(a)); preselectedIndices.push(currentIndex); currentIndex++; } } // Non-configured agents last nonConfiguredAgents.forEach((a) => { allAgentChoices.push(getAgentChoiceForPrompt(a)); currentIndex++; }); if (allAgentChoices.length === 0) { output_1.output.success({ title: `No new agents to configure. All ${!usingAllAgents ? 'selected' : 'supported'} AI agents are already configured:`, bodyLines: fullyConfiguredAgents.map((agent) => `- ${agent.displayName}`), }); process.exit(0); } let selectedAgents; if (options.interactive !== false) { try { selectedAgents = (await (0, enquirer_1.prompt)({ type: 'multiselect', name: 'agents', message: 'Which AI agents would you like to configure? (space to select, enter to confirm)', choices: allAgentChoices, initial: preselectedIndices, required: true, footer: function () { const focused = this.focused; return pc.dim(` ${getAgentFooterDescription(focused.agentConfiguration)}`); }, })).agents; } catch { process.exit(1); } } else { // non-interactive with explicit --agents: configure all requested selectedAgents = allAgentChoices.map((a) => a.name); } if (selectedAgents?.length === 0) { output_1.output.log({ title: 'No agents selected', }); process.exit(0); } const configSpinner = ora(`Configuring agent(s)...`).start(); try { await (0, utils_1.configureAgents)(selectedAgents, workspace_root_1.workspaceRoot, false); // Combine all agent configurations for display const allAgentConfigs = [ ...nonConfiguredAgents, ...partiallyConfiguredAgents, ...fullyConfiguredAgents, ]; const configuredOrUpdatedAgents = allAgentConfigs.filter((a) => selectedAgents.includes(a.name) || fullyConfiguredAgents.some((f) => f.name === a.name)); configSpinner.stop(); output_1.output.success({ title: 'AI agents configured successfully', bodyLines: configuredOrUpdatedAgents.map((agent) => `${agent.displayName}: ${getAgentConfiguredDescription(agent)}`), }); return; } catch (e) { configSpinner.fail('Failed to set up AI agents'); output_1.output.error({ title: 'Error details:', bodyLines: [e.message], }); process.exit(1); } } /** * Get the verbose footer description for an agent. * Describes the end state per agent type. */ function getAgentFooterDescription(agent) { // Extract filename from rulesPath const rulesFile = agent.rulesPath.split('/').pop() || 'AGENTS.md'; switch (agent.name) { case 'claude': { let description = `Installs Nx plugin (MCP + skills + agents). Updates ${rulesFile}.`; // Check if .mcp.json exists with nx-mcp - if so, mention cleanup const mcpJsonPath = (0, constants_1.claudeMcpJsonPath)(workspace_root_1.workspaceRoot); if ((0, node_fs_1.existsSync)(mcpJsonPath)) { try { const mcpJsonContents = JSON.parse((0, node_fs_1.readFileSync)(mcpJsonPath, 'utf-8')); if (mcpJsonContents?.mcpServers?.['nx-mcp']) { description += ' Removes nx-mcp from .mcp.json (now handled by plugin).'; } } catch { // Ignore errors reading .mcp.json } } return description; } case 'cursor': case 'copilot': return `Installs Nx Console (MCP). Adds skills and agents. Updates ${rulesFile}.`; case 'gemini': case 'opencode': return `Configures MCP server. Adds skills and agents. Updates ${rulesFile}.`; case 'codex': return `Configures MCP server. Adds skills. Updates ${rulesFile}.`; default: return ''; } } /** * Get a compact description of what was configured for an agent. * Used in the post-configuration output. */ function getAgentConfiguredDescription(agent) { // Extract filename from rulesPath const rulesFile = agent.rulesPath.split('/').pop() || 'AGENTS.md'; switch (agent.name) { case 'claude': return `Nx plugin (MCP + skills + agents) + ${rulesFile}`; case 'cursor': case 'copilot': return `Nx Console (MCP) + skills + ${rulesFile}`; case 'gemini': case 'opencode': return `MCP + skills + ${rulesFile}`; case 'codex': return `MCP + skills + ${rulesFile}`; default: return ''; } } function getAgentChoiceForPrompt(agent) { const partiallyConfigured = agent.mcp !== agent.rules; const needsUpdate = partiallyConfigured || agent.outdated; return { name: agent.name, message: needsUpdate ? `${agent.displayName} (update available)` : agent.displayName, agentConfiguration: agent, }; } function normalizeOptions(options) { const agents = (options.agents ?? utils_1.supportedAgents).filter((a) => utils_1.supportedAgents.includes(a)); // it used to be just --check which was implicitly 'outdated' const check = (options.check === true ? 'outdated' : options.check) ?? false; return { ...options, agents, check, }; } async function resetDaemonAgentStatus() { try { // Don't check daemonClient.enabled() — the CLI sets NX_DAEMON=false for // configure-ai-agents (it doesn't need the daemon to do its work), but a // daemon started by a previous command may still be running and serving // cached status. We just need to reach it to reset its cache. if (await client_1.daemonClient.isServerAvailable()) { await client_1.daemonClient.resetConfigureAiAgentsStatus(); } } catch { // Daemon may not be running, that's fine } finally { // Close the daemon socket so the process can exit cleanly. client_1.daemonClient.reset(); } }