From 6e152e03999d81da798cb265538d046c028a347e Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Fri, 13 Mar 2026 15:57:45 +0200 Subject: [PATCH 01/15] feat(commands): add MCP command for configuring Ignite UI MCP server --- packages/cli/lib/cli.ts | 2 + packages/cli/lib/commands/index.ts | 1 + packages/cli/lib/commands/mcp.ts | 125 +++++++++++++++++++++++++++++ packages/cli/lib/commands/types.ts | 4 +- 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 packages/cli/lib/commands/mcp.ts diff --git a/packages/cli/lib/cli.ts b/packages/cli/lib/cli.ts index 1909f6248..9ab0b7661 100644 --- a/packages/cli/lib/cli.ts +++ b/packages/cli/lib/cli.ts @@ -9,6 +9,7 @@ import { doc, generate, list, + mcp, newCommand, start, test, @@ -54,6 +55,7 @@ export async function run(args = null) { .command(test) .command(list) .command(upgrade) + .command(mcp) .version(false) // disable built-in `yargs.version` to override it with our custom option .options({ version: { diff --git a/packages/cli/lib/commands/index.ts b/packages/cli/lib/commands/index.ts index bc048b730..af5a4f3b0 100644 --- a/packages/cli/lib/commands/index.ts +++ b/packages/cli/lib/commands/index.ts @@ -4,6 +4,7 @@ export { default as config } from "./config"; export { default as doc } from "./doc"; export { default as generate } from "./generate"; export { default as list } from "./list"; +export { default as mcp } from "./mcp"; export { default as newCommand } from "./new"; export { default as start } from "./start"; export { default as test } from "./test"; diff --git a/packages/cli/lib/commands/mcp.ts b/packages/cli/lib/commands/mcp.ts new file mode 100644 index 000000000..897826e53 --- /dev/null +++ b/packages/cli/lib/commands/mcp.ts @@ -0,0 +1,125 @@ +import { GoogleAnalytics, Util } from "@igniteui/cli-core"; +import { ArgumentsCamelCase, CommandModule } from "yargs"; +import * as path from "path"; +import * as fs from "fs"; +import { homedir } from "os"; + +const MCP_SERVER_KEY = "igniteui-mcp-server"; + + +function resolveMcpEntry(): { command: string; args: string[] } { + // Resolve the MCP server entry from the CLI's own node_modules. + // This works in both the monorepo (file: dep) and when the CLI is globally installed. + try { + const pkgEntry = require.resolve("igniteui-mcp-server"); + return { command: "node", args: [pkgEntry] }; + } catch { + // Not installed alongside the CLI — download and run on demand via npx + return { command: "npx", args: ["-y", "igniteui-mcp-server"] }; + } +} + +type Client = "vscode" | "claude"; + +interface McpServerEntry { + command: string; + args: string[]; +} + +interface VsCodeMcpConfig { + servers: Record; +} + +interface ClaudeDesktopConfig { + mcpServers: Record; +} + +function getConfigPath(client: Client): string { + switch (client) { + case "vscode": + return path.join(process.cwd(), ".vscode", "mcp.json"); + case "claude": { + const platform = process.platform; + if (platform === "win32") { + return path.join(process.env.APPDATA || homedir(), "Claude", "claude_desktop_config.json"); + } else if (platform === "darwin") { + return path.join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"); + } else { + return path.join(homedir(), ".config", "Claude", "claude_desktop_config.json"); + } + } + } +} + +function readJson(filePath: string, fallback: T): T { + try { + return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; + } catch { + return fallback; + } +} + +function writeJson(filePath: string, data: unknown): void { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8"); +} + +function configureVsCode(entry: McpServerEntry): void { + const configPath = getConfigPath("vscode"); + const config = readJson(configPath, { servers: {} }); + config.servers = config.servers || {}; + config.servers[MCP_SERVER_KEY] = entry; + writeJson(configPath, config); + Util.log(Util.greenCheck() + ` MCP server configured in ${configPath}`); +} + +function configureClaude(entry: McpServerEntry): void { + const configPath = getConfigPath("claude"); + const config = readJson(configPath, { mcpServers: {} }); + config.mcpServers = config.mcpServers || {}; + config.mcpServers[MCP_SERVER_KEY] = entry; + writeJson(configPath, config); + Util.log(Util.greenCheck() + ` MCP server configured in ${configPath}`); +} + +const command: CommandModule = { + command: "mcp", + describe: "Configure the Ignite UI MCP server for an AI client", + builder: (yargs) => { + return yargs + .option("client", { + alias: "c", + describe: "The AI client to configure (vscode, claude)", + type: "string", + choices: ["vscode", "claude"], + default: "vscode" + }) + .usage(""); // do not show any usage instructions before the commands list + }, + async handler(argv: ArgumentsCamelCase) { + GoogleAnalytics.post({ + t: "screenview", + cd: "MCP" + }); + + const client = argv.client as Client; + const entry = resolveMcpEntry(); + + GoogleAnalytics.post({ + t: "event", + ec: "$ig mcp", + ea: `client: ${client}` + }); + + switch (client) { + case "vscode": + configureVsCode(entry); + break; + case "claude": + configureClaude(entry); + break; + } + } +}; + +export default command; diff --git a/packages/cli/lib/commands/types.ts b/packages/cli/lib/commands/types.ts index a3e9d3d52..21575f0a3 100644 --- a/packages/cli/lib/commands/types.ts +++ b/packages/cli/lib/commands/types.ts @@ -12,6 +12,7 @@ export const DOC_COMMAND_NAME = "doc"; export const TEST_COMMAND_NAME = "test"; export const LIST_COMMAND_NAME = "list"; export const UPGRADE_COMMAND_NAME = "upgrade-packages"; +export const MCP_COMMAND_NAME = "mcp"; export const ALL_COMMANDS = new Set([ ADD_COMMAND_NAME, @@ -23,7 +24,8 @@ export const ALL_COMMANDS = new Set([ DOC_COMMAND_NAME, TEST_COMMAND_NAME, LIST_COMMAND_NAME, - UPGRADE_COMMAND_NAME + UPGRADE_COMMAND_NAME, + MCP_COMMAND_NAME ]); export interface PositionalArgs { From d66670a123c03abe165dd1ca32956b1a4b4c8f31 Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Mon, 16 Mar 2026 17:05:09 +0200 Subject: [PATCH 02/15] feat(mcp): add MCP server configuration in project setup --- packages/cli/lib/PromptSession.ts | 32 +++++++++++++++++++++++ packages/cli/lib/commands/mcp.ts | 8 ++++++ packages/core/prompt/BasePromptSession.ts | 17 ++++++++++++ 3 files changed, 57 insertions(+) diff --git a/packages/cli/lib/PromptSession.ts b/packages/cli/lib/PromptSession.ts index 99353b3b9..76838e306 100644 --- a/packages/cli/lib/PromptSession.ts +++ b/packages/cli/lib/PromptSession.ts @@ -3,6 +3,7 @@ import { ProjectLibrary, PromptTaskContext, Task, Util } from "@igniteui/cli-core"; import * as path from "path"; +import * as fs from "fs"; import { default as add } from "./commands/add"; import { default as start } from "./commands/start"; import { default as upgrade } from "./commands/upgrade"; @@ -105,6 +106,37 @@ export class PromptSession extends BasePromptSession { await upgrade.upgrade({ skipInstall: true, _: ["upgrade"], $0: "upgrade" }); } + protected async configureMcp(): Promise { + const MCP_SERVER_KEY = "igniteui-mcp-server"; + let command: string; + let args: string[]; + try { + const pkgEntry = require.resolve("igniteui-mcp-server"); + command = "node"; + args = [pkgEntry]; + } catch { + command = "npx"; + args = ["-y", "igniteui-mcp-server"]; + } + const configPath = path.join(process.cwd(), ".vscode", "mcp.json"); + let config: { servers: Record } = { servers: {} }; + try { + config = JSON.parse(fs.readFileSync(configPath, "utf8")); + } catch { /* file doesn't exist yet */ } + config.servers = config.servers || {}; + + if (config.servers[MCP_SERVER_KEY]) { + Util.log(Util.greenCheck() + ` Ignite UI MCP server already configured in ${configPath}`); + return; + } + + // Preserve existing MCP entries and add ours + config.servers[MCP_SERVER_KEY] = { command, args }; + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8"); + Util.log(Util.greenCheck() + ` MCP server configured in ${configPath}`); + } + /** * Get user name and set template's extra configurations if any * @param projectLibrary to add component to diff --git a/packages/cli/lib/commands/mcp.ts b/packages/cli/lib/commands/mcp.ts index 897826e53..0b6345ef8 100644 --- a/packages/cli/lib/commands/mcp.ts +++ b/packages/cli/lib/commands/mcp.ts @@ -68,6 +68,10 @@ function configureVsCode(entry: McpServerEntry): void { const configPath = getConfigPath("vscode"); const config = readJson(configPath, { servers: {} }); config.servers = config.servers || {}; + if (config.servers[MCP_SERVER_KEY]) { + Util.log( ` Ignite UI MCP server already configured in ${configPath}`); + return; + } config.servers[MCP_SERVER_KEY] = entry; writeJson(configPath, config); Util.log(Util.greenCheck() + ` MCP server configured in ${configPath}`); @@ -77,6 +81,10 @@ function configureClaude(entry: McpServerEntry): void { const configPath = getConfigPath("claude"); const config = readJson(configPath, { mcpServers: {} }); config.mcpServers = config.mcpServers || {}; + if (config.mcpServers[MCP_SERVER_KEY]) { + Util.log(` Ignite UI MCP server already configured in ${configPath}`); + return; + } config.mcpServers[MCP_SERVER_KEY] = entry; writeJson(configPath, config); Util.log(Util.greenCheck() + ` MCP server configured in ${configPath}`); diff --git a/packages/core/prompt/BasePromptSession.ts b/packages/core/prompt/BasePromptSession.ts index b73c1650b..92de3aefd 100644 --- a/packages/core/prompt/BasePromptSession.ts +++ b/packages/core/prompt/BasePromptSession.ts @@ -98,6 +98,9 @@ export abstract class BasePromptSession { /** Upgrade packages to use private Infragistics feed */ protected abstract upgradePackages(); + /** Configure the Ignite UI MCP server for the project */ + protected abstract configureMcp(): Promise; + /** * Get user name and set template's extra configurations if any * @param projectLibrary to add component to @@ -426,6 +429,20 @@ export abstract class BasePromptSession { await this.upgradePackages(); } } + + const addMcp = await this.getUserInput({ + type: "list", + name: "addMcp", + message: "Would you like to add the Ignite UI MCP server to this project?", + choices: [ + { value: "yes", name: "Yes (adds .vscode/mcp.json)", short: "Yes" }, + { value: "no", name: "Skip for now", short: "Skip" } + ], + default: "yes" + }); + if (addMcp === "yes") { + await this.configureMcp(); + } const defaultPort = config.project.defaultPort; const port = await this.getUserInput({ From 098b795dfe37adba0bb7a253efe9d9e4dfb3cb5b Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Wed, 15 Apr 2026 15:09:17 +0300 Subject: [PATCH 03/15] feat: add ai-config command to CLI and update command registration --- packages/cli/lib/cli.ts | 2 ++ packages/cli/lib/commands/ai-config.ts | 2 +- packages/cli/lib/commands/index.ts | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/lib/cli.ts b/packages/cli/lib/cli.ts index 9ab0b7661..117c8584c 100644 --- a/packages/cli/lib/cli.ts +++ b/packages/cli/lib/cli.ts @@ -4,6 +4,7 @@ import { add, ADD_COMMAND_NAME, ALL_COMMANDS, + aiConfig, build, config, doc, @@ -56,6 +57,7 @@ export async function run(args = null) { .command(list) .command(upgrade) .command(mcp) + .command(aiConfig) .version(false) // disable built-in `yargs.version` to override it with our custom option .options({ version: { diff --git a/packages/cli/lib/commands/ai-config.ts b/packages/cli/lib/commands/ai-config.ts index 81a9d1c99..b69e3d515 100644 --- a/packages/cli/lib/commands/ai-config.ts +++ b/packages/cli/lib/commands/ai-config.ts @@ -91,7 +91,7 @@ function configureClaude(entry: McpServerEntry): void { } const command: CommandModule = { - command: "mcp", + command: "ai-config", describe: "Configure the Ignite UI MCP server for an AI client", builder: (yargs) => { return yargs diff --git a/packages/cli/lib/commands/index.ts b/packages/cli/lib/commands/index.ts index af5a4f3b0..8ac2a429d 100644 --- a/packages/cli/lib/commands/index.ts +++ b/packages/cli/lib/commands/index.ts @@ -1,4 +1,5 @@ export { default as add } from "./add"; +export { default as aiConfig } from "./ai-config"; export { default as build } from "./build"; export { default as config } from "./config"; export { default as doc } from "./doc"; From 742251c8f211c684be31bdb071b35eb9f927d254 Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Thu, 16 Apr 2026 15:03:24 +0300 Subject: [PATCH 04/15] feat: add ai-config command and update MCP server configuration --- packages/cli/lib/commands/ai-config.ts | 107 +++++------------- packages/cli/lib/commands/types.ts | 4 +- packages/core/prompt/BasePromptSession.ts | 14 +-- .../src/prompt/SchematicsPromptSession.ts | 4 + spec/acceptance/help-spec.ts | 1 + spec/unit/PromptSession-spec.ts | 6 +- 6 files changed, 43 insertions(+), 93 deletions(-) diff --git a/packages/cli/lib/commands/ai-config.ts b/packages/cli/lib/commands/ai-config.ts index b69e3d515..64964acc6 100644 --- a/packages/cli/lib/commands/ai-config.ts +++ b/packages/cli/lib/commands/ai-config.ts @@ -2,24 +2,19 @@ import { ArgumentsCamelCase, CommandModule } from "yargs"; import * as path from "path"; import * as fs from "fs"; -import { homedir } from "os"; -const MCP_SERVER_KEY = "igniteui-mcp-server"; +const IGNITEUI_SERVER_KEY = "igniteui-cli"; +const IGNITEUI_THEMING_SERVER_KEY = "igniteui-theming"; +const igniteuiServer = { + command: "npx", + args: ["-y", "igniteui-cli@next", "mcp"] +}; -function resolveMcpEntry(): { command: string; args: string[] } { - // Resolve the MCP server entry from the CLI's own node_modules. - // This works in both the monorepo (file: dep) and when the CLI is globally installed. - try { - const pkgEntry = require.resolve("igniteui-mcp-server"); - return { command: "node", args: [pkgEntry] }; - } catch { - // Not installed alongside the CLI ΓÇö download and run on demand via npx - return { command: "npx", args: ["-y", "igniteui-mcp-server"] }; - } -} - -type Client = "vscode" | "claude"; +const igniteuiThemingServer = { + command: "npx", + args: ["-y", "igniteui-theming", "igniteui-theming-mcp"] +}; interface McpServerEntry { command: string; @@ -30,25 +25,8 @@ interface VsCodeMcpConfig { servers: Record; } -interface ClaudeDesktopConfig { - mcpServers: Record; -} - -function getConfigPath(client: Client): string { - switch (client) { - case "vscode": - return path.join(process.cwd(), ".vscode", "mcp.json"); - case "claude": { - const platform = process.platform; - if (platform === "win32") { - return path.join(process.env.APPDATA || homedir(), "Claude", "claude_desktop_config.json"); - } else if (platform === "darwin") { - return path.join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"); - } else { - return path.join(homedir(), ".config", "Claude", "claude_desktop_config.json"); - } - } - } +function getConfigPath(): string { + return path.join(process.cwd(), ".vscode", "mcp.json"); } function readJson(filePath: string, fallback: T): T { @@ -64,69 +42,46 @@ function writeJson(filePath: string, data: unknown): void { fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8"); } -function configureVsCode(entry: McpServerEntry): void { - const configPath = getConfigPath("vscode"); +function configureVsCode(): void { + const configPath = getConfigPath(); const config = readJson(configPath, { servers: {} }); config.servers = config.servers || {}; - if (config.servers[MCP_SERVER_KEY]) { - Util.log( ` Ignite UI MCP server already configured in ${configPath}`); - return; + + let modified = false; + if (!config.servers[IGNITEUI_SERVER_KEY]) { + config.servers[IGNITEUI_SERVER_KEY] = igniteuiServer; + modified = true; + } + if (!config.servers[IGNITEUI_THEMING_SERVER_KEY]) { + config.servers[IGNITEUI_THEMING_SERVER_KEY] = igniteuiThemingServer; + modified = true; } - config.servers[MCP_SERVER_KEY] = entry; - writeJson(configPath, config); - Util.log(Util.greenCheck() + ` MCP server configured in ${configPath}`); -} -function configureClaude(entry: McpServerEntry): void { - const configPath = getConfigPath("claude"); - const config = readJson(configPath, { mcpServers: {} }); - config.mcpServers = config.mcpServers || {}; - if (config.mcpServers[MCP_SERVER_KEY]) { - Util.log(` Ignite UI MCP server already configured in ${configPath}`); + if (!modified) { + Util.log(` Ignite UI MCP servers already configured in ${configPath}`); return; } - config.mcpServers[MCP_SERVER_KEY] = entry; writeJson(configPath, config); - Util.log(Util.greenCheck() + ` MCP server configured in ${configPath}`); + Util.log(Util.greenCheck() + ` MCP servers configured in ${configPath}`); } const command: CommandModule = { command: "ai-config", describe: "Configure the Ignite UI MCP server for an AI client", - builder: (yargs) => { - return yargs - .option("client", { - alias: "c", - describe: "The AI client to configure (vscode, claude)", - type: "string", - choices: ["vscode", "claude"], - default: "vscode" - }) - .usage(""); // do not show any usage instructions before the commands list - }, - async handler(argv: ArgumentsCamelCase) { + builder: (yargs) => yargs.usage(""), + async handler(_argv: ArgumentsCamelCase) { GoogleAnalytics.post({ t: "screenview", cd: "MCP" }); - const client = argv.client as Client; - const entry = resolveMcpEntry(); - GoogleAnalytics.post({ t: "event", - ec: "$ig mcp", - ea: `client: ${client}` + ec: "$ig ai-config", + ea: "client: vscode" }); - switch (client) { - case "vscode": - configureVsCode(entry); - break; - case "claude": - configureClaude(entry); - break; - } + configureVsCode(); } }; diff --git a/packages/cli/lib/commands/types.ts b/packages/cli/lib/commands/types.ts index f6ec139d1..cbf6a3fe5 100644 --- a/packages/cli/lib/commands/types.ts +++ b/packages/cli/lib/commands/types.ts @@ -13,6 +13,7 @@ export const TEST_COMMAND_NAME = "test"; export const LIST_COMMAND_NAME = "list"; export const UPGRADE_COMMAND_NAME = "upgrade-packages"; export const MCP_COMMAND_NAME = "mcp"; +export const AI_CONFIG_COMMAND_NAME = "ai-config"; export const ALL_COMMANDS = new Set([ ADD_COMMAND_NAME, @@ -25,7 +26,8 @@ export const ALL_COMMANDS = new Set([ TEST_COMMAND_NAME, LIST_COMMAND_NAME, UPGRADE_COMMAND_NAME, - MCP_COMMAND_NAME + MCP_COMMAND_NAME, + AI_CONFIG_COMMAND_NAME ]); export interface PositionalArgs { diff --git a/packages/core/prompt/BasePromptSession.ts b/packages/core/prompt/BasePromptSession.ts index 030aa34f8..eedd64630 100644 --- a/packages/core/prompt/BasePromptSession.ts +++ b/packages/core/prompt/BasePromptSession.ts @@ -422,19 +422,7 @@ export abstract class BasePromptSession { } } - const addMcp = await this.getUserInput({ - type: "list", - name: "addMcp", - message: "Would you like to add the Ignite UI MCP server to this project?", - choices: [ - { value: "yes", name: "Yes (adds .vscode/mcp.json)", short: "Yes" }, - { value: "no", name: "Skip for now", short: "Skip" } - ], - default: "yes" - }); - if (addMcp === "yes") { - await this.configureMcp(); - } + await this.configureMcp(); const defaultPort = config.project.defaultPort; const port = await this.getUserInput({ diff --git a/packages/ng-schematics/src/prompt/SchematicsPromptSession.ts b/packages/ng-schematics/src/prompt/SchematicsPromptSession.ts index 2a2a5dbef..f6e4d622a 100644 --- a/packages/ng-schematics/src/prompt/SchematicsPromptSession.ts +++ b/packages/ng-schematics/src/prompt/SchematicsPromptSession.ts @@ -58,6 +58,10 @@ export class SchematicsPromptSession extends BasePromptSession { // TODO? } + protected async configureMcp(): Promise { + // No-op in schematics context + } + protected async upgradePackages() { this.userAnswers.set("upgradePackages", true); } diff --git a/spec/acceptance/help-spec.ts b/spec/acceptance/help-spec.ts index d92b28952..2f2c31119 100644 --- a/spec/acceptance/help-spec.ts +++ b/spec/acceptance/help-spec.ts @@ -26,6 +26,7 @@ describe("Help command", () => { upgrade-packages upgrades Ignite UI Packages mcp Starts the Ignite UI MCP server for AI assistant integration + ai-config Configure the Ignite UI MCP server for an AI client Options: -v, --version Show current Ignite UI CLI version [boolean] -h, --help Show help [boolean]`.replace(/\s/g, ""); diff --git a/spec/unit/PromptSession-spec.ts b/spec/unit/PromptSession-spec.ts index 265ea850b..7ef10114e 100644 --- a/spec/unit/PromptSession-spec.ts +++ b/spec/unit/PromptSession-spec.ts @@ -491,7 +491,7 @@ describe("Unit - PromptSession", () => { await mockSession.chooseActionLoop(mockProjectLibrary); expect(mockSession.chooseActionLoop).toHaveBeenCalledTimes(1); expect(InquirerWrapper.select).toHaveBeenCalledTimes(9); - expect(Util.log).toHaveBeenCalledTimes(3); + expect(Util.log).toHaveBeenCalledTimes(4); expect(PackageManager.flushQueue).toHaveBeenCalledWith(true); expect(start.start).toHaveBeenCalledTimes(1); expect(add.addTemplate).toHaveBeenCalledTimes(1); @@ -568,7 +568,7 @@ describe("Unit - PromptSession", () => { expect(mockSession.chooseActionLoop).toHaveBeenCalledTimes(1); expect(InquirerWrapper.select).toHaveBeenCalledTimes(5); expect(InquirerWrapper.input).toHaveBeenCalledTimes(2); - expect(Util.log).toHaveBeenCalledTimes(3); + expect(Util.log).toHaveBeenCalledTimes(4); expect(PackageManager.flushQueue).toHaveBeenCalledWith(true); expect(start.start).toHaveBeenCalledTimes(1); expect(Util.getAvailableName).toHaveBeenCalledTimes(1); @@ -691,7 +691,7 @@ describe("Unit - PromptSession", () => { expect(InquirerWrapper.select).toHaveBeenCalledTimes(10); expect(InquirerWrapper.input).toHaveBeenCalledTimes(2); expect(InquirerWrapper.checkbox).toHaveBeenCalledTimes(1); - expect(Util.log).toHaveBeenCalledTimes(3); + expect(Util.log).toHaveBeenCalledTimes(4); expect(PackageManager.flushQueue).toHaveBeenCalledWith(true); expect(start.start).toHaveBeenCalledTimes(1); expect(add.addTemplate).toHaveBeenCalledTimes(1); From d30ed606bed89508ec932faa71be82df9228058a Mon Sep 17 00:00:00 2001 From: Marina Stoyanova Date: Fri, 17 Apr 2026 09:50:48 +0300 Subject: [PATCH 05/15] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/cli/lib/commands/mcp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/lib/commands/mcp.ts b/packages/cli/lib/commands/mcp.ts index b1fe60d52..6b18fbdbb 100644 --- a/packages/cli/lib/commands/mcp.ts +++ b/packages/cli/lib/commands/mcp.ts @@ -1,4 +1,4 @@ -import * as fs from "fs"; +import * as fs from "fs"; import * as path from "path"; import { spawn } from "child_process"; import { CommandType, PositionalArgs } from "./types"; From 5b61175a7cc79b516f22295a204f82c87cb72b7d Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Fri, 17 Apr 2026 11:38:54 +0300 Subject: [PATCH 06/15] feat: implement IFileSystem interface and update file handling in PromptSession and ai-config commands --- packages/cli/lib/PromptSession.ts | 14 +- packages/cli/lib/commands/ai-config.ts | 19 ++- packages/core/types/FileSystem.ts | 1 + packages/core/util/FileSystem.ts | 3 + .../ng-schematics/src/utils/NgFileSystem.ts | 4 + spec/unit/PromptSession-spec.ts | 25 +++- spec/unit/ai-config-spec.ts | 129 ++++++++++++++++++ spec/unit/packageResolve-spec.ts | 3 + spec/unit/ts-transform/Mock-FS.ts | 4 + 9 files changed, 179 insertions(+), 23 deletions(-) create mode 100644 spec/unit/ai-config-spec.ts diff --git a/packages/cli/lib/PromptSession.ts b/packages/cli/lib/PromptSession.ts index 78bd4142a..074640271 100644 --- a/packages/cli/lib/PromptSession.ts +++ b/packages/cli/lib/PromptSession.ts @@ -1,9 +1,8 @@ import { - BasePromptSession, GoogleAnalytics, InquirerWrapper, PackageManager, ProjectConfig, + BasePromptSession, FsFileSystem, GoogleAnalytics, IFileSystem, InquirerWrapper, PackageManager, ProjectConfig, ProjectLibrary, PromptTaskContext, Task, Util } from "@igniteui/cli-core"; import * as path from "path"; -import * as fs from "fs"; import { default as add } from "./commands/add"; import { default as start } from "./commands/start"; import { default as upgrade } from "./commands/upgrade"; @@ -11,8 +10,11 @@ import { TemplateManager } from "./TemplateManager"; export class PromptSession extends BasePromptSession { - constructor(templateManager: TemplateManager) { + private readonly mcpFs: IFileSystem; + + constructor(templateManager: TemplateManager, mcpFs: IFileSystem = new FsFileSystem()) { super(templateManager); + this.mcpFs = mcpFs; } public static async chooseTerm() { @@ -119,7 +121,7 @@ export class PromptSession extends BasePromptSession { const configPath = path.join(process.cwd(), ".vscode", "mcp.json"); let config: { servers: Record } = { servers: {} }; try { - config = JSON.parse(fs.readFileSync(configPath, "utf8")); + config = JSON.parse(this.mcpFs.readFile(configPath, "utf8")); } catch { /* file doesn't exist yet */ } config.servers = config.servers || {}; @@ -130,8 +132,8 @@ export class PromptSession extends BasePromptSession { // Preserve existing MCP entries and add ours config.servers[MCP_SERVER_KEY] = { command, args }; - fs.mkdirSync(path.dirname(configPath), { recursive: true }); - fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8"); + this.mcpFs.mkdir(path.dirname(configPath), { recursive: true }); + this.mcpFs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n"); Util.log(Util.greenCheck() + ` MCP server configured in ${configPath}`); } diff --git a/packages/cli/lib/commands/ai-config.ts b/packages/cli/lib/commands/ai-config.ts index 64964acc6..4102f07bb 100644 --- a/packages/cli/lib/commands/ai-config.ts +++ b/packages/cli/lib/commands/ai-config.ts @@ -1,7 +1,6 @@ -import { GoogleAnalytics, Util } from "@igniteui/cli-core"; +import { FsFileSystem, GoogleAnalytics, IFileSystem, Util } from "@igniteui/cli-core"; import { ArgumentsCamelCase, CommandModule } from "yargs"; import * as path from "path"; -import * as fs from "fs"; const IGNITEUI_SERVER_KEY = "igniteui-cli"; const IGNITEUI_THEMING_SERVER_KEY = "igniteui-theming"; @@ -29,22 +28,22 @@ function getConfigPath(): string { return path.join(process.cwd(), ".vscode", "mcp.json"); } -function readJson(filePath: string, fallback: T): T { +function readJson(filePath: string, fallback: T, fileSystem: IFileSystem): T { try { - return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; + return JSON.parse(fileSystem.readFile(filePath)) as T; } catch { return fallback; } } -function writeJson(filePath: string, data: unknown): void { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8"); +function writeJson(filePath: string, data: unknown, fileSystem: IFileSystem): void { + fileSystem.mkdir(path.dirname(filePath), { recursive: true }); + fileSystem.writeFile(filePath, JSON.stringify(data, null, 2) + "\n"); } -function configureVsCode(): void { +export function configureVsCode(fileSystem: IFileSystem = new FsFileSystem()): void { const configPath = getConfigPath(); - const config = readJson(configPath, { servers: {} }); + const config = readJson(configPath, { servers: {} }, fileSystem); config.servers = config.servers || {}; let modified = false; @@ -61,7 +60,7 @@ function configureVsCode(): void { Util.log(` Ignite UI MCP servers already configured in ${configPath}`); return; } - writeJson(configPath, config); + writeJson(configPath, config, fileSystem); Util.log(Util.greenCheck() + ` MCP servers configured in ${configPath}`); } diff --git a/packages/core/types/FileSystem.ts b/packages/core/types/FileSystem.ts index 8e9f33194..5be8938ba 100644 --- a/packages/core/types/FileSystem.ts +++ b/packages/core/types/FileSystem.ts @@ -2,6 +2,7 @@ export interface IFileSystem { fileExists(filePath: string): boolean; readFile(filePath: string, encoding?: string): string; writeFile(filePath: string, text: string): void; + mkdir(dirPath: string, options?: { recursive?: boolean }): void; directoryExists(dirPath: string): boolean; /** diff --git a/packages/core/util/FileSystem.ts b/packages/core/util/FileSystem.ts index ddce66d24..6fe253aa9 100644 --- a/packages/core/util/FileSystem.ts +++ b/packages/core/util/FileSystem.ts @@ -21,6 +21,9 @@ export class FsFileSystem implements IFileSystem { public writeFile(filePath: string, text: string): void { fs.writeFileSync(filePath, text); } + public mkdir(dirPath: string, options?: { recursive?: boolean }): void { + fs.mkdirSync(dirPath, options); + } public directoryExists(dirPath: string): boolean { try { return fs.statSync(dirPath).isDirectory(); diff --git a/packages/ng-schematics/src/utils/NgFileSystem.ts b/packages/ng-schematics/src/utils/NgFileSystem.ts index d5d652309..51e250ba0 100644 --- a/packages/ng-schematics/src/utils/NgFileSystem.ts +++ b/packages/ng-schematics/src/utils/NgFileSystem.ts @@ -19,6 +19,10 @@ export class NgTreeFileSystem implements IFileSystem { : this.tree.create(filePath, text); } + public mkdir(_dirPath: string, _options?: { recursive?: boolean }): void { + // Angular Tree manages directories implicitly; no-op here. + } + public directoryExists(dirPath: string): boolean { const dir = this.tree.getDir(dirPath); return dir.subdirs.length || dir.subfiles.length ? true : false; diff --git a/spec/unit/PromptSession-spec.ts b/spec/unit/PromptSession-spec.ts index 7ef10114e..0411b6e16 100644 --- a/spec/unit/PromptSession-spec.ts +++ b/spec/unit/PromptSession-spec.ts @@ -1,4 +1,4 @@ -import { App, BaseTemplate, Config, ControlExtraConfigType, GoogleAnalytics, InquirerWrapper, PackageManager, ProjectConfig, +import { App, BaseTemplate, Config, ControlExtraConfigType, GoogleAnalytics, IFileSystem, InquirerWrapper, PackageManager, ProjectConfig, ProjectLibrary, ProjectTemplate, Template, Util } from "@igniteui/cli-core"; import * as path from "path"; import { default as add } from "../../packages/cli/lib/commands/add"; @@ -8,6 +8,17 @@ import { PromptSession } from "../../packages/cli/lib/PromptSession"; import { TemplateManager } from "../../packages/cli/lib/TemplateManager"; import { Separator } from "@inquirer/prompts"; +function createMockMcpFs(): IFileSystem { + return { + fileExists: jasmine.createSpy("fileExists").and.returnValue(false), + readFile: jasmine.createSpy("readFile").and.throwError("ENOENT"), + writeFile: jasmine.createSpy("writeFile"), + mkdir: jasmine.createSpy("mkdir"), + directoryExists: jasmine.createSpy("directoryExists").and.returnValue(false), + glob: jasmine.createSpy("glob").and.returnValue([]) + }; +} + function createMockBaseTemplate(): BaseTemplate { return { id: "mock-template-id", @@ -457,7 +468,7 @@ describe("Unit - PromptSession", () => { getProjectLibraryNames: projectLibraries, getProjectLibraryByName: mockProjectLibrary }); - const mockSession = new PromptSession(mockTemplate); + const mockSession = new PromptSession(mockTemplate, createMockMcpFs()); const mockProjectConfig = { project: { defaultPort: 4200 @@ -531,7 +542,7 @@ describe("Unit - PromptSession", () => { getProjectLibrary: mockProjectLibrary, getProjectLibraryByName: mockProjectLibrary }); - const mockSession = new PromptSession(mockTemplate); + const mockSession = new PromptSession(mockTemplate, createMockMcpFs()); const mockProjectConfig = { packagesInstalled: true, project: { @@ -649,12 +660,12 @@ describe("Unit - PromptSession", () => { getProjectLibraryNames: projectLibraries, getProjectLibraryByName: mockProjectLibrary }); - const mockSession = new PromptSession(mockTemplate); + const mockSession = new PromptSession(mockTemplate, createMockMcpFs()); const mockProjectConfig = { project: { defaultPort: 4200 } - } as unknown as Config; + } as unknown as Config; spyOn(ProjectConfig, "localConfig").and.returnValue(mockProjectConfig); spyOn(mockSession, "chooseActionLoop").and.callThrough(); spyOn(Util, "log"); @@ -731,7 +742,7 @@ describe("Unit - PromptSession", () => { } as unknown as Config; spyOn(ProjectConfig, "localConfig").and.returnValue(mockProjectConfig); spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(true); - const mockSession = new PromptSession(mockTemplate); + const mockSession = new PromptSession(mockTemplate, createMockMcpFs()); spyOn(mockSession, "chooseActionLoop").and.callThrough(); spyOn(InquirerWrapper, "select").and.returnValues( Promise.resolve("Complete & Run"), @@ -773,7 +784,7 @@ describe("Unit - PromptSession", () => { spyOn(ProjectConfig, "localConfig").and.returnValue(mockProjectConfig); spyOn(ProjectConfig, "setConfig"); - const mockSession = new PromptSession({} as any); + const mockSession = new PromptSession({} as any, createMockMcpFs()); spyOn(mockSession as any, "generateActionChoices").and.returnValues([]); spyOn(mockSession as any, "getUserInput").and.returnValues( Promise.resolve("Complete & Run"), diff --git a/spec/unit/ai-config-spec.ts b/spec/unit/ai-config-spec.ts new file mode 100644 index 000000000..b1716684c --- /dev/null +++ b/spec/unit/ai-config-spec.ts @@ -0,0 +1,129 @@ +import * as path from "path"; +import { GoogleAnalytics, IFileSystem, Util } from "@igniteui/cli-core"; +import { configureVsCode } from "../../packages/cli/lib/commands/ai-config"; +import { default as aiConfig } from "../../packages/cli/lib/commands/ai-config"; + +const IGNITEUI_SERVER_KEY = "igniteui-cli"; +const IGNITEUI_THEMING_SERVER_KEY = "igniteui-theming"; +const igniteuiServer = { command: "npx", args: ["-y", "igniteui-cli@next", "mcp"] }; +const igniteuiThemingServer = { command: "npx", args: ["-y", "igniteui-theming", "igniteui-theming-mcp"] }; + +function createMockFs(existingContent?: string): IFileSystem { + return { + fileExists: jasmine.createSpy("fileExists"), + readFile: existingContent + ? jasmine.createSpy("readFile").and.returnValue(existingContent) + : jasmine.createSpy("readFile").and.throwError("ENOENT"), + writeFile: jasmine.createSpy("writeFile"), + mkdir: jasmine.createSpy("mkdir"), + directoryExists: jasmine.createSpy("directoryExists"), + glob: jasmine.createSpy("glob").and.returnValue([]) + }; +} + +describe("Unit - ai-config command", () => { + const configPath = path.join(process.cwd(), ".vscode", "mcp.json"); + + beforeAll(() => { + spyOn(GoogleAnalytics, "post"); + }); + + beforeEach(() => { + spyOn(Util, "log"); + spyOn(Util, "greenCheck").and.returnValue("✓"); + }); + + function writtenConfig(mockFs: IFileSystem): Record { + const content = (mockFs.writeFile as jasmine.Spy).calls.mostRecent().args[1] as string; + return JSON.parse(content); + } + + describe("configureVsCode", () => { + it("creates .vscode/mcp.json with both servers when file does not exist", () => { + const mockFs = createMockFs(); + + configureVsCode(mockFs); + + expect(mockFs.mkdir).toHaveBeenCalledWith(path.dirname(configPath), { recursive: true }); + expect(mockFs.writeFile).toHaveBeenCalled(); + const config = writtenConfig(mockFs); + expect((config.servers as any)[IGNITEUI_SERVER_KEY]).toEqual(igniteuiServer); + expect((config.servers as any)[IGNITEUI_THEMING_SERVER_KEY]).toEqual(igniteuiThemingServer); + }); + + it("adds both servers when file exists but servers object is empty", () => { + const mockFs = createMockFs(JSON.stringify({ servers: {} })); + + configureVsCode(mockFs); + + expect(mockFs.writeFile).toHaveBeenCalled(); + const config = writtenConfig(mockFs); + expect((config.servers as any)[IGNITEUI_SERVER_KEY]).toEqual(igniteuiServer); + expect((config.servers as any)[IGNITEUI_THEMING_SERVER_KEY]).toEqual(igniteuiThemingServer); + }); + + it("adds missing igniteui-theming server when only igniteui-cli is present", () => { + const mockFs = createMockFs(JSON.stringify({ + servers: { [IGNITEUI_SERVER_KEY]: igniteuiServer } + })); + + configureVsCode(mockFs); + + expect(mockFs.writeFile).toHaveBeenCalled(); + const config = writtenConfig(mockFs); + expect((config.servers as any)[IGNITEUI_SERVER_KEY]).toEqual(igniteuiServer); + expect((config.servers as any)[IGNITEUI_THEMING_SERVER_KEY]).toEqual(igniteuiThemingServer); + }); + + it("adds missing igniteui-cli server when only igniteui-theming is present", () => { + const mockFs = createMockFs(JSON.stringify({ + servers: { [IGNITEUI_THEMING_SERVER_KEY]: igniteuiThemingServer } + })); + + configureVsCode(mockFs); + + expect(mockFs.writeFile).toHaveBeenCalled(); + const config = writtenConfig(mockFs); + expect((config.servers as any)[IGNITEUI_SERVER_KEY]).toEqual(igniteuiServer); + expect((config.servers as any)[IGNITEUI_THEMING_SERVER_KEY]).toEqual(igniteuiThemingServer); + }); + + it("is a no-op and logs when both servers are already configured", () => { + const mockFs = createMockFs(JSON.stringify({ + servers: { + [IGNITEUI_SERVER_KEY]: igniteuiServer, + [IGNITEUI_THEMING_SERVER_KEY]: igniteuiThemingServer + } + })); + + configureVsCode(mockFs); + + expect(mockFs.writeFile).not.toHaveBeenCalled(); + expect(Util.log).toHaveBeenCalledWith(jasmine.stringContaining("already configured")); + }); + + it("preserves existing third-party servers when adding igniteui servers", () => { + const thirdPartyServer = { command: "node", args: ["server.js"] }; + const mockFs = createMockFs(JSON.stringify({ + servers: { "other-server": thirdPartyServer } + })); + + configureVsCode(mockFs); + + expect(mockFs.writeFile).toHaveBeenCalled(); + const config = writtenConfig(mockFs); + expect((config.servers as any)["other-server"]).toEqual(thirdPartyServer); + expect((config.servers as any)[IGNITEUI_SERVER_KEY]).toEqual(igniteuiServer); + expect((config.servers as any)[IGNITEUI_THEMING_SERVER_KEY]).toEqual(igniteuiThemingServer); + }); + }); + + describe("handler", () => { + it("posts analytics and calls configureVsCode", async () => { + await aiConfig.handler({ _: ["ai-config"], $0: "ig" }); + + expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "screenview", cd: "MCP" })); + expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "event", ec: "$ig ai-config" })); + }); + }); +}); diff --git a/spec/unit/packageResolve-spec.ts b/spec/unit/packageResolve-spec.ts index 0898399c7..1d396e5bc 100644 --- a/spec/unit/packageResolve-spec.ts +++ b/spec/unit/packageResolve-spec.ts @@ -15,6 +15,9 @@ class MockFileSystem implements IFileSystem { public writeFile(filePath: string, text: string): void { throw new Error("writeFile not implemented."); } + public mkdir(dirPath: string): void { + throw new Error("mkdir not implemented."); + } public directoryExists(dirPath: string): boolean { throw new Error("directoryExists not implemented."); } diff --git a/spec/unit/ts-transform/Mock-FS.ts b/spec/unit/ts-transform/Mock-FS.ts index 4385a678d..a644f6bc5 100644 --- a/spec/unit/ts-transform/Mock-FS.ts +++ b/spec/unit/ts-transform/Mock-FS.ts @@ -28,6 +28,10 @@ export class MockFS implements IFileSystem { this.fsMap.set(filePath, text); } + public mkdir(_dirPath: string, _options?: { recursive?: boolean }): void { + // no-op for in-memory mock + } + public directoryExists(dirPath: string): boolean { throw new Error('directoryExists is not implemented.'); } From 6bc22875e0bf4e1595a01b6580710cbe99373661 Mon Sep 17 00:00:00 2001 From: damyanpetev Date: Fri, 17 Apr 2026 18:58:00 +0300 Subject: [PATCH 07/15] refactor: reuse ai-config logic in prompt session --- packages/cli/lib/PromptSession.ts | 37 +++-------------------- packages/cli/lib/commands/ai-config.ts | 6 +++- packages/core/prompt/BasePromptSession.ts | 2 +- spec/unit/PromptSession-spec.ts | 37 +++++++++++------------ 4 files changed, 27 insertions(+), 55 deletions(-) diff --git a/packages/cli/lib/PromptSession.ts b/packages/cli/lib/PromptSession.ts index 074640271..e365992c1 100644 --- a/packages/cli/lib/PromptSession.ts +++ b/packages/cli/lib/PromptSession.ts @@ -1,20 +1,18 @@ import { - BasePromptSession, FsFileSystem, GoogleAnalytics, IFileSystem, InquirerWrapper, PackageManager, ProjectConfig, + BasePromptSession, GoogleAnalytics, InquirerWrapper, PackageManager, ProjectConfig, ProjectLibrary, PromptTaskContext, Task, Util } from "@igniteui/cli-core"; import * as path from "path"; import { default as add } from "./commands/add"; +import { configure as aiConfigure } from "./commands/ai-config"; import { default as start } from "./commands/start"; import { default as upgrade } from "./commands/upgrade"; import { TemplateManager } from "./TemplateManager"; export class PromptSession extends BasePromptSession { - private readonly mcpFs: IFileSystem; - - constructor(templateManager: TemplateManager, mcpFs: IFileSystem = new FsFileSystem()) { + constructor(templateManager: TemplateManager) { super(templateManager); - this.mcpFs = mcpFs; } public static async chooseTerm() { @@ -107,34 +105,7 @@ export class PromptSession extends BasePromptSession { } protected async configureMcp(): Promise { - const MCP_SERVER_KEY = "igniteui-mcp-server"; - let command: string; - let args: string[]; - try { - const pkgEntry = require.resolve("igniteui-mcp-server"); - command = "node"; - args = [pkgEntry]; - } catch { - command = "npx"; - args = ["-y", "igniteui-mcp-server"]; - } - const configPath = path.join(process.cwd(), ".vscode", "mcp.json"); - let config: { servers: Record } = { servers: {} }; - try { - config = JSON.parse(this.mcpFs.readFile(configPath, "utf8")); - } catch { /* file doesn't exist yet */ } - config.servers = config.servers || {}; - - if (config.servers[MCP_SERVER_KEY]) { - Util.log(Util.greenCheck() + ` Ignite UI MCP server already configured in ${configPath}`); - return; - } - - // Preserve existing MCP entries and add ours - config.servers[MCP_SERVER_KEY] = { command, args }; - this.mcpFs.mkdir(path.dirname(configPath), { recursive: true }); - this.mcpFs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n"); - Util.log(Util.greenCheck() + ` MCP server configured in ${configPath}`); + aiConfigure(); } /** diff --git a/packages/cli/lib/commands/ai-config.ts b/packages/cli/lib/commands/ai-config.ts index 4102f07bb..d1bdc83c3 100644 --- a/packages/cli/lib/commands/ai-config.ts +++ b/packages/cli/lib/commands/ai-config.ts @@ -64,6 +64,10 @@ export function configureVsCode(fileSystem: IFileSystem = new FsFileSystem()): v Util.log(Util.greenCheck() + ` MCP servers configured in ${configPath}`); } +export function configure(fileSystem: IFileSystem = new FsFileSystem()): void { + configureVsCode(fileSystem); +} + const command: CommandModule = { command: "ai-config", describe: "Configure the Ignite UI MCP server for an AI client", @@ -80,7 +84,7 @@ const command: CommandModule = { ea: "client: vscode" }); - configureVsCode(); + configure(); } }; diff --git a/packages/core/prompt/BasePromptSession.ts b/packages/core/prompt/BasePromptSession.ts index eedd64630..8e314845a 100644 --- a/packages/core/prompt/BasePromptSession.ts +++ b/packages/core/prompt/BasePromptSession.ts @@ -421,7 +421,7 @@ export abstract class BasePromptSession { await this.upgradePackages(); } } - + await this.configureMcp(); const defaultPort = config.project.defaultPort; diff --git a/spec/unit/PromptSession-spec.ts b/spec/unit/PromptSession-spec.ts index 0411b6e16..5e2772807 100644 --- a/spec/unit/PromptSession-spec.ts +++ b/spec/unit/PromptSession-spec.ts @@ -1,24 +1,14 @@ -import { App, BaseTemplate, Config, ControlExtraConfigType, GoogleAnalytics, IFileSystem, InquirerWrapper, PackageManager, ProjectConfig, +import { App, BaseTemplate, Config, ControlExtraConfigType, GoogleAnalytics, InquirerWrapper, PackageManager, ProjectConfig, ProjectLibrary, ProjectTemplate, Template, Util } from "@igniteui/cli-core"; import * as path from "path"; import { default as add } from "../../packages/cli/lib/commands/add"; +import * as aiConfig from "../../packages/cli/lib/commands/ai-config"; import { default as start } from "../../packages/cli/lib/commands/start"; import { default as upgrade } from "../../packages/cli/lib/commands/upgrade"; import { PromptSession } from "../../packages/cli/lib/PromptSession"; import { TemplateManager } from "../../packages/cli/lib/TemplateManager"; import { Separator } from "@inquirer/prompts"; -function createMockMcpFs(): IFileSystem { - return { - fileExists: jasmine.createSpy("fileExists").and.returnValue(false), - readFile: jasmine.createSpy("readFile").and.throwError("ENOENT"), - writeFile: jasmine.createSpy("writeFile"), - mkdir: jasmine.createSpy("mkdir"), - directoryExists: jasmine.createSpy("directoryExists").and.returnValue(false), - glob: jasmine.createSpy("glob").and.returnValue([]) - }; -} - function createMockBaseTemplate(): BaseTemplate { return { id: "mock-template-id", @@ -110,6 +100,10 @@ describe("Unit - PromptSession", () => { spyOn(GoogleAnalytics, "post"); }); + beforeEach(() => { + spyOn(aiConfig, "configure"); + }); + // TODO: most of the tests use same setup - move the setup to beforeAll call it("chooseTerm - Should call itself if no term is passed.", async () => { spyOn(PromptSession, "chooseTerm").and.callThrough(); @@ -468,7 +462,7 @@ describe("Unit - PromptSession", () => { getProjectLibraryNames: projectLibraries, getProjectLibraryByName: mockProjectLibrary }); - const mockSession = new PromptSession(mockTemplate, createMockMcpFs()); + const mockSession = new PromptSession(mockTemplate); const mockProjectConfig = { project: { defaultPort: 4200 @@ -502,9 +496,10 @@ describe("Unit - PromptSession", () => { await mockSession.chooseActionLoop(mockProjectLibrary); expect(mockSession.chooseActionLoop).toHaveBeenCalledTimes(1); expect(InquirerWrapper.select).toHaveBeenCalledTimes(9); - expect(Util.log).toHaveBeenCalledTimes(4); + expect(Util.log).toHaveBeenCalledTimes(3); expect(PackageManager.flushQueue).toHaveBeenCalledWith(true); expect(start.start).toHaveBeenCalledTimes(1); + expect(aiConfig.configure).toHaveBeenCalledTimes(1); expect(add.addTemplate).toHaveBeenCalledTimes(1); expect(InquirerWrapper.input).toHaveBeenCalledWith({ type: "input", @@ -542,7 +537,7 @@ describe("Unit - PromptSession", () => { getProjectLibrary: mockProjectLibrary, getProjectLibraryByName: mockProjectLibrary }); - const mockSession = new PromptSession(mockTemplate, createMockMcpFs()); + const mockSession = new PromptSession(mockTemplate); const mockProjectConfig = { packagesInstalled: true, project: { @@ -579,9 +574,10 @@ describe("Unit - PromptSession", () => { expect(mockSession.chooseActionLoop).toHaveBeenCalledTimes(1); expect(InquirerWrapper.select).toHaveBeenCalledTimes(5); expect(InquirerWrapper.input).toHaveBeenCalledTimes(2); - expect(Util.log).toHaveBeenCalledTimes(4); + expect(Util.log).toHaveBeenCalledTimes(3); expect(PackageManager.flushQueue).toHaveBeenCalledWith(true); expect(start.start).toHaveBeenCalledTimes(1); + expect(aiConfig.configure).toHaveBeenCalledTimes(1); expect(Util.getAvailableName).toHaveBeenCalledTimes(1); expect(add.addTemplate).toHaveBeenCalledTimes(1); expect(add.addTemplate).toHaveBeenCalledWith("Custom Template Name", mockSelectedTemplate); @@ -660,7 +656,7 @@ describe("Unit - PromptSession", () => { getProjectLibraryNames: projectLibraries, getProjectLibraryByName: mockProjectLibrary }); - const mockSession = new PromptSession(mockTemplate, createMockMcpFs()); + const mockSession = new PromptSession(mockTemplate); const mockProjectConfig = { project: { defaultPort: 4200 @@ -702,9 +698,10 @@ describe("Unit - PromptSession", () => { expect(InquirerWrapper.select).toHaveBeenCalledTimes(10); expect(InquirerWrapper.input).toHaveBeenCalledTimes(2); expect(InquirerWrapper.checkbox).toHaveBeenCalledTimes(1); - expect(Util.log).toHaveBeenCalledTimes(4); + expect(Util.log).toHaveBeenCalledTimes(3); expect(PackageManager.flushQueue).toHaveBeenCalledWith(true); expect(start.start).toHaveBeenCalledTimes(1); + expect(aiConfig.configure).toHaveBeenCalledTimes(1); expect(add.addTemplate).toHaveBeenCalledTimes(1); expect(InquirerWrapper.checkbox).toHaveBeenCalledWith({ type: "checkbox", @@ -742,7 +739,7 @@ describe("Unit - PromptSession", () => { } as unknown as Config; spyOn(ProjectConfig, "localConfig").and.returnValue(mockProjectConfig); spyOn(ProjectConfig, "hasLocalConfig").and.returnValue(true); - const mockSession = new PromptSession(mockTemplate, createMockMcpFs()); + const mockSession = new PromptSession(mockTemplate); spyOn(mockSession, "chooseActionLoop").and.callThrough(); spyOn(InquirerWrapper, "select").and.returnValues( Promise.resolve("Complete & Run"), @@ -784,7 +781,7 @@ describe("Unit - PromptSession", () => { spyOn(ProjectConfig, "localConfig").and.returnValue(mockProjectConfig); spyOn(ProjectConfig, "setConfig"); - const mockSession = new PromptSession({} as any, createMockMcpFs()); + const mockSession = new PromptSession({} as any); spyOn(mockSession as any, "generateActionChoices").and.returnValues([]); spyOn(mockSession as any, "getUserInput").and.returnValues( Promise.resolve("Complete & Run"), From ec5f5266d155c1748d4d6bbbe102ad1023952fc3 Mon Sep 17 00:00:00 2001 From: damyanpetev Date: Fri, 17 Apr 2026 19:01:52 +0300 Subject: [PATCH 08/15] refactor: rename prompt session method to match command --- packages/cli/lib/PromptSession.ts | 2 +- packages/core/prompt/BasePromptSession.ts | 6 +++--- .../ng-schematics/src/prompt/SchematicsPromptSession.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/lib/PromptSession.ts b/packages/cli/lib/PromptSession.ts index e365992c1..b92a4326e 100644 --- a/packages/cli/lib/PromptSession.ts +++ b/packages/cli/lib/PromptSession.ts @@ -104,7 +104,7 @@ export class PromptSession extends BasePromptSession { await upgrade.upgrade({ skipInstall: true, _: ["upgrade"], $0: "upgrade" }); } - protected async configureMcp(): Promise { + protected async configureAI(): Promise { aiConfigure(); } diff --git a/packages/core/prompt/BasePromptSession.ts b/packages/core/prompt/BasePromptSession.ts index 8e314845a..fbe2ae737 100644 --- a/packages/core/prompt/BasePromptSession.ts +++ b/packages/core/prompt/BasePromptSession.ts @@ -98,8 +98,8 @@ export abstract class BasePromptSession { /** Upgrade packages to use private Infragistics feed */ protected abstract upgradePackages(); - /** Configure the Ignite UI MCP server for the project */ - protected abstract configureMcp(): Promise; + /** Configure the Ignite UI AI tooling (MCP server) for the project */ + protected abstract configureAI(): Promise; /** * Get user name and set template's extra configurations if any @@ -422,7 +422,7 @@ export abstract class BasePromptSession { } } - await this.configureMcp(); + await this.configureAI(); const defaultPort = config.project.defaultPort; const port = await this.getUserInput({ diff --git a/packages/ng-schematics/src/prompt/SchematicsPromptSession.ts b/packages/ng-schematics/src/prompt/SchematicsPromptSession.ts index f6e4d622a..8babc54f7 100644 --- a/packages/ng-schematics/src/prompt/SchematicsPromptSession.ts +++ b/packages/ng-schematics/src/prompt/SchematicsPromptSession.ts @@ -58,7 +58,7 @@ export class SchematicsPromptSession extends BasePromptSession { // TODO? } - protected async configureMcp(): Promise { + protected async configureAI(): Promise { // No-op in schematics context } From a7d9551fbdf83eb81e6ca1a2365d7d1090595857 Mon Sep 17 00:00:00 2001 From: damyanpetev Date: Fri, 17 Apr 2026 19:04:20 +0300 Subject: [PATCH 09/15] refactor: rename ai-config mcp method --- packages/cli/lib/commands/ai-config.ts | 4 ++-- spec/unit/ai-config-spec.ts | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/cli/lib/commands/ai-config.ts b/packages/cli/lib/commands/ai-config.ts index d1bdc83c3..27d433534 100644 --- a/packages/cli/lib/commands/ai-config.ts +++ b/packages/cli/lib/commands/ai-config.ts @@ -41,7 +41,7 @@ function writeJson(filePath: string, data: unknown, fileSystem: IFileSystem): vo fileSystem.writeFile(filePath, JSON.stringify(data, null, 2) + "\n"); } -export function configureVsCode(fileSystem: IFileSystem = new FsFileSystem()): void { +export function configureMCP(fileSystem: IFileSystem = new FsFileSystem()): void { const configPath = getConfigPath(); const config = readJson(configPath, { servers: {} }, fileSystem); config.servers = config.servers || {}; @@ -65,7 +65,7 @@ export function configureVsCode(fileSystem: IFileSystem = new FsFileSystem()): v } export function configure(fileSystem: IFileSystem = new FsFileSystem()): void { - configureVsCode(fileSystem); + configureMCP(fileSystem); } const command: CommandModule = { diff --git a/spec/unit/ai-config-spec.ts b/spec/unit/ai-config-spec.ts index b1716684c..2c85799e8 100644 --- a/spec/unit/ai-config-spec.ts +++ b/spec/unit/ai-config-spec.ts @@ -1,6 +1,6 @@ import * as path from "path"; import { GoogleAnalytics, IFileSystem, Util } from "@igniteui/cli-core"; -import { configureVsCode } from "../../packages/cli/lib/commands/ai-config"; +import { configureMCP } from "../../packages/cli/lib/commands/ai-config"; import { default as aiConfig } from "../../packages/cli/lib/commands/ai-config"; const IGNITEUI_SERVER_KEY = "igniteui-cli"; @@ -38,11 +38,11 @@ describe("Unit - ai-config command", () => { return JSON.parse(content); } - describe("configureVsCode", () => { + describe("configureMCP", () => { it("creates .vscode/mcp.json with both servers when file does not exist", () => { const mockFs = createMockFs(); - configureVsCode(mockFs); + configureMCP(mockFs); expect(mockFs.mkdir).toHaveBeenCalledWith(path.dirname(configPath), { recursive: true }); expect(mockFs.writeFile).toHaveBeenCalled(); @@ -54,7 +54,7 @@ describe("Unit - ai-config command", () => { it("adds both servers when file exists but servers object is empty", () => { const mockFs = createMockFs(JSON.stringify({ servers: {} })); - configureVsCode(mockFs); + configureMCP(mockFs); expect(mockFs.writeFile).toHaveBeenCalled(); const config = writtenConfig(mockFs); @@ -67,7 +67,7 @@ describe("Unit - ai-config command", () => { servers: { [IGNITEUI_SERVER_KEY]: igniteuiServer } })); - configureVsCode(mockFs); + configureMCP(mockFs); expect(mockFs.writeFile).toHaveBeenCalled(); const config = writtenConfig(mockFs); @@ -80,7 +80,7 @@ describe("Unit - ai-config command", () => { servers: { [IGNITEUI_THEMING_SERVER_KEY]: igniteuiThemingServer } })); - configureVsCode(mockFs); + configureMCP(mockFs); expect(mockFs.writeFile).toHaveBeenCalled(); const config = writtenConfig(mockFs); @@ -96,7 +96,7 @@ describe("Unit - ai-config command", () => { } })); - configureVsCode(mockFs); + configureMCP(mockFs); expect(mockFs.writeFile).not.toHaveBeenCalled(); expect(Util.log).toHaveBeenCalledWith(jasmine.stringContaining("already configured")); @@ -108,7 +108,7 @@ describe("Unit - ai-config command", () => { servers: { "other-server": thirdPartyServer } })); - configureVsCode(mockFs); + configureMCP(mockFs); expect(mockFs.writeFile).toHaveBeenCalled(); const config = writtenConfig(mockFs); @@ -119,7 +119,7 @@ describe("Unit - ai-config command", () => { }); describe("handler", () => { - it("posts analytics and calls configureVsCode", async () => { + it("posts analytics and calls configureMCP", async () => { await aiConfig.handler({ _: ["ai-config"], $0: "ig" }); expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "screenview", cd: "MCP" })); From 27d184547dfedf2f2671473b4f08e254237457af Mon Sep 17 00:00:00 2001 From: damyanpetev Date: Fri, 17 Apr 2026 19:26:46 +0300 Subject: [PATCH 10/15] test(ai-config): spy on fs write to avoid writing to physical fs --- spec/unit/ai-config-spec.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/spec/unit/ai-config-spec.ts b/spec/unit/ai-config-spec.ts index 2c85799e8..abf53c57c 100644 --- a/spec/unit/ai-config-spec.ts +++ b/spec/unit/ai-config-spec.ts @@ -1,7 +1,7 @@ import * as path from "path"; import { GoogleAnalytics, IFileSystem, Util } from "@igniteui/cli-core"; import { configureMCP } from "../../packages/cli/lib/commands/ai-config"; -import { default as aiConfig } from "../../packages/cli/lib/commands/ai-config"; +import * as aiConfig from "../../packages/cli/lib/commands/ai-config"; const IGNITEUI_SERVER_KEY = "igniteui-cli"; const IGNITEUI_THEMING_SERVER_KEY = "igniteui-theming"; @@ -119,9 +119,15 @@ describe("Unit - ai-config command", () => { }); describe("handler", () => { - it("posts analytics and calls configureMCP", async () => { - await aiConfig.handler({ _: ["ai-config"], $0: "ig" }); + it("posts analytics and calls configure", async () => { + const fs = require("fs"); + spyOn(fs, "readFileSync").and.throwError(new Error("ENOENT")); + spyOn(fs, "mkdirSync"); + spyOn(fs, "writeFileSync"); + await aiConfig.default.handler({ _: ["ai-config"], $0: "ig" }); + + expect(Util.log).toHaveBeenCalledWith(jasmine.stringContaining("MCP servers configured")); expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "screenview", cd: "MCP" })); expect(GoogleAnalytics.post).toHaveBeenCalledWith(jasmine.objectContaining({ t: "event", ec: "$ig ai-config" })); }); From 9fcbcb2828808dc3e9408c6ef57c7f5a791e224c Mon Sep 17 00:00:00 2001 From: damyanpetev Date: Fri, 17 Apr 2026 19:40:18 +0300 Subject: [PATCH 11/15] refactor(fs): roll directory create into write file directly --- packages/cli/lib/commands/ai-config.ts | 1 - packages/core/types/FileSystem.ts | 1 - packages/core/util/FileSystem.ts | 7 +++--- .../ng-schematics/src/utils/NgFileSystem.ts | 4 --- spec/unit/FsFileSystem-spec.ts | 25 +++++++++++++++++++ spec/unit/ai-config-spec.ts | 3 +-- spec/unit/packageResolve-spec.ts | 3 --- spec/unit/ts-transform/Mock-FS.ts | 4 --- 8 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/cli/lib/commands/ai-config.ts b/packages/cli/lib/commands/ai-config.ts index 27d433534..21768da8a 100644 --- a/packages/cli/lib/commands/ai-config.ts +++ b/packages/cli/lib/commands/ai-config.ts @@ -37,7 +37,6 @@ function readJson(filePath: string, fallback: T, fileSystem: IFileSystem): T } function writeJson(filePath: string, data: unknown, fileSystem: IFileSystem): void { - fileSystem.mkdir(path.dirname(filePath), { recursive: true }); fileSystem.writeFile(filePath, JSON.stringify(data, null, 2) + "\n"); } diff --git a/packages/core/types/FileSystem.ts b/packages/core/types/FileSystem.ts index 5be8938ba..8e9f33194 100644 --- a/packages/core/types/FileSystem.ts +++ b/packages/core/types/FileSystem.ts @@ -2,7 +2,6 @@ export interface IFileSystem { fileExists(filePath: string): boolean; readFile(filePath: string, encoding?: string): string; writeFile(filePath: string, text: string): void; - mkdir(dirPath: string, options?: { recursive?: boolean }): void; directoryExists(dirPath: string): boolean; /** diff --git a/packages/core/util/FileSystem.ts b/packages/core/util/FileSystem.ts index 6fe253aa9..cf90fd384 100644 --- a/packages/core/util/FileSystem.ts +++ b/packages/core/util/FileSystem.ts @@ -19,11 +19,12 @@ export class FsFileSystem implements IFileSystem { return fs.readFileSync(filePath).toString(); } public writeFile(filePath: string, text: string): void { + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } fs.writeFileSync(filePath, text); } - public mkdir(dirPath: string, options?: { recursive?: boolean }): void { - fs.mkdirSync(dirPath, options); - } public directoryExists(dirPath: string): boolean { try { return fs.statSync(dirPath).isDirectory(); diff --git a/packages/ng-schematics/src/utils/NgFileSystem.ts b/packages/ng-schematics/src/utils/NgFileSystem.ts index 51e250ba0..d5d652309 100644 --- a/packages/ng-schematics/src/utils/NgFileSystem.ts +++ b/packages/ng-schematics/src/utils/NgFileSystem.ts @@ -19,10 +19,6 @@ export class NgTreeFileSystem implements IFileSystem { : this.tree.create(filePath, text); } - public mkdir(_dirPath: string, _options?: { recursive?: boolean }): void { - // Angular Tree manages directories implicitly; no-op here. - } - public directoryExists(dirPath: string): boolean { const dir = this.tree.getDir(dirPath); return dir.subdirs.length || dir.subfiles.length ? true : false; diff --git a/spec/unit/FsFileSystem-spec.ts b/spec/unit/FsFileSystem-spec.ts index a9c991031..d957c4557 100644 --- a/spec/unit/FsFileSystem-spec.ts +++ b/spec/unit/FsFileSystem-spec.ts @@ -1,3 +1,4 @@ +import * as fs from "fs"; import * as glob from "glob"; import { FsFileSystem } from "../../packages/core/util/FileSystem"; @@ -8,6 +9,30 @@ describe("Unit - FsFileSystem", () => { fileSystem = new FsFileSystem(); }); + describe("writeFile", () => { + it("should create parent directories when they do not exist", () => { + spyOn(fs, "existsSync").and.returnValue(false); + spyOn(fs, "mkdirSync"); + spyOn(fs, "writeFileSync"); + + fileSystem.writeFile("/some/new/dir/file.json", "content"); + + expect(fs.mkdirSync).toHaveBeenCalledWith("/some/new/dir", { recursive: true }); + expect(fs.writeFileSync).toHaveBeenCalledWith("/some/new/dir/file.json", "content"); + }); + + it("should skip mkdir when parent directory already exists", () => { + spyOn(fs, "existsSync").and.returnValue(true); + spyOn(fs, "mkdirSync"); + spyOn(fs, "writeFileSync"); + + fileSystem.writeFile("/existing/dir/file.json", "content"); + + expect(fs.mkdirSync).not.toHaveBeenCalled(); + expect(fs.writeFileSync).toHaveBeenCalledWith("/existing/dir/file.json", "content"); + }); + }); + describe("glob", () => { it("should pass a forward-slash pattern to glob even when dirPath uses backslashes", () => { spyOn(glob, "sync").and.returnValue([]); diff --git a/spec/unit/ai-config-spec.ts b/spec/unit/ai-config-spec.ts index abf53c57c..d0d5b416b 100644 --- a/spec/unit/ai-config-spec.ts +++ b/spec/unit/ai-config-spec.ts @@ -15,7 +15,6 @@ function createMockFs(existingContent?: string): IFileSystem { ? jasmine.createSpy("readFile").and.returnValue(existingContent) : jasmine.createSpy("readFile").and.throwError("ENOENT"), writeFile: jasmine.createSpy("writeFile"), - mkdir: jasmine.createSpy("mkdir"), directoryExists: jasmine.createSpy("directoryExists"), glob: jasmine.createSpy("glob").and.returnValue([]) }; @@ -44,7 +43,6 @@ describe("Unit - ai-config command", () => { configureMCP(mockFs); - expect(mockFs.mkdir).toHaveBeenCalledWith(path.dirname(configPath), { recursive: true }); expect(mockFs.writeFile).toHaveBeenCalled(); const config = writtenConfig(mockFs); expect((config.servers as any)[IGNITEUI_SERVER_KEY]).toEqual(igniteuiServer); @@ -122,6 +120,7 @@ describe("Unit - ai-config command", () => { it("posts analytics and calls configure", async () => { const fs = require("fs"); spyOn(fs, "readFileSync").and.throwError(new Error("ENOENT")); + spyOn(fs, "existsSync").and.returnValue(false); spyOn(fs, "mkdirSync"); spyOn(fs, "writeFileSync"); diff --git a/spec/unit/packageResolve-spec.ts b/spec/unit/packageResolve-spec.ts index 1d396e5bc..0898399c7 100644 --- a/spec/unit/packageResolve-spec.ts +++ b/spec/unit/packageResolve-spec.ts @@ -15,9 +15,6 @@ class MockFileSystem implements IFileSystem { public writeFile(filePath: string, text: string): void { throw new Error("writeFile not implemented."); } - public mkdir(dirPath: string): void { - throw new Error("mkdir not implemented."); - } public directoryExists(dirPath: string): boolean { throw new Error("directoryExists not implemented."); } diff --git a/spec/unit/ts-transform/Mock-FS.ts b/spec/unit/ts-transform/Mock-FS.ts index a644f6bc5..4385a678d 100644 --- a/spec/unit/ts-transform/Mock-FS.ts +++ b/spec/unit/ts-transform/Mock-FS.ts @@ -28,10 +28,6 @@ export class MockFS implements IFileSystem { this.fsMap.set(filePath, text); } - public mkdir(_dirPath: string, _options?: { recursive?: boolean }): void { - // no-op for in-memory mock - } - public directoryExists(dirPath: string): boolean { throw new Error('directoryExists is not implemented.'); } From a4be5e0846ded2871b4e9fdb6f05be8d07791210 Mon Sep 17 00:00:00 2001 From: damyanpetev Date: Fri, 17 Apr 2026 19:47:49 +0300 Subject: [PATCH 12/15] chore: descriptions --- packages/cli/lib/commands/ai-config.ts | 2 +- packages/core/prompt/BasePromptSession.ts | 2 +- spec/acceptance/help-spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/lib/commands/ai-config.ts b/packages/cli/lib/commands/ai-config.ts index 21768da8a..abb200748 100644 --- a/packages/cli/lib/commands/ai-config.ts +++ b/packages/cli/lib/commands/ai-config.ts @@ -69,7 +69,7 @@ export function configure(fileSystem: IFileSystem = new FsFileSystem()): void { const command: CommandModule = { command: "ai-config", - describe: "Configure the Ignite UI MCP server for an AI client", + describe: "Configure Ignite UI AI tooling (MCP servers)", builder: (yargs) => yargs.usage(""), async handler(_argv: ArgumentsCamelCase) { GoogleAnalytics.post({ diff --git a/packages/core/prompt/BasePromptSession.ts b/packages/core/prompt/BasePromptSession.ts index fbe2ae737..abcc5927f 100644 --- a/packages/core/prompt/BasePromptSession.ts +++ b/packages/core/prompt/BasePromptSession.ts @@ -98,7 +98,7 @@ export abstract class BasePromptSession { /** Upgrade packages to use private Infragistics feed */ protected abstract upgradePackages(); - /** Configure the Ignite UI AI tooling (MCP server) for the project */ + /** Configure Ignite UI AI tooling (MCP servers) for the project */ protected abstract configureAI(): Promise; /** diff --git a/spec/acceptance/help-spec.ts b/spec/acceptance/help-spec.ts index 2f2c31119..615c4841d 100644 --- a/spec/acceptance/help-spec.ts +++ b/spec/acceptance/help-spec.ts @@ -26,7 +26,7 @@ describe("Help command", () => { upgrade-packages upgrades Ignite UI Packages mcp Starts the Ignite UI MCP server for AI assistant integration - ai-config Configure the Ignite UI MCP server for an AI client + ai-config Configure Ignite UI AI tooling (MCP servers) Options: -v, --version Show current Ignite UI CLI version [boolean] -h, --help Show help [boolean]`.replace(/\s/g, ""); From 59a6f16a306d44e47d9d4a86080bbba084cd109b Mon Sep 17 00:00:00 2001 From: damyanpetev Date: Fri, 17 Apr 2026 19:56:09 +0300 Subject: [PATCH 13/15] docs: update readme --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 23eed322e..942132a50 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,10 @@ Quickly create projects including [Ignite UI for Angular](https://www.infragisti - Step by step guide ### Supported frameworks - * jQuery * Angular * React + * Web Components + * jQuery ### Prerequisites The repository houses multiple packages and orchestrates building and publishing them with [lerna](https://github.com/lerna/lerna) and [yarn workspaces](https://yarnpkg.com/lang/en/docs/workspaces/). @@ -51,6 +52,7 @@ This monorepo contains several packages that combine into the `igniteui-cli`: * [Generate Ignite UI for React project](#generate-ignite-ui-for-react-project) * [Adding components](#adding-components) * [Build and run](#build-and-run) +* [Configure AI Tooling](#configure-ai-tooling) * [MCP Server](#mcp-server) * [Using with AI Assistants](#using-with-ai-assistants) * [Testing with MCP Inspector](#testing-with-mcp-inspector) @@ -141,6 +143,16 @@ ig build ig start ``` +## Configure AI Tooling + +To automatically configure Ignite UI MCP servers for VS Code, run: + +```bash +ig ai-config +``` + +This creates or updates `.vscode/mcp.json` in the current project with entries for both the [Ignite UI MCP](#mcp-server) and `igniteui-theming` MCP servers. Existing servers in the file are preserved. Newly projects are create with AI tooling configuration out of the box. + ## MCP Server The CLI includes a bundled [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that provides AI assistants with Ignite UI documentation search, API reference lookup, and scaffolding guidance for Angular, React, Blazor, and Web Components. @@ -158,7 +170,7 @@ ig mcp --debug # Enable debug logging to mcp-server.log ### Using with AI Assistants -Configure your MCP client (e.g., VS Code, Claude Desktop, Cursor) to use the CLI as the MCP server: +For VS Code, the `ig ai-config` command handles configuration automatically (see above). For other MCP clients (e.g., Claude Desktop, Cursor), configure them manually to use the CLI as the MCP server: ```json { From 17169fd29e9be6a321a5d05e4d67711cdc9b6625 Mon Sep 17 00:00:00 2001 From: damyanpetev Date: Fri, 17 Apr 2026 20:05:05 +0300 Subject: [PATCH 14/15] test: fix fs import so spy works --- spec/unit/FsFileSystem-spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/FsFileSystem-spec.ts b/spec/unit/FsFileSystem-spec.ts index d957c4557..f815784b1 100644 --- a/spec/unit/FsFileSystem-spec.ts +++ b/spec/unit/FsFileSystem-spec.ts @@ -1,4 +1,4 @@ -import * as fs from "fs"; +import fs = require("fs"); import * as glob from "glob"; import { FsFileSystem } from "../../packages/core/util/FileSystem"; From 82c1ce1d3af0767eec90d04a99bae57a388970f2 Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Fri, 17 Apr 2026 20:11:34 +0300 Subject: [PATCH 15/15] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- packages/cli/lib/commands/ai-config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 942132a50..2fed80987 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ To automatically configure Ignite UI MCP servers for VS Code, run: ig ai-config ``` -This creates or updates `.vscode/mcp.json` in the current project with entries for both the [Ignite UI MCP](#mcp-server) and `igniteui-theming` MCP servers. Existing servers in the file are preserved. Newly projects are create with AI tooling configuration out of the box. +This creates or updates `.vscode/mcp.json` in the current project with entries for both the [Ignite UI MCP](#mcp-server) and `igniteui-theming` MCP servers. Existing servers in the file are preserved. New projects are created with AI tooling configuration out of the box. ## MCP Server diff --git a/packages/cli/lib/commands/ai-config.ts b/packages/cli/lib/commands/ai-config.ts index abb200748..e7e183b4b 100644 --- a/packages/cli/lib/commands/ai-config.ts +++ b/packages/cli/lib/commands/ai-config.ts @@ -1,4 +1,4 @@ -import { FsFileSystem, GoogleAnalytics, IFileSystem, Util } from "@igniteui/cli-core"; +import { FsFileSystem, GoogleAnalytics, IFileSystem, Util } from "@igniteui/cli-core"; import { ArgumentsCamelCase, CommandModule } from "yargs"; import * as path from "path";