diff --git a/e2e/nx/src/configure-ai-agents.test.ts b/e2e/nx/src/configure-ai-agents.test.ts index 7e3077bb8e595e..ebf2dff605f2fe 100644 --- a/e2e/nx/src/configure-ai-agents.test.ts +++ b/e2e/nx/src/configure-ai-agents.test.ts @@ -9,8 +9,14 @@ import { } from '@nx/e2e-utils'; describe('configure-ai-agents', () => { - beforeAll(() => newProject({ packages: ['@nx/web', '@nx/js', '@nx/react'] })); - afterAll(() => cleanupProject()); + beforeAll(() => { + process.env.NX_USE_LOCAL = 'true'; + newProject({ packages: ['@nx/web', '@nx/js', '@nx/react'] }); + }); + afterAll(() => { + delete process.env.NX_USE_LOCAL; + cleanupProject(); + }); it('should generate local configuration', () => { runCLI(`configure-ai-agents --agents claude --no-interactive`); @@ -184,4 +190,244 @@ describe('configure-ai-agents', () => { expect(skillsContents.length).toBeGreaterThan(0); }); }); + + describe('--no-interactive without --agents (AI agent-like mode)', () => { + // Without explicit --agents, --no-interactive should behave like AI agent + // mode: update outdated agents only, report non-configured ones. + + beforeAll(() => { + // Start clean: remove all agent configs + removeFile('CLAUDE.md'); + removeFile('.claude'); + removeFile('.gemini'); + removeFile('AGENTS.md'); + removeFile('opencode.json'); + removeFile('.opencode'); + removeFile('.codex'); + }); + + it('should report all agents as not yet configured on a clean workspace', () => { + const output = runCLI('configure-ai-agents --no-interactive'); + + // Nothing should have been configured + expect(() => readFile('CLAUDE.md')).toThrow(); + + // Should report non-configured agents + expect(output).toContain('not yet configured'); + expect(output).toContain('configure-ai-agents --agents'); + }); + + it('should update outdated agents and report non-configured ones', () => { + // Configure claude first + runCLI('configure-ai-agents --agents claude --no-interactive'); + expect(readFile('CLAUDE.md')).toContain('# General Guidelines'); + + // Make it outdated + updateFile('CLAUDE.md', (content: string) => + content.replace('nx_docs', 'nx_docs_outdated') + ); + + const output = runCLI('configure-ai-agents --no-interactive'); + + // Should have updated the outdated claude config + expect(readFile('CLAUDE.md')).not.toContain('nx_docs_outdated'); + expect(output).toContain('configured successfully'); + + // Should report non-configured agents + expect(output).toContain('not yet configured'); + }); + + it('should report up-to-date when all configured agents are current', () => { + // Claude is already fully configured from previous test + const output = runCLI('configure-ai-agents --no-interactive'); + + expect(output).toContain('up to date'); + }); + + it('should not configure partially configured agents', () => { + // Remove the rules file to make claude partially configured + removeFile('CLAUDE.md'); + + const output = runCLI('configure-ai-agents --no-interactive'); + + // Should NOT have restored the missing rules file — partial agents + // are not auto-configured (same as AI agent mode for non-detected agents) + expect(() => readFile('CLAUDE.md')).toThrow(); + expect(output).toContain('up to date'); + + // Restore for subsequent tests + runCLI('configure-ai-agents --agents claude --no-interactive'); + }); + }); + + describe('when running from a detected AI agent', () => { + // Simulate Claude Code detection via CLAUDECODE env var. + // No --no-interactive flag, no --agents flag: the command auto-detects + // which agent is calling it and configures accordingly. + const claudeEnv = { CLAUDECODE: '1' }; + + beforeAll(() => { + // Start clean: remove all agent configs + removeFile('CLAUDE.md'); + removeFile('.claude'); + removeFile('.gemini'); + }); + + it('should configure the detected agent without prompting on a clean workspace', () => { + // No --agents flag — the command detects claude via CLAUDECODE env var + const output = runCLI('configure-ai-agents', { + env: claudeEnv, + }); + + // Should have configured claude (the detected agent) + expect(readFile('CLAUDE.md')).toContain('# General Guidelines'); + expect(readFile('.claude/settings.json')).toContain('nx-claude-plugins'); + + // Should mention other unconfigured agents with a command to configure them + expect(output).toContain('not yet configured'); + expect(output).toContain('configure-ai-agents --agents'); + }); + + it('should report up-to-date when the detected agent is fully configured', () => { + // Claude is already fully configured from previous test + const output = runCLI('configure-ai-agents', { + env: claudeEnv, + }); + + expect(output).toContain('up to date'); + }); + + it('should update the detected agent when its rules are outdated', () => { + // Make claude outdated + updateFile('CLAUDE.md', (content: string) => + content.replace('nx_docs', 'nx_docs_outdated') + ); + + const output = runCLI('configure-ai-agents', { + env: claudeEnv, + }); + + // Should have updated claude's rules + expect(readFile('CLAUDE.md')).not.toContain('nx_docs_outdated'); + expect(output).toContain('configured successfully'); + }); + + it('should update other outdated agents alongside the detected agent', () => { + // Configure gemini fully first + runCLI('configure-ai-agents --agents gemini --no-interactive'); + expect(readFile('AGENTS.md')).toBeTruthy(); + + // Make gemini outdated + updateFile('AGENTS.md', (content: string) => + content.replace('nx_docs', 'nx_docs_outdated') + ); + + // Run from detected claude agent — should also update outdated gemini + const output = runCLI('configure-ai-agents', { + env: claudeEnv, + }); + + // Gemini should have been updated because it was outdated + expect(readFile('AGENTS.md')).not.toContain('nx_docs_outdated'); + expect(output).toContain('configured successfully'); + }); + + it('should list non-configured agents with a suggested command', () => { + // Remove gemini config to make it non-configured + // Note: .gemini removal is sufficient — without settings.json, gemini is non-configured + removeFile('.gemini'); + + const output = runCLI('configure-ai-agents', { + env: claudeEnv, + }); + + // Claude is up-to-date, gemini is non-configured + // Should suggest running configure-ai-agents for gemini + expect(output).toContain('not yet configured'); + expect(output).toContain('gemini'); + expect(output).toContain('configure-ai-agents --agents'); + }); + + it('should ignore the detected agent when --agents is explicitly passed', () => { + // Clean up both agents + removeFile('CLAUDE.md'); + removeFile('.claude'); + removeFile('.gemini'); + + // Explicitly pass --agents gemini — should ONLY configure gemini, + // ignoring the detected claude agent entirely + runCLI('configure-ai-agents --agents gemini --no-interactive', { + env: claudeEnv, + }); + + // Gemini should be configured because it was explicitly requested + expect(readFile('AGENTS.md')).toContain('# General Guidelines'); + + // Claude should NOT be configured — --agents overrides detection + expect(() => readFile('CLAUDE.md')).toThrow(); + }); + + it('should throw with --check when the detected agent is outdated', () => { + // Restore claude config (previous test removed it) + runCLI('configure-ai-agents --agents claude --no-interactive'); + + // Make claude outdated + updateFile('CLAUDE.md', (content: string) => + content.replace('nx_docs', 'nx_docs_outdated') + ); + + let didThrow = false; + try { + runCLI('configure-ai-agents --check', { + env: claudeEnv, + }); + } catch { + didThrow = true; + } + expect(didThrow).toBe(true); + + // Restore + runCLI('configure-ai-agents --agents claude --no-interactive'); + }); + + it('should throw with --check when a non-detected agent is outdated', () => { + // Configure gemini fully + runCLI('configure-ai-agents --agents gemini --no-interactive'); + + // Make gemini outdated while claude stays up-to-date + updateFile('AGENTS.md', (content: string) => + content.replace('nx_docs', 'nx_docs_outdated') + ); + + let didThrow = false; + try { + runCLI('configure-ai-agents --check', { + env: claudeEnv, + }); + } catch { + didThrow = true; + } + expect(didThrow).toBe(true); + + // Restore + runCLI('configure-ai-agents --agents gemini --no-interactive'); + }); + + it('should not throw with --check when --agents scopes to up-to-date agents only', () => { + // Make claude outdated + updateFile('CLAUDE.md', (content: string) => + content.replace('nx_docs', 'nx_docs_outdated') + ); + + // --agents gemini --check should only check gemini, ignoring + // the detected (and outdated) claude entirely + const output = runCLI('configure-ai-agents --agents gemini --check', { + env: claudeEnv, + }); + expect(output).toContain('up to date'); + + // Restore + runCLI('configure-ai-agents --agents claude --no-interactive'); + }); + }); }); diff --git a/e2e/utils/get-env-info.ts b/e2e/utils/get-env-info.ts index 27b03671872edf..8d2f4e2a1a132e 100644 --- a/e2e/utils/get-env-info.ts +++ b/e2e/utils/get-env-info.ts @@ -152,6 +152,7 @@ export function getStrippedEnvironmentVariables() { 'NX_ISOLATE_PLUGINS', 'NX_VERBOSE_LOGGING', 'NX_NATIVE_LOGGING', + 'NX_USE_LOCAL', ]; if (key.startsWith('NX_') && !allowedKeys.includes(key)) { @@ -169,6 +170,22 @@ export function getStrippedEnvironmentVariables() { return false; } + // Remove AI agent detection env vars to prevent the test runner's + // environment (e.g., running inside Claude Code) from leaking into + // e2e test subprocesses. Tests that need these pass them explicitly. + const aiAgentEnvVars = [ + 'CLAUDECODE', + 'CLAUDE_CODE', + 'OPENCODE', + 'GEMINI_CLI', + 'CURSOR_TRACE_ID', + 'COMPOSER_NO_INTERACTION', + 'REPL_ID', + ]; + if (aiAgentEnvVars.includes(key)) { + return false; + } + return true; }) ); diff --git a/packages/nx/src/command-line/configure-ai-agents/configure-ai-agents.ts b/packages/nx/src/command-line/configure-ai-agents/configure-ai-agents.ts index a52cc701c12beb..009b5aaa9d3009 100644 --- a/packages/nx/src/command-line/configure-ai-agents/configure-ai-agents.ts +++ b/packages/nx/src/command-line/configure-ai-agents/configure-ai-agents.ts @@ -3,8 +3,10 @@ import { existsSync, readFileSync } from 'node:fs'; import { relative } from 'node:path'; import * as pc from 'picocolors'; import { claudeMcpJsonPath } from '../../ai/constants'; +import { detectAiAgent } from '../../ai/detect-ai-agent'; import { Agent, + agentDisplayMap, AgentConfiguration, configureAgents, getAgentConfigurations, @@ -208,6 +210,99 @@ export async function configureAiAgentsHandlerImpl( 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 = detectAiAgent(); + const agentsExplicitlyPassed = options.agents !== undefined; + const isAutoMode = + !agentsExplicitlyPassed && (options.interactive === false || detectedAgent); + + if (isAutoMode) { + const agentsToConfig: Agent[] = []; + 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 + ? `${ + 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 configureAgents(agentsToConfig, workspaceRoot, false); + configSpinner.stop(); + + 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.error({ + title: 'Error details:', + bodyLines: [e.message], + }); + process.exit(1); + } + } else { + output.success({ + title: nothingToDoMessage, + }); + } + + if (stillNonConfigured.length > 0) { + const agentNames = stillNonConfigured.map((a) => a.name); + 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: AgentPromptChoice[] = []; const preselectedIndices: number[] = []; let currentIndex = 0; @@ -268,7 +363,7 @@ export async function configureAiAgentsHandlerImpl( process.exit(1); } } else { - // in non-interactive mode, configure all + // non-interactive with explicit --agents: configure all requested selectedAgents = allAgentChoices.map((a) => a.name); }