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
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
72 changes: 63 additions & 9 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,7 @@ 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 (!agents.length) {
agents = await promptForAgents();
}
Expand All @@ -53,9 +71,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 +100,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 +113,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 +126,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 +157,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,7 +172,20 @@ 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");
}

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

GoogleAnalytics.post({
t: "event",
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