Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions packages/cli/lib/PromptSession.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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";
Expand All @@ -10,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() {
Expand Down Expand Up @@ -103,6 +106,37 @@ export class PromptSession extends BasePromptSession {
await upgrade.upgrade({ skipInstall: true, _: ["upgrade"], $0: "upgrade" });
}

protected async configureMcp(): Promise<void> {
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"];
Comment on lines +111 to +119
}
const configPath = path.join(process.cwd(), ".vscode", "mcp.json");
let config: { servers: Record<string, { command: string; args: string[] }> } = { 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}`);
Comment on lines +121 to +137
}

/**
* Get user name and set template's extra configurations if any
* @param projectLibrary to add component to
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
add,
ADD_COMMAND_NAME,
ALL_COMMANDS,
aiConfig,
build,
config,
doc,
Expand Down Expand Up @@ -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: {
Expand Down
87 changes: 87 additions & 0 deletions packages/cli/lib/commands/ai-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { FsFileSystem, GoogleAnalytics, IFileSystem, Util } from "@igniteui/cli-core";
import { ArgumentsCamelCase, CommandModule } from "yargs";
import * as path from "path";

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"]
};

interface McpServerEntry {
command: string;
args: string[];
}

interface VsCodeMcpConfig {
servers: Record<string, McpServerEntry>;
}

function getConfigPath(): string {
return path.join(process.cwd(), ".vscode", "mcp.json");
}

function readJson<T>(filePath: string, fallback: T, fileSystem: IFileSystem): T {
try {
return JSON.parse(fileSystem.readFile(filePath)) as T;
} catch {
return fallback;
}
}

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");
}

export function configureVsCode(fileSystem: IFileSystem = new FsFileSystem()): void {
const configPath = getConfigPath();
const config = readJson<VsCodeMcpConfig>(configPath, { servers: {} }, fileSystem);
config.servers = config.servers || {};

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;
}

if (!modified) {
Util.log(` Ignite UI MCP servers already configured in ${configPath}`);
return;
}
writeJson(configPath, config, fileSystem);
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) => yargs.usage(""),
async handler(_argv: ArgumentsCamelCase) {
GoogleAnalytics.post({
t: "screenview",
cd: "MCP"
});

GoogleAnalytics.post({
t: "event",
ec: "$ig ai-config",
ea: "client: vscode"
});

configureVsCode();
}
};

export default command;
1 change: 1 addition & 0 deletions packages/cli/lib/commands/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/lib/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/prompt/BasePromptSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;

/**
* Get user name and set template's extra configurations if any
* @param projectLibrary to add component to
Expand Down Expand Up @@ -418,6 +421,8 @@ export abstract class BasePromptSession {
await this.upgradePackages();
}
}

await this.configureMcp();
Comment on lines +424 to +425

const defaultPort = config.project.defaultPort;
const port = await this.getUserInput({
Expand Down
1 change: 1 addition & 0 deletions packages/core/types/FileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/core/util/FileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions packages/ng-schematics/src/prompt/SchematicsPromptSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export class SchematicsPromptSession extends BasePromptSession {
// TODO?
}

protected async configureMcp(): Promise<void> {
// No-op in schematics context
}

protected async upgradePackages() {
this.userAnswers.set("upgradePackages", true);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/ng-schematics/src/utils/NgFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions spec/acceptance/help-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, "");
Expand Down
31 changes: 21 additions & 10 deletions spec/unit/PromptSession-spec.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -491,7 +502,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);
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -568,7 +579,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);
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -691,7 +702,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);
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down
Loading
Loading