diff --git a/.vscode/launch.json b/.vscode/launch.json index e3596beae..8b2d31748 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -80,12 +80,11 @@ "program": "${workspaceRoot}/packages/cli/bin/execute.js", "console": "externalTerminal", "preLaunchTask": "build", - "outFiles": [ "${workspaceRoot}/lib/**/*.js", - "${workspaceRoot}/spec/**/*.js" ], "args": [ "new", "angularproj", - "--framework=angular" + "--framework=angular", + "--skip-install" ] }, { @@ -298,7 +297,7 @@ "preLaunchTask": "build", "outFiles": ["${workspaceFolder}/**/*.js"], "args": [ - "list" + "ai-config" ] }, { "type": "node", diff --git a/packages/cli/lib/commands/ai-config.ts b/packages/cli/lib/commands/ai-config.ts index 20e697d39..0e0c95209 100644 --- a/packages/cli/lib/commands/ai-config.ts +++ b/packages/cli/lib/commands/ai-config.ts @@ -1,14 +1,17 @@ -import { addMcpServers, AI_AGENT_LABELS, AI_AGENT_CHOICES, AIAgentTarget, copyAgentInstructionFiles, copyAISkillsToProject, GoogleAnalytics, InquirerWrapper, Util, VS_CODE_MCP_PATH } from "@igniteui/cli-core"; +import { addMcpServers, AI_AGENT_LABELS, AI_AGENT_CHOICES, AIAgentTarget, copyAgentInstructionFiles, copyAISkillsToProject, GoogleAnalytics, InquirerWrapper, Util, AiCodingAssistant, AI_ASSISTANT_MCP_CONFIGS, AI_ASSISTANT_CHOICES, AI_ASSISTANT_LABELS } from "@igniteui/cli-core"; import { ArgumentsCamelCase, CommandModule } from "yargs"; -export function configureMCP(): void { - const modified = addMcpServers(VS_CODE_MCP_PATH); +export function configureMCP(assistants: AiCodingAssistant[] = ["vscode"]): void { + for (const assistant of assistants) { + const { mcpFilePath } = AI_ASSISTANT_MCP_CONFIGS[assistant]; + const modified = addMcpServers(assistant); - if (!modified) { - Util.log(` Ignite UI MCP servers already configured in ${VS_CODE_MCP_PATH}`); - return; + if (!modified) { + Util.log(` Ignite UI MCP servers already configured in ${mcpFilePath}`); + } else { + Util.log(Util.greenCheck() + ` MCP servers configured in ${mcpFilePath}`); + } } - Util.log(Util.greenCheck() + ` MCP servers configured in ${VS_CODE_MCP_PATH}`); } export function configureSkills(agents: AIAgentTarget[]): void { @@ -26,20 +29,28 @@ export function configureSkills(agents: AIAgentTarget[]): void { } } -export async function configure(agents?: AIAgentTarget[], skills = true): Promise { +export async function configure(agents?: AIAgentTarget[], skills = true, assistants?: AiCodingAssistant[]): Promise { if (!agents?.length) { agents = await promptForAgents(); } - if (!agents.length) return; - configureMCP(); + + if (!assistants?.length) { + assistants = await promptForAssistant(); + } + configureMCP(assistants); + if (skills) { configureSkills(agents); } copyAgentInstructionFiles(agents); } + const AI_AGENT_CHECKBOX_DEFAULTS: AIAgentTarget[] = ["generic", "claude"]; + +const AI_ASSISTANT_CHECKBOX_DEFAULTS: AiCodingAssistant[] = ["vscode", "claude-code"]; + const AI_AGENT_CHECKBOX_CHOICES = [ - { value: "none", name: "None (skip AI configuration)" }, + { value: "none", name: "None (skip skills and instructions)" }, ...AI_AGENT_CHOICES.map(agent => ({ value: agent, name: AI_AGENT_LABELS[agent], @@ -47,11 +58,20 @@ const AI_AGENT_CHECKBOX_CHOICES = [ })) ]; +const AI_ASSISTANT_CHECKBOX_CHOICES = [ + { value: "none", name: "None (skip MCP configuration)" }, + ...AI_ASSISTANT_CHOICES.map(a => ({ + value: a, + name: AI_ASSISTANT_LABELS[a], + checked: AI_ASSISTANT_CHECKBOX_DEFAULTS.includes(a) + })) +]; + export async function promptForAgents(): Promise { let selected: AIAgentTarget[] = AI_AGENT_CHECKBOX_DEFAULTS; if (Util.canPrompt()) { const result = await InquirerWrapper.checkbox({ - message: "Which AI tools do you want to generate configuration files for?", + message: "Which AI agents do you want to generate skills and instructions for?", required: true, choices: AI_AGENT_CHECKBOX_CHOICES }); @@ -60,6 +80,19 @@ export async function promptForAgents(): Promise { return selected; } +export async function promptForAssistant(): Promise { + let selected: AiCodingAssistant[] = AI_ASSISTANT_CHECKBOX_DEFAULTS; + if (Util.canPrompt()) { + const result = await InquirerWrapper.checkbox({ + message: "Which coding assistants should MCP servers be configured for?", + required: true, + choices: AI_ASSISTANT_CHECKBOX_CHOICES + }); + selected = result.includes("none") ? [] : result as AiCodingAssistant[]; + } + return selected; +} + const command: CommandModule = { command: "ai-config", describe: "Configures Ignite UI AI tooling (MCP servers, AI coding skills and instructions)", @@ -70,9 +103,15 @@ const command: CommandModule = { describe: "AI agents/tools to generate configuration files for", choices: AI_AGENT_CHOICES, type: "array" + }) + .option("assistant", { + describe: "Coding assistant(s) to configure MCP servers for", + choices: AI_ASSISTANT_CHOICES, + type: "array" }), async handler(argv: ArgumentsCamelCase) { let agents = argv.agent as AIAgentTarget[] | undefined; + let assistants = argv.assistant as AiCodingAssistant[] | undefined; GoogleAnalytics.post({ t: "screenview", cd: "Ai Config" @@ -81,17 +120,26 @@ const command: CommandModule = { if (!agents?.length) { agents = await promptForAgents(); } + if (!assistants?.length) { + assistants = await promptForAssistant(); + } + GoogleAnalytics.post({ t: "event", ec: "$ig ai-config", - ea: `agent: ${agents.join(", ")}` + ea: `agent: ${agents?.join(", ") || "none"}; assistant: ${assistants?.join(", ") || "none"}` }); + if (!assistants.length) { + Util.log("No MCP configuration selected. Skipping."); + return; + } + if (!agents.length) { Util.log("No AI configuration selected. Skipping."); return; } - await configure(agents); + await configure(agents, true, assistants); } }; diff --git a/packages/cli/lib/commands/new.ts b/packages/cli/lib/commands/new.ts index 9e1c94618..5803f979e 100644 --- a/packages/cli/lib/commands/new.ts +++ b/packages/cli/lib/commands/new.ts @@ -1,4 +1,4 @@ -import { AI_AGENT_CHOICES, AIAgentTarget, GoogleAnalytics, PackageManager, ProjectConfig, ProjectLibrary, Util } from "@igniteui/cli-core"; +import { AI_AGENT_CHOICES, AIAgentTarget, AiCodingAssistant, GoogleAnalytics, PackageManager, ProjectConfig, ProjectLibrary, Util } from "@igniteui/cli-core"; import * as path from "path"; import { PromptSession } from "./../PromptSession"; import { NewCommandType, PositionalArgs } from "./types"; @@ -166,7 +166,7 @@ const command: NewCommandType = { } process.chdir(argv.name); - await configure(argv.agents as AIAgentTarget[] | undefined); + await configure(argv.agents as AIAgentTarget[] | undefined, true, argv.assistants as AiCodingAssistant[] | undefined); process.chdir(".."); if (!argv["skip-git"] && !ProjectConfig.getConfig().skipGit) { diff --git a/packages/core/util/mcp-config.ts b/packages/core/util/mcp-config.ts index bf41928df..f01ad99b0 100644 --- a/packages/core/util/mcp-config.ts +++ b/packages/core/util/mcp-config.ts @@ -7,6 +7,30 @@ export interface McpServerEntry { args: string[]; } +export const AI_ASSISTANT_CHOICES = ["vscode", "claude-code", "cursor", "gemini", "junie"] as const; +export type AiCodingAssistant = typeof AI_ASSISTANT_CHOICES[number]; + +interface AssistantMcpConfig { + mcpFilePath: string; + rootKey: "servers" | "mcpServers"; +} + +export const AI_ASSISTANT_LABELS: Record = { + "vscode": "VS Code (GitHub Copilot)", + "claude-code": "Claude Code", + "cursor": "Cursor", + "gemini": "Gemini", + "junie": "JetBrains Junie", +}; + +export const AI_ASSISTANT_MCP_CONFIGS: Record = { + "vscode": { mcpFilePath: ".vscode/mcp.json", rootKey: "servers" }, + "claude-code": { mcpFilePath: ".mcp.json", rootKey: "mcpServers" }, + "cursor": { mcpFilePath: ".cursor/mcp.json", rootKey: "mcpServers" }, + "gemini": { mcpFilePath: ".gemini/settings.json", rootKey: "mcpServers" }, + "junie": { mcpFilePath: ".junie/mcp/mcp.json", rootKey: "mcpServers" }, +}; + const IGNITEUI_MCP_SERVERS: Record = { "igniteui-cli": { command: "npx", @@ -18,18 +42,18 @@ const IGNITEUI_MCP_SERVERS: Record = { } }; -export const VS_CODE_MCP_PATH = ".vscode/mcp.json"; - /** - * Reads .vscode/mcp.json, ensures all IgniteUI MCP servers are present, + * Reads the assistant-specific MCP config file, ensures all IgniteUI MCP servers are present, * optionally adds additional servers. Creates the file if it doesn't exist. + * @param assistant target AI coding assistant (defaults to "vscode") * @param additionalServers optional extra servers to include alongside the built-in ones * @returns whether the file was modified */ export function addMcpServers( - mcpFilePath: string, + assistant: AiCodingAssistant = "vscode", additionalServers?: Record ): boolean { + const { mcpFilePath, rootKey } = AI_ASSISTANT_MCP_CONFIGS[assistant]; const fileSystem = App.container.get(FS_TOKEN); const servers = { ...additionalServers, ...IGNITEUI_MCP_SERVERS }; @@ -44,12 +68,12 @@ export function addMcpServers( if (Object.keys(servers).length === 0) { return false; } - fileSystem.writeFile(mcpFilePath, JSON.stringify({ servers }, null, 2) + "\n"); + fileSystem.writeFile(mcpFilePath, JSON.stringify({ [rootKey]: servers }, null, 2) + "\n"); return true; } const parsed = jsonc.parse(existingContent); - const existing = parsed.servers ?? {}; + const existing = parsed[rootKey] ?? {}; const formattingOptions: jsonc.FormattingOptions = { tabSize: 2, insertSpaces: true }; let text = existingContent; @@ -57,7 +81,7 @@ export function addMcpServers( for (const [key, value] of Object.entries(servers)) { if (!existing[key]) { - const edits = jsonc.modify(text, ["servers", key], value, { formattingOptions }); + const edits = jsonc.modify(text, [rootKey, key], value, { formattingOptions }); text = jsonc.applyEdits(text, edits); modified = true; } @@ -69,3 +93,4 @@ export function addMcpServers( return modified; } + diff --git a/packages/ng-schematics/src/cli-config/ai-config-schema.json b/packages/ng-schematics/src/cli-config/ai-config-schema.json index 6e68dc9fd..8f176d9f3 100644 --- a/packages/ng-schematics/src/cli-config/ai-config-schema.json +++ b/packages/ng-schematics/src/cli-config/ai-config-schema.json @@ -15,11 +15,11 @@ "enum": ["none", "claude", "copilot", "cursor", "codex", "windsurf", "gemini", "junie", "generic"] }, "x-prompt": { - "message": "Which AI tools do you want to generate configuration files for?", + "message": "Which AI agents do you want to generate skills and instructions for?", "type": "list", "multiselect": true, "items": [ - { "value": "none", "label": "None (skip AI configuration)" }, + { "value": "none", "label": "None (skip skills and instructions)" }, { "value": "generic", "label": "Generic (Adding .agents/skills and AGENTS.md)" }, { "value": "claude", "label": "Claude (Adding .claude/skills and CLAUDE.md)" }, { "value": "copilot", "label": "Copilot (Adding .github/skills and copilot-instructions.md)" }, @@ -30,6 +30,28 @@ { "value": "junie", "label": "Junie (Adding .junie/skills and .junie/guidelines.md)" } ] } + }, + "assistant": { + "type": "array", + "description": "Coding assistant(s) to configure MCP servers for.", + "default": ["vscode"], + "items": { + "type": "string", + "enum": ["none", "vscode", "cursor", "claude-code", "gemini", "junie"] + }, + "x-prompt": { + "message": "Which coding assistants should MCP servers be configured for?", + "type": "list", + "multiselect": true, + "items": [ + { "value": "none", "label": "None (skip MCP configuration)" }, + { "value": "vscode", "label": "VS Code (GitHub Copilot)" }, + { "value": "cursor", "label": "Cursor" }, + { "value": "claude-code", "label": "Claude Code" }, + { "value": "gemini", "label": "Gemini" }, + { "value": "junie", "label": "JetBrains Junie" } + ] + } } } } diff --git a/packages/ng-schematics/src/cli-config/index.ts b/packages/ng-schematics/src/cli-config/index.ts index 15c72be7c..a6a04d546 100644 --- a/packages/ng-schematics/src/cli-config/index.ts +++ b/packages/ng-schematics/src/cli-config/index.ts @@ -2,7 +2,7 @@ import * as ts from "typescript"; import { DependencyNotFoundException } from "@angular-devkit/core"; import { chain, FileDoesNotExistException, Rule, SchematicContext, Tree } from "@angular-devkit/schematics"; import { RunSchematicTask } from "@angular-devkit/schematics/tasks"; -import { addClassToBody, addMcpServers, AIAgentTarget, App, copyAgentInstructionFiles, copyAISkillsToProject, FormatSettings, McpServerEntry, NPM_ANGULAR, resolvePackage, TEMPLATE_MANAGER, TypeScriptAstTransformer, TypeScriptUtils, VS_CODE_MCP_PATH } from "@igniteui/cli-core"; +import { addClassToBody, addMcpServers, AIAgentTarget, AiCodingAssistant, App, copyAgentInstructionFiles, copyAISkillsToProject, FormatSettings, McpServerEntry, NPM_ANGULAR, resolvePackage, TEMPLATE_MANAGER, TypeScriptAstTransformer, TypeScriptUtils } from "@igniteui/cli-core"; import { AngularTypeScriptFileUpdate } from "@igniteui/angular-templates"; import { createCliConfig } from "../utils/cli-config"; import { setVirtual } from "../utils/NgFileSystem"; @@ -127,7 +127,7 @@ function appInit(tree: Tree) { setVirtual(tree); } -function aiConfig({ init, agents }: { init: boolean; agents: AIAgentTarget[] }): Rule { +function aiConfig({ init, agents, assistants = ["vscode"] }: { init: boolean; agents: AIAgentTarget[]; assistants?: AiCodingAssistant[] }): Rule { return (tree: Tree) => { if (init) { appInit(tree); @@ -142,18 +142,19 @@ function aiConfig({ init, agents }: { init: boolean; agents: AIAgentTarget[] }): } }; - addMcpServers(VS_CODE_MCP_PATH, angularCliServer); + for (const assistant of assistants) { + addMcpServers(assistant, angularCliServer); + } }; } /** Standalone `ai-config` schematic entry */ -export function addAIConfig(options: { agents?: AIAgentTarget[] } = {}): Rule { +export function addAIConfig(options: { agents?: AIAgentTarget[]; /* TODO: assistants */ assistant?: AiCodingAssistant[] } = {}): Rule { const selected = options.agents?.length ? options.agents : [] as AIAgentTarget[]; const agents = selected.includes("none" as any) ? [] : selected; - if (!agents.length) { - return (tree: Tree) => tree; - } - return aiConfig({ init: true, agents }); + const selectedAssistants = options.assistant?.length ? options.assistant : ["vscode"] as AiCodingAssistant[]; + const assistants = selectedAssistants.includes("none" as any) ? [] : selectedAssistants; + return aiConfig({ init: true, agents, assistants }); } export default function (): Rule { diff --git a/packages/ng-schematics/src/cli-config/index_spec.ts b/packages/ng-schematics/src/cli-config/index_spec.ts index 726a42241..7a7c93f5e 100644 --- a/packages/ng-schematics/src/cli-config/index_spec.ts +++ b/packages/ng-schematics/src/cli-config/index_spec.ts @@ -432,5 +432,37 @@ export const appConfig: ApplicationConfig = { expect(aiSkillsModule.copyAISkillsToProject).toHaveBeenCalledWith(["claude", "cursor"]); expect(aiSkillsModule.copyAgentInstructionFiles).toHaveBeenCalledWith(["claude", "cursor"]); }); + + it("should default MCP config to .vscode/mcp.json with servers key", async () => { + await runner.runSchematic("ai-config", {}, tree); + + const mcpFilePath = "/.vscode/mcp.json"; + expect(tree.exists(mcpFilePath)).toBeTruthy(); + const content = JSON.parse(tree.readContent(mcpFilePath)); + expect(content.servers).toBeDefined(); + expect(content.servers["igniteui-cli"]).toEqual({ command: "npx", args: ["-y", "igniteui-cli", "mcp"] }); + }); + + it("should write to .cursor/mcp.json with mcpServers key when assistant is cursor", async () => { + await runner.runSchematic("ai-config", { assistant: ["cursor"] }, tree); + + const mcpFilePath = "/.cursor/mcp.json"; + expect(tree.exists(mcpFilePath)).toBeTruthy(); + const content = JSON.parse(tree.readContent(mcpFilePath)); + expect(content.mcpServers).toBeDefined(); + expect(content.mcpServers["igniteui-cli"]).toEqual({ command: "npx", args: ["-y", "igniteui-cli", "mcp"] }); + expect(content.mcpServers["angular-cli"]).toEqual({ command: "npx", args: ["-y", "@angular/cli", "mcp"] }); + expect(content.servers).toBeUndefined(); + }); + + it("should write to .mcp.json when assistant is claude-code", async () => { + await runner.runSchematic("ai-config", { assistant: ["claude-code"] }, tree); + + const mcpFilePath = "/.mcp.json"; + expect(tree.exists(mcpFilePath)).toBeTruthy(); + const content = JSON.parse(tree.readContent(mcpFilePath)); + expect(content.mcpServers["igniteui-cli"]).toBeDefined(); + expect(content.mcpServers["angular-cli"]).toBeDefined(); + }); }); }); diff --git a/spec/unit/ai-config-spec.ts b/spec/unit/ai-config-spec.ts index 740d7847b..ca242ed6f 100644 --- a/spec/unit/ai-config-spec.ts +++ b/spec/unit/ai-config-spec.ts @@ -50,6 +50,19 @@ describe("Unit - ai-config command", () => { expect((config.servers as any)[IGNITEUI_THEMING_SERVER_KEY]).toEqual(igniteuiThemingServer); }); + it("creates config with mcpServers key for non-vscode assistants", () => { + const mockFs = createMockFs(); + App.container.set(FS_TOKEN, mockFs); + + configureMCP(["cursor"]); + + expect(mockFs.writeFile).toHaveBeenCalledWith(".cursor/mcp.json", jasmine.any(String)); + const config = writtenConfig(mockFs); + expect((config.mcpServers as any)[IGNITEUI_SERVER_KEY]).toEqual(igniteuiServer); + expect((config.mcpServers as any)[IGNITEUI_THEMING_SERVER_KEY]).toEqual(igniteuiThemingServer); + expect(config.servers).toBeUndefined(); + }); + it("adds both servers when file exists but servers object is empty", () => { const mockFs = createMockFs(JSON.stringify({ servers: {} })); App.container.set(FS_TOKEN, mockFs); @@ -264,16 +277,19 @@ describe("Unit - ai-config command", () => { it("prompts for agents when --agent is not provided", async () => { App.container.set(FS_TOKEN, createMockFs()); spyOn(Util, "canPrompt").and.returnValue(true); - spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["claude"])); + spyOn(InquirerWrapper, "checkbox").and.returnValues( + Promise.resolve(["vscode"]), + Promise.resolve(["claude"]) + ); await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" }); expect(InquirerWrapper.checkbox).toHaveBeenCalledWith(jasmine.objectContaining({ - message: "Which AI tools do you want to generate configuration files for?", + message: "Which AI agents do you want to generate skills and instructions for?", required: true })); expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "screenview", cd: "Ai Config" })); - expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "event", ea: "agent: claude" })); + expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "event", ea: "agent: claude; assistant: vscode" })); }); it("uses defaults without prompting when canPrompt returns false", async () => { @@ -284,42 +300,102 @@ describe("Unit - ai-config command", () => { await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" }); expect(InquirerWrapper.checkbox).not.toHaveBeenCalled(); - expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "event", ea: "agent: generic, claude" })); + expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "event", ea: "agent: generic, claude; assistant: vscode, claude-code" })); }); it("logs skipping and does not post analytics when none is selected", async () => { App.container.set(FS_TOKEN, createMockFs()); spyOn(Util, "canPrompt").and.returnValue(true); spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["none"])); + spyOn(InquirerWrapper, "select").and.returnValue(Promise.resolve("vscode")); await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" }); expect(Util.log).toHaveBeenCalledWith(jasmine.stringContaining("Skipping")); expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "screenview", cd: "Ai Config" })); - expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "event", ea: "agent: " })); + expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "event", ea: "agent: none; assistant: none" })); + }); + + it("still configures MCP when none is selected for skills", async () => { + const mockFs = createMockFs(); + App.container.set(FS_TOKEN, mockFs); + spyOn(InquirerWrapper, "checkbox").and.returnValues( + Promise.resolve(["vscode"]), + Promise.resolve(["none"]) + ); + + await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" }); + + expect(mockFs.writeFile).toHaveBeenCalled(); + // TODO: toHaveBeenCalledWith check for mcp.json + expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "screenview", cd: "Ai Config" })); + expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ ea: "agent: none; assistant: none" })); }); it("configures multiple agents when selected interactively", async () => { App.container.set(FS_TOKEN, createMockFs()); spyOn(Util, "canPrompt").and.returnValue(true); - spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["claude", "cursor"])); + spyOn(InquirerWrapper, "checkbox").and.returnValues( + Promise.resolve(["vscode"]), + Promise.resolve(["claude", "cursor"]) + ); await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" }); expect(InquirerWrapper.checkbox).toHaveBeenCalledWith(jasmine.objectContaining({ - message: "Which AI tools do you want to generate configuration files for?" + message: "Which AI agents do you want to generate skills and instructions for?" })); - expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ ea: "agent: claude, cursor" })); + expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ ea: "agent: claude, cursor; assistant: vscode" })); }); it("skips prompt when --agent is provided", async () => { App.container.set(FS_TOKEN, createMockFs()); - spyOn(InquirerWrapper, "checkbox"); + spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["vscode"])); await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", agent: ["cursor"] }); - expect(InquirerWrapper.checkbox).not.toHaveBeenCalled(); - expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ ea: "agent: cursor" })); + expect(InquirerWrapper.checkbox).not.toHaveBeenCalledWith(jasmine.objectContaining({ + message: "Which AI agents do you want to generate skills and instructions for?" + })); + expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ ea: "agent: cursor; assistant: vscode" })); + }); + + it("skips assistant prompt when --assistant is provided", async () => { + App.container.set(FS_TOKEN, createMockFs()); + spyOn(InquirerWrapper, "checkbox").and.returnValue(Promise.resolve(["claude"])); + + await aiConfig.default.handler({ _: ["ai-config"], $0: "ig", assistant: ["cursor"] }); + + expect(InquirerWrapper.checkbox).toHaveBeenCalledTimes(1); + }); + + it("prompts for assistant with correct message", async () => { + App.container.set(FS_TOKEN, createMockFs()); + spyOn(InquirerWrapper, "checkbox").and.returnValues( + Promise.resolve(["cursor"]), + Promise.resolve(["claude"]) + ); + + await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" }); + + expect(InquirerWrapper.checkbox).toHaveBeenCalledWith(jasmine.objectContaining({ + message: "Which coding assistants should MCP servers be configured for?" + })); + }); + + it("writes to correct config path for selected assistant", async () => { + const mockFs = createMockFs(); + App.container.set(FS_TOKEN, mockFs); + spyOn(InquirerWrapper, "checkbox").and.returnValues( + Promise.resolve(["claude-code"]), + Promise.resolve(["none"]) + ); + + await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" }); + + expect(mockFs.writeFile).toHaveBeenCalledWith(".mcp.json", jasmine.any(String)); + const config = writtenConfig(mockFs); + expect((config.mcpServers as any)[IGNITEUI_SERVER_KEY]).toEqual(igniteuiServer); }); }); });