Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
* **Template package updates:** updated scaffolded package references for Angular, React, and Web Components templates — `igniteui-angular` to `~21.2.5`, `igniteui-react` / `igniteui-react-grids` / `igniteui-react-dockmanager` to `~19.7.0`, `igniteui-webcomponents` to `~7.2.0`, and `igniteui-webcomponents-grids` to `~7.1.0`.
* **MCP — API reference links in docs:** Infragistics API documentation links embedded in the docs served by the MCP server are now rewritten to deterministic `get_api_reference` tool references (e.g. `mcp:get_api_reference?platform=webcomponents&component=IgcCheckboxComponent&member=checked`). AI assistants reading the docs resolve API links through the in-tool API lookup instead of fetching external HTML pages.
* **MCP `get_api_reference` — member-level lookup:** The `get_api_reference` tool now accepts an optional `member` parameter to return a single property, method, or event entry instead of the full component (for example `member="checked"`). It takes precedence over `section`, tolerates `Component#member` fragment-style references, reports the canonical member-name casing in the response, and returns a clear error when the requested member does not exist on the component.
* Improved `ig ai-config` command output:
- Shared AI config utilities now follow a return-only pattern — callers handle all logging, eliminating duplicate output when invoked from Angular schematics.
- The command now reports how the project framework was detected (e.g., from `package.json`, `.csproj`, or project configuration).
- Skills source discovery feedback: logs the installed package name and version (e.g., `igniteui-angular@21.1.0`) or indicates bundled skills are being used.

# 15.2.1 (2026-05-20)

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/lib/PromptSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class PromptSession extends BasePromptSession {
}

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

protected override templateSelectedTask(type: "component" | "view" = "component"): Task<PromptTaskContext> {
Expand Down
72 changes: 59 additions & 13 deletions packages/cli/lib/commands/ai-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
App,
type BaseTemplateManager,
TEMPLATE_MANAGER,
type AISkillsCopyResult,
} from "@igniteui/cli-core";
import { ArgumentsCamelCase, CommandModule } from "yargs";

Expand All @@ -25,32 +26,77 @@ export function configureMCP(assistants: AiCodingAssistant[]): void {
const modified = addMcpServers(assistant);

if (!modified) {
Util.log(` Ignite UI MCP servers already configured in ${mcpFilePath}`);
Util.log(Util.greenCheck() + ` Ignite UI MCP servers already configured in ${mcpFilePath}`);
} else {
Util.log(Util.greenCheck() + ` MCP servers configured in ${mcpFilePath}`);
}
}
}

export function configureSkills(agents: AIAgentTarget[], framework: string): void {
function logFileActions(result: AISkillsCopyResult): void {
for (const detail of result.details) {
if (detail.action === "created") {
Util.log(` Created ${detail.path}`);
} else if (detail.action === "updated") {
Util.log(` Updated ${detail.path}`);
}
}
}

function logResultSummary(result: AISkillsCopyResult, label: string): void {
if (result.failed > 0) {
Util.warn(`Failed to write ${result.failed} ${label} file(s) out of ${result.found}.`, "yellow");
} else if (result.found > 0 && result.skipped === result.found) {
Util.log(Util.greenCheck() + ` ${label} file(s) already up-to-date.`);
} else if (result.found > 0) {
const written = result.found - result.skipped;
Util.log(Util.greenCheck() + ` ${written} ${label} file(s) created or updated.`);
}
}

export function configureSkills(agents: AIAgentTarget[], framework: string, verbose = true): 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");
} else if (result.failed > 0) {
Util.warn(`Failed to write ${result.failed} skill file(s) out of ${result.found}.`, "yellow");
} else if (result.skipped === result.found) {
Util.log("Everything is already up-to-date.");
} else {
const written = result.found - result.skipped;
Util.log(Util.greenCheck() + ` ${written} AI skill file(s) created or updated.`);
return;
}

if (verbose) {
for (const source of result.sources) {
if (source.type === "package") {
Util.log(`Using skills from ${source.packageName}@${source.packageVersion}`);
} else {
Util.log("Using bundled Ignite UI skills");
}
}
logFileActions(result);
}

logResultSummary(result, "AI skill");
}

export function configureInstructions(agents: AIAgentTarget[], framework: string, verbose = true): void {
const result = copyAgentInstructionFiles(agents, framework);
if (verbose) {
logFileActions(result);
}
logResultSummary(result, "instruction");
}

type AIAgentOption = AIAgentTarget | "none";
type AIAssistantOption = AiCodingAssistant | "none";

export async function configure(framework: string, agents: AIAgentOption[] = [], assistants: AIAssistantOption[] = [], skills = true): Promise<{ agents: AIAgentTarget[], assistants: AiCodingAssistant[] }> {
interface ConfigureOptions {
agents?: AIAgentOption[];
assistants?: AIAssistantOption[];
skills?: boolean;
verbose?: boolean;
}

export async function configure(framework: string, options: ConfigureOptions = {}): Promise<{ agents: AIAgentTarget[], assistants: AiCodingAssistant[] }> {
const { skills = true, verbose = true } = options;
let { agents = [], assistants = [] } = options;
if (framework === "jquery") {
// currently not supported
return { agents: [], assistants: [] };
Expand All @@ -76,9 +122,9 @@ export async function configure(framework: string, agents: AIAgentOption[] = [],
Util.log("No AI configuration selected. Skipping.");
} else {
if (skills) {
configureSkills(resolvedAgents, framework);
configureSkills(resolvedAgents, framework, verbose);
}
copyAgentInstructionFiles(resolvedAgents, framework);
configureInstructions(resolvedAgents, framework, verbose);
}

return { agents: resolvedAgents, assistants: resolvedAssistants };
Expand Down Expand Up @@ -194,7 +240,7 @@ const command: CommandModule = {
Util.log("AI Config currently not available for jQuery projects.");
}

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

GoogleAnalytics.post({
t: "event",
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/lib/commands/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,10 @@ const command: NewCommandType = {
}

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

Util.log(Util.greenCheck() + " Project Created");
Expand Down
68 changes: 49 additions & 19 deletions packages/core/util/ai-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NPM_ANGULAR, NPM_REACT, NPM_WEBCOMPONENTS, resolvePackage, UPGRADEABLE_
import { App } from "./App";
import { FsFileSystem } from "./FileSystem";
import { TEMPLATE_MANAGER } from "./GlobalConstants";
import { Util } from "./Util";


export const AI_AGENT_CHOICES = ["generic", "claude", "copilot", "cursor", "codex", "windsurf", "gemini", "junie"] as const;
export type AIAgentTarget = typeof AI_AGENT_CHOICES[number];
Expand Down Expand Up @@ -57,12 +57,27 @@ export function getInstructionFilePath(target: AIAgentTarget): string {
return AI_AGENT_INSTRUCTION_FILES[target];
}

export type AIFileAction = "created" | "updated" | "skipped";

export interface AIFileActionDetail {
path: string;
action: AIFileAction;
}

export interface AISkillsCopyResult {
found: number;
skipped: number;
failed: number;
details: AIFileActionDetail[];
sources: SkillsSource[];
}

export type SkillsSourceType = "package" | "bundled";

export type SkillsSource =
| { type: "package"; packageName: string; packageVersion: string; path: string }
| { type: "bundled"; path: string };

export const AGENTS_TEMPLATE_FILE = "AGENTS.md";
export const AI_CONFIG_PROJECT_ID = "ai-config";
export const AI_SKILLS_DIR_NAME = "skills";
Expand All @@ -80,13 +95,13 @@ function resolveTemplateFilesDir(framework: string): string | null {
}

/**
* Returns the list of 'skills/' directory paths found in installed
* Returns the list of skills sources found in installed
* 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(framework: string): string[] {
function resolveSkillsRoots(framework: string): SkillsSource[] {
const fs = App.container.get<IFileSystem>(FS_TOKEN);
const roots: string[] = [];
const roots: SkillsSource[] = [];
Comment thread
damyanpetev marked this conversation as resolved.

const allPkgKeys = Object.keys(UPGRADEABLE_PACKAGES);
let candidates = new Set<string>();
Expand All @@ -101,19 +116,25 @@ function resolveSkillsRoots(framework: string): string[] {
candidates = new Set([...allPkgKeys, NPM_REACT, NPM_WEBCOMPONENTS]);
}

const srcFs = new FsFileSystem();
for (const pkg of candidates) {
const resolved = resolvePackage(pkg as keyof typeof UPGRADEABLE_PACKAGES);
const skillsRoot = `node_modules/${resolved}/${AI_SKILLS_DIR_NAME}`;
if (fs.directoryExists(skillsRoot) && !roots.includes(skillsRoot)) {
roots.push(skillsRoot);
if (fs.directoryExists(skillsRoot) && !roots.some(r => r.path === skillsRoot)) {
let version = "unknown";
try {
const pkgJson = JSON.parse(srcFs.readFile(`node_modules/${resolved}/package.json`));
version = pkgJson.version ?? "unknown";
} catch { /* version stays unknown */ }
roots.push({ type: "package", packageName: resolved, packageVersion: version, path: skillsRoot });
}
}

if (!roots.length) {
// if no root discovered, take the root from the appropriate project template files:
const filesDir = resolveTemplateFilesDir(framework);
if (filesDir) {
roots.push(path.join(filesDir, AI_SKILLS_DIR_NAME));
roots.push({ type: "bundled", path: path.join(filesDir, AI_SKILLS_DIR_NAME) });
}
}

Expand All @@ -126,24 +147,26 @@ function resolveSkillsRoots(framework: string): string[] {
* @param agents – list of AI agent targets to copy skills for
*/
export function copyAISkillsToProject(agents: AIAgentTarget[], framework: string): AISkillsCopyResult {
const result: AISkillsCopyResult = { found: 0, skipped: 0, failed: 0 };
const result: AISkillsCopyResult = { found: 0, skipped: 0, failed: 0, details: [], sources: [] };
// 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(framework);
const skillsSources = resolveSkillsRoots(framework);

if (!skillsRoots.length) {
if (!skillsSources.length) {
return result;
}

const multiRoot = skillsRoots.length > 1;
result.sources = skillsSources;
const multiRoot = skillsSources.length > 1;

for (const agent of agents) {
const outputDir = getSkillsDir(agent);

for (const skillsRoot of skillsRoots) {
for (const source of skillsSources) {
const skillsRoot = source.path;
const rawPaths = srcFs.glob(skillsRoot, "**/*");
const pkgDirName = multiRoot ? path.basename(path.dirname(skillsRoot)) : "";

Expand All @@ -164,13 +187,14 @@ export function copyAISkillsToProject(agents: AIAgentTarget[], framework: string
const existingContent = destFs.readFile(dest);
if (existingContent === newContent) {
result.skipped++;
result.details.push({ path: dest, action: "skipped" });
continue;
}
destFs.writeFile(dest, newContent);
Util.log(`${Util.greenCheck()} Updated ${dest}`);
result.details.push({ path: dest, action: "updated" });
} else {
destFs.writeFile(dest, newContent);
Util.log(`${Util.greenCheck()} Created ${dest}`);
result.details.push({ path: dest, action: "created" });
}
} catch {
result.failed++;
Expand Down Expand Up @@ -204,15 +228,17 @@ function resolveAgentsContent(framework: string): string | null {
* each of the given agents.
* @param agents – list of AI agent targets to create instruction files for
*/
export function copyAgentInstructionFiles(agents: AIAgentTarget[], framework: string): void {
export function copyAgentInstructionFiles(agents: AIAgentTarget[], framework: string): AISkillsCopyResult {
const result: AISkillsCopyResult = { found: 0, skipped: 0, failed: 0, details: [], sources: [] };
const content = resolveAgentsContent(framework);
if (!content) {
return;
return result;
}

const destFs = App.container.get<IFileSystem>(FS_TOKEN);

for (const agent of agents) {
result.found++;
const dest = getInstructionFilePath(agent);
const fileContent = agent === "cursor"
? `---\ncontext: true\npriority: high\nscope: project\n---\n${content}`
Expand All @@ -221,16 +247,20 @@ export function copyAgentInstructionFiles(agents: AIAgentTarget[], framework: st
if (destFs.fileExists(dest)) {
const existing = destFs.readFile(dest);
if (existing === fileContent) {
result.skipped++;
result.details.push({ path: dest, action: "skipped" });
continue;
}
destFs.writeFile(dest, fileContent);
Util.log(`${Util.greenCheck()} Updated ${dest}`);
result.details.push({ path: dest, action: "updated" });
} else {
destFs.writeFile(dest, fileContent);
Util.log(`${Util.greenCheck()} Created ${dest}`);
result.details.push({ path: dest, action: "created" });
}
} catch {
/* skip on error */
result.failed++;
}
}

return result;
}
14 changes: 11 additions & 3 deletions packages/core/util/detect-framework.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { App } from "./App";
import { IFileSystem, FS_TOKEN } from "../types/FileSystem";
import { ProjectConfig } from "./ProjectConfig";
import { Util } from "./Util";

type Framework = "angular" | "react" | "webcomponents" | "blazor" | "jquery";

/**
* Attempt to detect project framework by first checking for local cli-config,
* then falling back to package.json analysis of `detectFrameworkFromPackageJson()`.
* then falling back to .csproj / package.json analysis.
* Logs the detection source when a framework is found.
* @returns The detected framework Id or `null` if no framework could be detected.
*/
export function detectFramework(): Framework | null {
let framework: Framework | null = null;
try {
// try project config first:
if (ProjectConfig.hasLocalConfig()) {
framework = ProjectConfig.getConfig().project?.framework?.toLowerCase() as Framework ?? null;
}
Comment thread
damyanpetev marked this conversation as resolved.
Expand Down Expand Up @@ -54,13 +55,16 @@ export function detectFrameworkFromPackageJson(): Exclude<Framework, "jquery"> |
]);

if (deps.has("@angular/core")) {
Util.log("Detected Angular project (from package.json)");
return "angular";
}
if (deps.has("react")) {
Util.log("Detected React project (from package.json)");
return "react";
}

// for now assume webcomponents as default fallback
Util.log("Assuming Web Components (no Angular/React deps found in package.json)");
return "webcomponents";
Comment on lines 66 to 68
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the log message to "Assuming Web Components (no Angular/React deps found in package.json)" and adjusted the matching test expectation in commit fix: update misleading WebComponents fallback log message.

}

Expand Down Expand Up @@ -135,6 +139,10 @@ export function detectBlazorFromCsproj(): boolean {
}
}

return csprojFiles.some(csproj => isBlazorProject(fs, csproj));
const detected = csprojFiles.some(csproj => isBlazorProject(fs, csproj));
if (detected) {
Util.log("Detected Blazor project (from .csproj)");
}
return detected;
}
//#endregion Blazor detection
2 changes: 1 addition & 1 deletion spec/unit/PromptSession-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ describe("Unit - PromptSession", () => {
expect(Util.log).toHaveBeenCalledWith(" Project structure generated.");
expect(Util.gitInit).toHaveBeenCalled();
expect(mockSession.chooseActionLoop).toHaveBeenCalled();
expect(aiConfig.configure).toHaveBeenCalledOnceWith("angular");
expect(aiConfig.configure).toHaveBeenCalledOnceWith("angular", jasmine.objectContaining({ verbose: false }));
});
it("start - New project - missing IFs", async () => {
const mockProject = {
Expand Down
Loading