Skip to content
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# 15.1.1
* Update(ai-config): Adding AI coding assistance integration for Blazor projects.
# 15.1.1 (2026-05-18)

## What's Changed
* Updated `ig ai-config` command:
- Added AI coding assistance integration for Blazor projects.
- Now accepts a `--framework` / `-f` option for explicit framework specification. When omitted, the command still attempts to auto-detect the framework, but if detection fails it now also prompts the user for selection (in TTY).

# 15.1.0 (2026-05-13)

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/lib/PromptSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export class PromptSession extends BasePromptSession {
await upgrade.upgrade({ skipInstall: true, _: ["upgrade"], $0: "upgrade" });
}

protected override async configureAI(): Promise<void> {
await aiConfigure();
protected override async configureAI(frameworkId: string): Promise<void> {
await aiConfigure(frameworkId);
}

protected override templateSelectedTask(type: "component" | "view" = "component"): Task<PromptTaskContext> {
Expand Down
84 changes: 74 additions & 10 deletions packages/cli/lib/commands/ai-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
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 {
addMcpServers,
AI_AGENT_LABELS,
AI_AGENT_CHOICES,
type AIAgentTarget,
copyAgentInstructionFiles,
copyAISkillsToProject,
GoogleAnalytics,
InquirerWrapper,
Util,
type AiCodingAssistant,
AI_ASSISTANT_MCP_CONFIGS,
AI_ASSISTANT_CHOICES,
AI_ASSISTANT_LABELS,
detectFramework,
App,
type BaseTemplateManager,
TEMPLATE_MANAGER,
} from "@igniteui/cli-core";
import { ArgumentsCamelCase, CommandModule } from "yargs";

export function configureMCP(assistants: AiCodingAssistant[]): void {
Expand All @@ -14,8 +32,8 @@ export function configureMCP(assistants: AiCodingAssistant[]): void {
}
}

export function configureSkills(agents: AIAgentTarget[]): void {
const result = copyAISkillsToProject(agents);
export function configureSkills(agents: AIAgentTarget[], framework: string): void {
const result = copyAISkillsToProject(agents, framework);
if (result.found === 0) {
Util.warn("No AI skill files found. Make sure packages are installed (npm install) " +
"and your Ignite UI packages are up-to-date.", "yellow");
Expand All @@ -32,7 +50,12 @@ export function configureSkills(agents: AIAgentTarget[]): void {
type AIAgentOption = AIAgentTarget | "none";
type AIAssistantOption = AiCodingAssistant | "none";

export async function configure(agents: AIAgentOption[] = [], assistants: AIAssistantOption[] = [], skills = true): Promise<{ agents: AIAgentTarget[], assistants: AiCodingAssistant[] }> {
export async function configure(framework: string, agents: AIAgentOption[] = [], assistants: AIAssistantOption[] = [], skills = true): Promise<{ agents: AIAgentTarget[], assistants: AiCodingAssistant[] }> {
if (framework === "jquery") {
// currently not supported
return { agents: [], assistants: [] };
}

if (!agents.length) {
agents = await promptForAgents();
}
Expand All @@ -53,9 +76,9 @@ export async function configure(agents: AIAgentOption[] = [], assistants: AIAssi
Util.log("No AI configuration selected. Skipping.");
} else {
if (skills) {
configureSkills(resolvedAgents);
configureSkills(resolvedAgents, framework);
}
copyAgentInstructionFiles(resolvedAgents);
copyAgentInstructionFiles(resolvedAgents, framework);
}

return { agents: resolvedAgents, assistants: resolvedAssistants };
Expand All @@ -82,7 +105,7 @@ const AI_ASSISTANT_CHECKBOX_CHOICES = [
}))
];

export async function promptForAgents(): Promise<AIAgentOption[]> {
async function promptForAgents(): Promise<AIAgentOption[]> {
let selected: AIAgentOption[] = AI_AGENT_CHECKBOX_DEFAULTS;
if (Util.canPrompt()) {
const result = await InquirerWrapper.checkbox({
Expand All @@ -95,7 +118,7 @@ export async function promptForAgents(): Promise<AIAgentOption[]> {
return selected;
}

export async function promptForAssistant(): Promise<AIAssistantOption[]> {
async function promptForAssistant(): Promise<AIAssistantOption[]> {
let selected: AIAssistantOption[] = AI_ASSISTANT_CHECKBOX_DEFAULTS;
if (Util.canPrompt()) {
const result = await InquirerWrapper.checkbox({
Expand All @@ -108,6 +131,23 @@ export async function promptForAssistant(): Promise<AIAssistantOption[]> {
return selected;
}

/** delayed call so it's not immediate on module import for testing purposes */
function getTemplateManager(): BaseTemplateManager {
return App.container.get<BaseTemplateManager>(TEMPLATE_MANAGER);
}

/** Separate from the PromptSession prompt due to step by step config */
async function promptForFrameworkId(): Promise<string> {
const tm = getTemplateManager();
const frameRes: string = await InquirerWrapper.select({
name: "framework",
message: "Choose framework:",
choices: tm.getFrameworkNames(true),
default: "Angular"
});
return tm.getFrameworkByName(frameRes).id;
}

const command: CommandModule = {
command: "ai-config",
describe: "Configures Ignite UI AI tooling (MCP servers, AI coding skills and instructions)",
Expand All @@ -122,6 +162,12 @@ const command: CommandModule = {
describe: "Coding assistant(s) to configure MCP servers for",
choices: [...AI_ASSISTANT_CHOICES, "none"] as string[],
type: "array"
})
.option("framework", {
alias: "f",
describe: "Manually set project framework to configure AI for.",
choices: getTemplateManager()?.getFrameworkIds(true),
type: "string"
}),
async handler(argv: ArgumentsCamelCase) {
const agents = (argv.agents ?? []) as AIAgentOption[];
Expand All @@ -131,12 +177,30 @@ const command: CommandModule = {
cd: "Ai Config"
});

const result = await configure(agents, assistants);
let framework: string = argv.framework as string ?? detectFramework();
if (!framework) {
Util.log("Framework not provided and couldn't detect project from config or structure.");
if (Util.canPrompt()) {
framework = await promptForFrameworkId();
} else {
return Util.error("Please provide --framework argument.", "red");
}
}
if (!getTemplateManager()?.getFrameworkById(framework)) {
return Util.error("Framework not supported", "red");
}

if (framework === "jquery") {
Util.log("AI Config currently not available for jQuery projects.");
}

const result = await configure(framework, agents, assistants);

GoogleAnalytics.post({
t: "event",
ec: "$ig ai-config",
ea: `agent: ${result.agents.join(", ") || "none"}; assistant: ${result.assistants.join(", ") || "none"}`
ea: `agent: ${result.agents.join(", ") || "none"}; assistant: ${result.assistants.join(", ") || "none"}`,
cd1: framework
});
}
};
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/lib/commands/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ const command: NewCommandType = {
}

process.chdir(argv.name);
await configure(argv.agents as (AIAgentTarget | "none")[], argv.assistants as (AiCodingAssistant | "none")[]);
await configure(argv.framework, argv.agents as (AIAgentTarget | "none")[], argv.assistants as (AiCodingAssistant | "none")[]);
process.chdir("..");

Util.log(Util.greenCheck() + " Project Created");
Expand Down
4 changes: 2 additions & 2 deletions packages/core/prompt/BasePromptSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export abstract class BasePromptSession {
}
// move cwd to project folder
process.chdir(projectName);
await this.configureAI();
await this.configureAI(framework.id);
}
await this.chooseActionLoop(projLibrary);
//TODO: restore cwd?
Expand Down Expand Up @@ -102,7 +102,7 @@ export abstract class BasePromptSession {
protected abstract upgradePackages();

/** Configure Ignite UI AI tooling (MCP servers and AI coding skills) for the project */
protected abstract configureAI(): Promise<void>;
protected abstract configureAI(frameworkId: string): Promise<void>;

/**
* Get user name and set template's extra configurations if any
Expand Down
54 changes: 10 additions & 44 deletions packages/core/util/ai-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import type { BaseTemplateManager } from "../templates";
import { FS_TOKEN, IFileSystem } from "../types/FileSystem";
import { NPM_ANGULAR, NPM_REACT, NPM_WEBCOMPONENTS, resolvePackage, UPGRADEABLE_PACKAGES } from "../update/package-resolve";
import { App } from "./App";
import { detectBlazorFromCsproj, detectFrameworkFromPackageJson } from "./detect-framework";
import { FsFileSystem } from "./FileSystem";
import { TEMPLATE_MANAGER } from "./GlobalConstants";
import { ProjectConfig } from "./ProjectConfig";
import { Util } from "./Util";

export const AI_AGENT_CHOICES = ["generic", "claude", "copilot", "cursor", "codex", "windsurf", "gemini", "junie"] as const;
Expand Down Expand Up @@ -86,26 +84,10 @@ function resolveTemplateFilesDir(framework: string): string | null {
* Ignite UI packages that are relevant to the project's detected framework.
* Falls back to the bundled template skills when no npm package is installed.
*/
function resolveSkillsRoots(): string[] {
function resolveSkillsRoots(framework: string): string[] {
const fs = App.container.get<IFileSystem>(FS_TOKEN);
const roots: string[] = [];

let framework: string | null = null;
try {
if (ProjectConfig.hasLocalConfig()) {
framework = ProjectConfig.getConfig().project?.framework?.toLowerCase() ?? null;
}
} catch { /* config not readable – fall through to scan all */ }

// Blazor has no npm package — when explicitly configured, skip npm scanning
if (framework === "blazor") {
const filesDir = resolveTemplateFilesDir(framework);
if (filesDir) {
roots.push(path.join(filesDir, AI_SKILLS_DIR_NAME));
}
return roots;
}

const allPkgKeys = Object.keys(UPGRADEABLE_PACKAGES);
let candidates = new Set<string>();
if (framework === "angular") {
Expand All @@ -129,13 +111,9 @@ function resolveSkillsRoots(): string[] {

if (!roots.length) {
// if no root discovered, take the root from the appropriate project template files:
// Try Blazor (.csproj) detection only as a last resort, after npm scanning found nothing
framework ??= detectBlazorFromCsproj() ? "blazor" : detectFrameworkFromPackageJson();
if (framework) {
const filesDir = resolveTemplateFilesDir(framework);
if (filesDir) {
roots.push(path.join(filesDir, AI_SKILLS_DIR_NAME));
}
const filesDir = resolveTemplateFilesDir(framework);
if (filesDir) {
roots.push(path.join(filesDir, AI_SKILLS_DIR_NAME));
}
}

Expand All @@ -147,14 +125,14 @@ function resolveSkillsRoots(): string[] {
* skills directories for each of the given AI agents.
* @param agents – list of AI agent targets to copy skills for
*/
export function copyAISkillsToProject(agents: AIAgentTarget[]): AISkillsCopyResult {
export function copyAISkillsToProject(agents: AIAgentTarget[], framework: string): AISkillsCopyResult {
const result: AISkillsCopyResult = { found: 0, skipped: 0, failed: 0 };
// Source reads (glob + readFile) always use physical FS - skill files can
// come from sources outside the project virtual tree (external/global package):
const srcFs = new FsFileSystem();
// Destination writes respect the App FS (which may be virtual):
const destFs = App.container.get<IFileSystem>(FS_TOKEN);
const skillsRoots = resolveSkillsRoots();
const skillsRoots = resolveSkillsRoots(framework);

if (!skillsRoots.length) {
return result;
Expand Down Expand Up @@ -208,20 +186,8 @@ export function copyAISkillsToProject(agents: AIAgentTarget[]): AISkillsCopyResu
* Resolves the AGENTS.md source file content from the bundled project template files.
* AGENTS.md lives only in the template files/ directory, not in npm packages.
*/
function resolveAgentsContent(): string | null {
let framework: string | null = null;
try {
if (ProjectConfig.hasLocalConfig()) {
framework = ProjectConfig.getConfig().project?.framework?.toLowerCase() ?? null;
}
} catch { /* fall through */ }
framework ??= detectBlazorFromCsproj() ? "blazor" : detectFrameworkFromPackageJson();

if (!framework) {
return null;
}

const filesDir = resolveTemplateFilesDir(framework);
function resolveAgentsContent(framework: string): string | null {
const filesDir = resolveTemplateFilesDir(framework.toLowerCase());
if (!filesDir) {
return null;
}
Expand All @@ -238,8 +204,8 @@ function resolveAgentsContent(): string | null {
* each of the given agents.
* @param agents – list of AI agent targets to create instruction files for
*/
export function copyAgentInstructionFiles(agents: AIAgentTarget[]): void {
const content = resolveAgentsContent();
export function copyAgentInstructionFiles(agents: AIAgentTarget[], framework: string): void {
const content = resolveAgentsContent(framework);
if (!content) {
return;
}
Expand Down
Loading
Loading