Skip to content

Commit e86787b

Browse files
damyanpetevCopilot
andauthored
AI config logging enhancements (#1711)
* refactor(ai-config): move per-file logging from shared utilities to command This avoids duplicating per-file CRUD messages doubling when called from schematics. * feat(ai-config): log skills and framework detection source info * fix: update misleading WebComponents fallback log message in detectFrameworkFromPackageJson --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 6e743b6 commit e86787b

11 files changed

Lines changed: 678 additions & 53 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
* **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`.
55
* **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.
66
* **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.
7+
* Improved `ig ai-config` command output:
8+
- Shared AI config utilities now follow a return-only pattern — callers handle all logging, eliminating duplicate output when invoked from Angular schematics.
9+
- The command now reports how the project framework was detected (e.g., from `package.json`, `.csproj`, or project configuration).
10+
- 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.
711

812
# 15.2.1 (2026-05-20)
913

packages/cli/lib/PromptSession.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class PromptSession extends BasePromptSession {
3939
}
4040

4141
protected override async configureAI(frameworkId: string): Promise<void> {
42-
await aiConfigure(frameworkId);
42+
await aiConfigure(frameworkId, { verbose: false });
4343
}
4444

4545
protected override templateSelectedTask(type: "component" | "view" = "component"): Task<PromptTaskContext> {

packages/cli/lib/commands/ai-config.ts

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
App,
1717
type BaseTemplateManager,
1818
TEMPLATE_MANAGER,
19+
type AISkillsCopyResult,
1920
} from "@igniteui/cli-core";
2021
import { ArgumentsCamelCase, CommandModule } from "yargs";
2122

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

2728
if (!modified) {
28-
Util.log(` Ignite UI MCP servers already configured in ${mcpFilePath}`);
29+
Util.log(Util.greenCheck() + ` Ignite UI MCP servers already configured in ${mcpFilePath}`);
2930
} else {
3031
Util.log(Util.greenCheck() + ` MCP servers configured in ${mcpFilePath}`);
3132
}
3233
}
3334
}
3435

35-
export function configureSkills(agents: AIAgentTarget[], framework: string): void {
36+
function logFileActions(result: AISkillsCopyResult): void {
37+
for (const detail of result.details) {
38+
if (detail.action === "created") {
39+
Util.log(` Created ${detail.path}`);
40+
} else if (detail.action === "updated") {
41+
Util.log(` Updated ${detail.path}`);
42+
}
43+
}
44+
}
45+
46+
function logResultSummary(result: AISkillsCopyResult, label: string): void {
47+
if (result.failed > 0) {
48+
Util.warn(`Failed to write ${result.failed} ${label} file(s) out of ${result.found}.`, "yellow");
49+
} else if (result.found > 0 && result.skipped === result.found) {
50+
Util.log(Util.greenCheck() + ` ${label} file(s) already up-to-date.`);
51+
} else if (result.found > 0) {
52+
const written = result.found - result.skipped;
53+
Util.log(Util.greenCheck() + ` ${written} ${label} file(s) created or updated.`);
54+
}
55+
}
56+
57+
export function configureSkills(agents: AIAgentTarget[], framework: string, verbose = true): void {
3658
const result = copyAISkillsToProject(agents, framework);
3759
if (result.found === 0) {
3860
Util.warn("No AI skill files found. Make sure packages are installed (npm install) " +
3961
"and your Ignite UI packages are up-to-date.", "yellow");
40-
} else if (result.failed > 0) {
41-
Util.warn(`Failed to write ${result.failed} skill file(s) out of ${result.found}.`, "yellow");
42-
} else if (result.skipped === result.found) {
43-
Util.log("Everything is already up-to-date.");
44-
} else {
45-
const written = result.found - result.skipped;
46-
Util.log(Util.greenCheck() + ` ${written} AI skill file(s) created or updated.`);
62+
return;
4763
}
64+
65+
if (verbose) {
66+
for (const source of result.sources) {
67+
if (source.type === "package") {
68+
Util.log(`Using skills from ${source.packageName}@${source.packageVersion}`);
69+
} else {
70+
Util.log("Using bundled Ignite UI skills");
71+
}
72+
}
73+
logFileActions(result);
74+
}
75+
76+
logResultSummary(result, "AI skill");
77+
}
78+
79+
export function configureInstructions(agents: AIAgentTarget[], framework: string, verbose = true): void {
80+
const result = copyAgentInstructionFiles(agents, framework);
81+
if (verbose) {
82+
logFileActions(result);
83+
}
84+
logResultSummary(result, "instruction");
4885
}
4986

5087
type AIAgentOption = AIAgentTarget | "none";
5188
type AIAssistantOption = AiCodingAssistant | "none";
5289

53-
export async function configure(framework: string, agents: AIAgentOption[] = [], assistants: AIAssistantOption[] = [], skills = true): Promise<{ agents: AIAgentTarget[], assistants: AiCodingAssistant[] }> {
90+
interface ConfigureOptions {
91+
agents?: AIAgentOption[];
92+
assistants?: AIAssistantOption[];
93+
skills?: boolean;
94+
verbose?: boolean;
95+
}
96+
97+
export async function configure(framework: string, options: ConfigureOptions = {}): Promise<{ agents: AIAgentTarget[], assistants: AiCodingAssistant[] }> {
98+
const { skills = true, verbose = true } = options;
99+
let { agents = [], assistants = [] } = options;
54100
if (framework === "jquery") {
55101
// currently not supported
56102
return { agents: [], assistants: [] };
@@ -76,9 +122,9 @@ export async function configure(framework: string, agents: AIAgentOption[] = [],
76122
Util.log("No AI configuration selected. Skipping.");
77123
} else {
78124
if (skills) {
79-
configureSkills(resolvedAgents, framework);
125+
configureSkills(resolvedAgents, framework, verbose);
80126
}
81-
copyAgentInstructionFiles(resolvedAgents, framework);
127+
configureInstructions(resolvedAgents, framework, verbose);
82128
}
83129

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

197-
const result = await configure(framework, agents, assistants);
243+
const result = await configure(framework, { agents, assistants });
198244

199245
GoogleAnalytics.post({
200246
t: "event",

packages/cli/lib/commands/new.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,10 @@ const command: NewCommandType = {
162162
}
163163

164164
process.chdir(argv.name);
165-
await configure(argv.framework, argv.agents as (AIAgentTarget | "none")[], argv.assistants as (AiCodingAssistant | "none")[]);
165+
await configure(argv.framework, {
166+
agents: argv.agents as (AIAgentTarget | "none")[],
167+
assistants: argv.assistants as (AiCodingAssistant | "none")[]
168+
});
166169
process.chdir("..");
167170

168171
Util.log(Util.greenCheck() + " Project Created");

packages/core/util/ai-skills.ts

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { NPM_ANGULAR, NPM_REACT, NPM_WEBCOMPONENTS, resolvePackage, UPGRADEABLE_
55
import { App } from "./App";
66
import { FsFileSystem } from "./FileSystem";
77
import { TEMPLATE_MANAGER } from "./GlobalConstants";
8-
import { Util } from "./Util";
8+
99

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

60+
export type AIFileAction = "created" | "updated" | "skipped";
61+
62+
export interface AIFileActionDetail {
63+
path: string;
64+
action: AIFileAction;
65+
}
66+
6067
export interface AISkillsCopyResult {
6168
found: number;
6269
skipped: number;
6370
failed: number;
71+
details: AIFileActionDetail[];
72+
sources: SkillsSource[];
6473
}
6574

75+
export type SkillsSourceType = "package" | "bundled";
76+
77+
export type SkillsSource =
78+
| { type: "package"; packageName: string; packageVersion: string; path: string }
79+
| { type: "bundled"; path: string };
80+
6681
export const AGENTS_TEMPLATE_FILE = "AGENTS.md";
6782
export const AI_CONFIG_PROJECT_ID = "ai-config";
6883
export const AI_SKILLS_DIR_NAME = "skills";
@@ -80,13 +95,13 @@ function resolveTemplateFilesDir(framework: string): string | null {
8095
}
8196

8297
/**
83-
* Returns the list of 'skills/' directory paths found in installed
98+
* Returns the list of skills sources found in installed
8499
* Ignite UI packages that are relevant to the project's detected framework.
85100
* Falls back to the bundled template skills when no npm package is installed.
86101
*/
87-
function resolveSkillsRoots(framework: string): string[] {
102+
function resolveSkillsRoots(framework: string): SkillsSource[] {
88103
const fs = App.container.get<IFileSystem>(FS_TOKEN);
89-
const roots: string[] = [];
104+
const roots: SkillsSource[] = [];
90105

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

119+
const srcFs = new FsFileSystem();
104120
for (const pkg of candidates) {
105121
const resolved = resolvePackage(pkg as keyof typeof UPGRADEABLE_PACKAGES);
106122
const skillsRoot = `node_modules/${resolved}/${AI_SKILLS_DIR_NAME}`;
107-
if (fs.directoryExists(skillsRoot) && !roots.includes(skillsRoot)) {
108-
roots.push(skillsRoot);
123+
if (fs.directoryExists(skillsRoot) && !roots.some(r => r.path === skillsRoot)) {
124+
let version = "unknown";
125+
try {
126+
const pkgJson = JSON.parse(srcFs.readFile(`node_modules/${resolved}/package.json`));
127+
version = pkgJson.version ?? "unknown";
128+
} catch { /* version stays unknown */ }
129+
roots.push({ type: "package", packageName: resolved, packageVersion: version, path: skillsRoot });
109130
}
110131
}
111132

112133
if (!roots.length) {
113134
// if no root discovered, take the root from the appropriate project template files:
114135
const filesDir = resolveTemplateFilesDir(framework);
115136
if (filesDir) {
116-
roots.push(path.join(filesDir, AI_SKILLS_DIR_NAME));
137+
roots.push({ type: "bundled", path: path.join(filesDir, AI_SKILLS_DIR_NAME) });
117138
}
118139
}
119140

@@ -126,24 +147,26 @@ function resolveSkillsRoots(framework: string): string[] {
126147
* @param agents – list of AI agent targets to copy skills for
127148
*/
128149
export function copyAISkillsToProject(agents: AIAgentTarget[], framework: string): AISkillsCopyResult {
129-
const result: AISkillsCopyResult = { found: 0, skipped: 0, failed: 0 };
150+
const result: AISkillsCopyResult = { found: 0, skipped: 0, failed: 0, details: [], sources: [] };
130151
// Source reads (glob + readFile) always use physical FS - skill files can
131152
// come from sources outside the project virtual tree (external/global package):
132153
const srcFs = new FsFileSystem();
133154
// Destination writes respect the App FS (which may be virtual):
134155
const destFs = App.container.get<IFileSystem>(FS_TOKEN);
135-
const skillsRoots = resolveSkillsRoots(framework);
156+
const skillsSources = resolveSkillsRoots(framework);
136157

137-
if (!skillsRoots.length) {
158+
if (!skillsSources.length) {
138159
return result;
139160
}
140161

141-
const multiRoot = skillsRoots.length > 1;
162+
result.sources = skillsSources;
163+
const multiRoot = skillsSources.length > 1;
142164

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

146-
for (const skillsRoot of skillsRoots) {
168+
for (const source of skillsSources) {
169+
const skillsRoot = source.path;
147170
const rawPaths = srcFs.glob(skillsRoot, "**/*");
148171
const pkgDirName = multiRoot ? path.basename(path.dirname(skillsRoot)) : "";
149172

@@ -164,13 +187,14 @@ export function copyAISkillsToProject(agents: AIAgentTarget[], framework: string
164187
const existingContent = destFs.readFile(dest);
165188
if (existingContent === newContent) {
166189
result.skipped++;
190+
result.details.push({ path: dest, action: "skipped" });
167191
continue;
168192
}
169193
destFs.writeFile(dest, newContent);
170-
Util.log(`${Util.greenCheck()} Updated ${dest}`);
194+
result.details.push({ path: dest, action: "updated" });
171195
} else {
172196
destFs.writeFile(dest, newContent);
173-
Util.log(`${Util.greenCheck()} Created ${dest}`);
197+
result.details.push({ path: dest, action: "created" });
174198
}
175199
} catch {
176200
result.failed++;
@@ -204,15 +228,17 @@ function resolveAgentsContent(framework: string): string | null {
204228
* each of the given agents.
205229
* @param agents – list of AI agent targets to create instruction files for
206230
*/
207-
export function copyAgentInstructionFiles(agents: AIAgentTarget[], framework: string): void {
231+
export function copyAgentInstructionFiles(agents: AIAgentTarget[], framework: string): AISkillsCopyResult {
232+
const result: AISkillsCopyResult = { found: 0, skipped: 0, failed: 0, details: [], sources: [] };
208233
const content = resolveAgentsContent(framework);
209234
if (!content) {
210-
return;
235+
return result;
211236
}
212237

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

215240
for (const agent of agents) {
241+
result.found++;
216242
const dest = getInstructionFilePath(agent);
217243
const fileContent = agent === "cursor"
218244
? `---\ncontext: true\npriority: high\nscope: project\n---\n${content}`
@@ -221,16 +247,20 @@ export function copyAgentInstructionFiles(agents: AIAgentTarget[], framework: st
221247
if (destFs.fileExists(dest)) {
222248
const existing = destFs.readFile(dest);
223249
if (existing === fileContent) {
250+
result.skipped++;
251+
result.details.push({ path: dest, action: "skipped" });
224252
continue;
225253
}
226254
destFs.writeFile(dest, fileContent);
227-
Util.log(`${Util.greenCheck()} Updated ${dest}`);
255+
result.details.push({ path: dest, action: "updated" });
228256
} else {
229257
destFs.writeFile(dest, fileContent);
230-
Util.log(`${Util.greenCheck()} Created ${dest}`);
258+
result.details.push({ path: dest, action: "created" });
231259
}
232260
} catch {
233-
/* skip on error */
261+
result.failed++;
234262
}
235263
}
264+
265+
return result;
236266
}

packages/core/util/detect-framework.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { App } from "./App";
22
import { IFileSystem, FS_TOKEN } from "../types/FileSystem";
33
import { ProjectConfig } from "./ProjectConfig";
4+
import { Util } from "./Util";
45

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

78
/**
89
* Attempt to detect project framework by first checking for local cli-config,
9-
* then falling back to package.json analysis of `detectFrameworkFromPackageJson()`.
10+
* then falling back to .csproj / package.json analysis.
11+
* Logs the detection source when a framework is found.
1012
* @returns The detected framework Id or `null` if no framework could be detected.
1113
*/
1214
export function detectFramework(): Framework | null {
1315
let framework: Framework | null = null;
1416
try {
15-
// try project config first:
1617
if (ProjectConfig.hasLocalConfig()) {
1718
framework = ProjectConfig.getConfig().project?.framework?.toLowerCase() as Framework ?? null;
1819
}
@@ -54,13 +55,16 @@ export function detectFrameworkFromPackageJson(): Exclude<Framework, "jquery"> |
5455
]);
5556

5657
if (deps.has("@angular/core")) {
58+
Util.log("Detected Angular project (from package.json)");
5759
return "angular";
5860
}
5961
if (deps.has("react")) {
62+
Util.log("Detected React project (from package.json)");
6063
return "react";
6164
}
6265

6366
// for now assume webcomponents as default fallback
67+
Util.log("Assuming Web Components (no Angular/React deps found in package.json)");
6468
return "webcomponents";
6569
}
6670

@@ -135,6 +139,10 @@ export function detectBlazorFromCsproj(): boolean {
135139
}
136140
}
137141

138-
return csprojFiles.some(csproj => isBlazorProject(fs, csproj));
142+
const detected = csprojFiles.some(csproj => isBlazorProject(fs, csproj));
143+
if (detected) {
144+
Util.log("Detected Blazor project (from .csproj)");
145+
}
146+
return detected;
139147
}
140148
//#endregion Blazor detection

spec/unit/PromptSession-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ describe("Unit - PromptSession", () => {
315315
expect(Util.log).toHaveBeenCalledWith(" Project structure generated.");
316316
expect(Util.gitInit).toHaveBeenCalled();
317317
expect(mockSession.chooseActionLoop).toHaveBeenCalled();
318-
expect(aiConfig.configure).toHaveBeenCalledOnceWith("angular");
318+
expect(aiConfig.configure).toHaveBeenCalledOnceWith("angular", jasmine.objectContaining({ verbose: false }));
319319
});
320320
it("start - New project - missing IFs", async () => {
321321
const mockProject = {

0 commit comments

Comments
 (0)