diff --git a/.agents/README.md b/.agents/README.md new file mode 100644 index 000000000..ab7298bcf --- /dev/null +++ b/.agents/README.md @@ -0,0 +1,111 @@ +# Ignite UI CLI Skills Guide + +AI skills for maintaining the Ignite UI CLI's **project and component templates** and the **MCP server's tools**: what each one does, how they fit together, and how to maintain them. + +Each procedure is written once, as a skill in `.agents/skills/` ([Agent Skills](https://agentskills.io/specification) format), which VS Code Copilot discovers automatically: you run a workflow by asking for the task, and Copilot loads the matching skill. There are no custom agent files. Shared conventions, including the rules every workflow follows, are in [`AGENTS.md`](../AGENTS.md). + +--- + +## Ground Rules + +- **A human decides what gets merged.** Run locally, a workflow never commits: it ends with the working tree changed and a report, and a human reviews and commits. Run by a cloud agent on GitHub (e.g. Copilot cloud agent from an issue or the web UI), it commits to its own branch and opens a pull request whose description is the report; a human reviews and merges. The exact git rules are in `AGENTS.md`. +- **The smoke test is the proof.** Jasmine stubs package installs, so template work is verified with `scripts/smoke-test.sh`. +- **Stop and report rather than guess.** Major version updates, missing APIs, id conflicts and anything out of scope are reported, not worked around. + +--- + +## Skills + +| Skill | Kind | Writes | Use it for | +|---|---|---|---| +| [`igniteui-cli-template-version-update`](./skills/igniteui-cli-template-version-update/SKILL.md) | Workflow | Template pins | Update Ignite UI versions that `ig new` / `ig add` scaffold (patch/minor only). In `dry-run` mode: a read-only report of version drift and peer conflicts | +| [`igniteui-cli-component-template`](./skills/igniteui-cli-component-template/SKILL.md) | Workflow | New template folders | Add a component template for Angular, React and/or Web Components | +| [`igniteui-cli-changelog`](./skills/igniteui-cli-changelog/SKILL.md) | Workflow | `CHANGELOG.md` | Draft or write the release section for a version cut | +| [`igniteui-cli-ai-config-skills-refresh`](./skills/igniteui-cli-ai-config-skills-refresh/SKILL.md) | Workflow | Bundled `ai-config` skills | Mirror the framework repositories' `skills/` into the `ai-config` templates. In `dry-run` mode: what changed upstream and content issues | +| [`igniteui-cli-mcp-tool`](./skills/igniteui-cli-mcp-tool/SKILL.md) | Workflow | MCP server `src/`, tests, tool docs | Add an MCP server tool or change an existing one. Its `scripts/call-tool.mjs` calls a tool on the built server over stdio | +| [`igniteui-cli-smoke-test`](./skills/igniteui-cli-smoke-test/SKILL.md) | Reference | — | Build, running one spec file, specs that guard templates, running the smoke test, checking kept projects, triaging failures ("Triaging a Run"), editing the script | +| [`igniteui-cli-template-anatomy`](./skills/igniteui-cli-template-anatomy/SKILL.md) | Reference | — | Template layout per framework, model templates, every place versions are pinned, parity auditing ("Auditing Parity Across Frameworks") | + +The workflows triage their own smoke runs; use the smoke-test skill directly for a run you started by hand. + +--- + +## How They Fit Together + +Each workflow runs on its own and does not call the others. You chain them. + +```text +"what's missing?" (anatomy skill) ──► component-template workflow ──┐ +"what's stale?" (dry-run) ──► template-version-update workflow ──┤ + ▼ + each runs scripts/smoke-test.sh and triages its own failures + │ + (at release cut) ▼ + changelog workflow +``` + +--- + +## Quick Start + +| Goal | Ask | +|---|---| +| Update Web Components template versions to latest | "update webcomponents template versions" | +| Update to a specific version | "update template pins of igniteui-angular to 22.3.0" | +| See which pins are stale or conflicting | "dry-run template version update, all frameworks" | +| Add a template | "add a Tile Manager component template for angular" | +| See which templates are missing | "which component templates are missing per framework?" | +| Make sense of a failed smoke run | "triage output/smoke" | +| Release notes for 15.9.0 | "draft the changelog for 15.9.0, 2026-10-15" | +| Refresh the skills `ig ai-config` bundles | "refresh ai-config skills for all frameworks" | +| See how far bundled skills lag behind | "dry-run ai-config skills refresh, all frameworks" | +| Add an MCP tool | "add an MCP tool that returns a component's theming variables" | +| Change an MCP tool | "let get_doc accept several doc names in one call" | + +Copilot picks the skill from its description. To be explicit, or in any other assistant, name the file: "Follow `.agents/skills/igniteui-cli-changelog/SKILL.md`: draft 15.9.0, 2026-10-15". + +**Keeping the session small.** The update and template workflows produce long build and smoke output. Start a fresh session per workflow. + +--- + +## File Locations + +```text +AGENTS.md ← shared conventions, incl. the rules for maintenance workflows +.agents/ + README.md ← this guide + skills//SKILL.md ← every procedure and reference; discovered by Copilot + skills//scripts/ ← helper scripts a skill runs +``` + +--- + +## Permissions + +Every limit on the workflows (the git rules, the "Maintenance Workflows" rules in `AGENTS.md`, each workflow's "Limits") is an instruction the assistant follows, not a technical restriction. Review the diff before you commit or merge the agent's pull request; branch protection on `master` is what actually keeps a cloud agent off it. + +### Cloud Agent + +Copilot cloud agent reads `AGENTS.md` and the skills in `.agents/skills/` on its own. Its environment comes from `.github/workflows/copilot-setup-steps.yml` (install, build, Playwright Chromium), which takes effect only once it is on `master`. The agent's firewall allows the npm and yarn registries and `raw.githubusercontent.com`. `igniteui-cli-ai-config-skills-refresh` also calls `api.github.com` (two requests per framework); if that host is blocked, add it to the repository's Copilot firewall allowlist. Sessions end after 59 minutes, so ask for one framework or a scoped smoke run per task. + +--- + +## Maintenance Notes + +- Skill frontmatter carries `name`, `description` and `license`. Keep the Skills table in line with them. +- A skill's `description` is loaded in every session and decides when the skill is picked. Lead with the term people use for the task, keep the "WHEN TO USE / WHEN NOT TO USE" form, and name each trigger once; the procedure itself belongs in the body. +- Each fact lives in one skill. Workflows point to the reference skill (anatomy, smoke-test) at the step that needs it instead of restating it. When repository facts change (new framework root, new pin location, smoke-test flags), update that reference skill (and, for pin locations, `igniteui-cli-template-version-update/scripts/template-pins.mjs`, which encodes them). +- Boundaries shared by all workflows go in the "Maintenance Workflows" section of `AGENTS.md`, which is always loaded; skills do not link it. +- `dry-run` mode is read-only by instruction only. + +### Adding a Workflow + +Add one only for a recurring, multi-step task. If an existing reference skill plus a plain request covers it, extend that skill instead. + +1. Write `.agents/skills/igniteui-cli-/SKILL.md`: a one-line scope, inputs, "Limits" (only limits specific to this workflow, stated as what the workflow changes and what stops it; the shared ones, including the git rules, stay in `AGENTS.md`), numbered steps with each stop condition at the step where it arises, "Follow-up Corrections" when the right fix differs from simply fixing and re-validating, final report and self-validation. Point to a reference skill at the step that uses it; put it under "Read first" only when most steps need it. +2. Add it to the Skills and Quick Start tables above. + +### Adding a Reference Skill + +1. Write `.agents/skills//SKILL.md` (`name` must match the folder: lowercase letters, digits, hyphens). +2. Add it to the Skills table above. diff --git a/.agents/skills/igniteui-cli-ai-config-skills-refresh/SKILL.md b/.agents/skills/igniteui-cli-ai-config-skills-refresh/SKILL.md new file mode 100644 index 000000000..50111bfae --- /dev/null +++ b/.agents/skills/igniteui-cli-ai-config-skills-refresh/SKILL.md @@ -0,0 +1,113 @@ +--- +name: igniteui-cli-ai-config-skills-refresh +description: "Refresh the Ignite UI AI skills bundled in the ai-config project templates (what ig ai-config copies when the project has no Ignite UI package with its own skills/) from the skills/ folders of the igniteui-angular, igniteui-react, igniteui-webcomponents and igniteui-blazor repositories. WHEN TO USE: syncing or checking the bundled ai-config skills; asking how far they lag behind a branch or tag (dry-run). WHEN NOT TO USE: this repository's own maintenance skills under .agents/skills; editing a skill's content (that belongs in the framework repository)." +license: MIT +--- + +# AI-Config Skills Refresh + +Mirrors each framework repository's `skills/` folder into the **bundled skills** of the `ai-config` project templates. `ig ai-config` copies these into a user's project when no installed Ignite UI package ships its own `skills/` (`resolveSkillsRoots` in `packages/core/util/ai-skills.ts`). + +| Framework | Source | Bundled copy | +|---|---|---| +| angular | `IgniteUI/igniteui-angular` | `packages/igx-templates/igx-ts/projects/ai-config/files/skills` | +| react | `IgniteUI/igniteui-react` | `packages/cli/templates/react/igr-ts/projects/ai-config/files/skills` | +| webcomponents | `IgniteUI/igniteui-webcomponents` | `packages/cli/templates/webcomponents/igc-ts/projects/ai-config/files/skills` | +| blazor | `IgniteUI/igniteui-blazor` | `packages/cli/templates/blazor/igb/projects/ai-config/files/skills` | + +The release build (`npm run build-pack`, run by `npm-publish.yml`) re-copies the same folders from `master` through `scripts/update-skills.ts`, so the published package always ships `master`'s skills. This workflow keeps the **committed** copies (used by local runs, specs and reviewers) in step with what ships, or pins them to another ref when asked. + +--- + +## Input + +- **Frameworks**: one or more of `angular`, `react`, `webcomponents`, `blazor`, or `all`. +- **Ref** (optional): branch, tag or commit in the source repositories. Default `master`, matching the release build. +- **Mode**: + - `update` (default): Steps 1-5. + - `dry-run`: Steps 1-2, then the dry-run report. No files change. + +If the frameworks are missing, stop and ask. A ref other than `master` is a deliberate choice: repeat it in the report, because the next release build still copies from `master`. + +--- + +## Limits + +- Your changes are the files under the bundled `skills/` folders in the table, nothing else. The `ai-config` templates' other files (`AGENTS.md`, instruction files, `index.ts`) stay as they are. +- The bundled copy is an exact mirror of the source `skills/` folder. Content problems (broken links, bad frontmatter) are reported for the framework repository to fix, not edited here. +- Do not run `scripts/update-skills.ts` or `npm run build:update-skills`: they `git fetch` and `git checkout -B` inside the documentation submodules, which writes to git and moves submodule pointers. Use the script below, which reads from GitHub over HTTPS and runs no git commands. + +--- + +## Script + +[`scripts/sync-skills.mjs`](./scripts/sync-skills.mjs) (Node 18+). It makes two `api.github.com` requests per framework (commit and file list), through `gh` when `gh auth status` passes, otherwise directly with `GH_TOKEN` / `GITHUB_TOKEN` if set (without a token GitHub allows 60 requests an hour). File contents come from `raw.githubusercontent.com` and are checked against the tree's hashes. In a cloud agent session, an `ERROR` naming `api.github.com` means the firewall blocks it: report that for every framework and stop. + +```bash +node .agents/skills/igniteui-cli-ai-config-skills-refresh/scripts/sync-skills.mjs \ + [--framework angular,react,webcomponents,blazor] [--ref ] [--apply] [--json] +``` + +- Resolves the ref to a commit, lists `skills/` through the GitHub tree API and compares each file's git blob hash with the bundled copy (CRLF in the working tree is ignored). Only changed or new files are downloaded. +- Prints `+` added, `~` modified, `-` deleted per framework, with the source commit. +- `!` lines are content issues in the result: a `SKILL.md` without frontmatter, `name` or `description`; a `name` that differs from its folder; relative Markdown links that point to a missing file or outside the `skills/` folder (they break once copied into a project). +- Without `--apply` nothing is written. `--apply` writes added and modified files, deletes removed ones and prunes empty folders. + +--- + +## Steps + +### 1. Record the Starting Point + +Run `git status --short` and keep the output, so the report separates your changes from what was already modified. + +### 2. Compare + +Run the script without `--apply` for the frameworks and ref in scope. A framework with an `ERROR` line (ref not found, no `skills/` folder, GitHub access) is a stop item for that framework; the others continue. + +In `dry-run` mode, stop here. + +### 3. Apply + +Run the script again with `--apply` and the same arguments. Then run it once more without `--apply`: every framework in scope should report `added=0 modified=0 deleted=0`. + +### 4. Validate + +1. `npm run build` if it has not run in this checkout. The spec runs compiled code. +2. `npx jasmine spec/unit/ai-config-spec.js`. +3. End to end, per framework in scope, in a new empty temporary directory **outside the repository** (no `package.json`, so the bundled skills are used): + ```bash + node /packages/cli/bin/execute.js ai-config --framework --agents claude --assistants none + ``` + It must report ` AI skill file(s) created or updated`, with `` equal to the number of files in that framework's bundled `skills/` folder, and `.claude/skills/` must contain them. Delete the temporary directory afterwards. + +The smoke test does not cover `ai-config` (hidden project), so it is not part of this workflow. + +### 5. Follow-up Corrections + +When the user asks for a different ref or framework set, rerun Steps 2-4 for the affected frameworks only. + +--- + +## Final Report + +1. **Per framework**: source repository, ref and commit (from the script), and the counts added / modified / deleted / unchanged. List renamed or removed skill folders explicitly; they change what users get. +2. **Content issues** (`!` lines), each as an item for the framework repository, with the file and the problem. +3. **Stopped**: frameworks with an error, and why. +4. **Validation**: spec result, and per framework the `ai-config` file count against the bundled folder's. +5. **Proposed CHANGELOG line**, e.g. + `* **chore(templates):** Updated the AI skills bundled with \`ig ai-config\` for Angular and Web Components.` +6. **Diff**: `git status --short` and `git diff --stat`, distinguishing pre-existing changes from yours. + +### Dry-Run Report + +Items 1-3 above, plus the date. State that nothing was written. + +## Final Self-Validation + +In `dry-run` mode only the first item applies, plus: `git status --short` is unchanged from Step 1. + +- [ ] Git was used only as the `AGENTS.md` git rules allow for this session, and `scripts/update-skills.ts` was not run. +- [ ] Only files under the bundled `skills/` folders in scope changed. +- [ ] A rerun of the script without `--apply` reports no changes for every framework in scope. +- [ ] The spec passed and each framework's `ai-config` run copied every bundled file. diff --git a/.agents/skills/igniteui-cli-ai-config-skills-refresh/scripts/sync-skills.mjs b/.agents/skills/igniteui-cli-ai-config-skills-refresh/scripts/sync-skills.mjs new file mode 100644 index 000000000..c786431cb --- /dev/null +++ b/.agents/skills/igniteui-cli-ai-config-skills-refresh/scripts/sync-skills.mjs @@ -0,0 +1,203 @@ +#!/usr/bin/env node +// Mirror skills/ from the Ignite UI framework repos into the ai-config project templates. +// Commit and file list from api.github.com (gh CLI if logged in, else fetch with GH_TOKEN/GITHUB_TOKEN if set); file contents from raw.githubusercontent.com. +// Read-only unless --apply. Never runs git. +// Usage: node sync-skills.mjs [--framework angular,react,webcomponents,blazor] [--ref ] [--apply] [--json] +import { execFile, execSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs"; +import { dirname, join, posix, relative, sep } from "node:path"; +import { promisify } from "node:util"; + +const run = promisify(execFile); +const root = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim(); + +const SOURCES = { + angular: { repo: "IgniteUI/igniteui-angular", dest: "packages/igx-templates/igx-ts/projects/ai-config/files/skills" }, + react: { repo: "IgniteUI/igniteui-react", dest: "packages/cli/templates/react/igr-ts/projects/ai-config/files/skills" }, + webcomponents: { repo: "IgniteUI/igniteui-webcomponents", dest: "packages/cli/templates/webcomponents/igc-ts/projects/ai-config/files/skills" }, + blazor: { repo: "IgniteUI/igniteui-blazor", dest: "packages/cli/templates/blazor/igb/projects/ai-config/files/skills" } +}; + +const args = process.argv.slice(2); +const value = (name, def) => { + const i = args.indexOf(name); + return i > -1 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : def; +}; +const frameworks = value("--framework", Object.keys(SOURCES).join(",")).split(","); +const ref = value("--ref", "master"); +const apply = args.includes("--apply"); +const asJson = args.includes("--json"); + +const unknown = frameworks.filter((f) => !SOURCES[f]); +if (unknown.length) { + console.error(`Unknown framework: ${unknown.join(", ")}. Use ${Object.keys(SOURCES).join(", ")}.`); + process.exit(1); +} +if (!/^[\w.\-/]+$/.test(ref) || ref.startsWith("-")) { + console.error(`Invalid ref '${ref}'.`); + process.exit(1); +} + +let useGh; +async function gh(path) { + if (useGh === undefined) useGh = await run("gh", ["auth", "status"]).then(() => true, () => false); + if (useGh) { + const { stdout } = await run("gh", ["api", path], { maxBuffer: 256 * 1024 * 1024 }); + return JSON.parse(stdout); + } + const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN; + const res = await fetch(`https://api.github.com/${path}`, { + headers: { Accept: "application/vnd.github+json", ...(token && { Authorization: `Bearer ${token}` }) } + }); + if (!res.ok) throw new Error(`api.github.com/${path}: ${res.status} ${(await res.text()).slice(0, 200)}`); + return res.json(); +} + +async function raw(repo, commit, path) { + const url = `https://raw.githubusercontent.com/${repo}/${commit}/skills/${path.split("/").map(encodeURIComponent).join("/")}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`${url}: ${res.status}`); + return Buffer.from(await res.arrayBuffer()); +} + +const blobSha = (buf) => createHash("sha1").update(`blob ${buf.length}\0`).update(buf).digest("hex"); +function sameAsRemote(buf, sha) { + if (blobSha(buf) === sha) return true; + const lf = Buffer.from(buf.toString("binary").replace(/\r\n/g, "\n"), "binary"); + return lf.length !== buf.length && blobSha(lf) === sha; +} + +function walk(dir, base = dir, out = []) { + if (!existsSync(dir)) return out; + for (const name of readdirSync(dir)) { + const p = join(dir, name); + if (statSync(p).isDirectory()) walk(p, base, out); + else out.push(relative(base, p).split(sep).join("/")); + } + return out; +} + +function pruneEmptyDirs(dir, keep) { + for (const name of readdirSync(dir)) { + const p = join(dir, name); + if (statSync(p).isDirectory()) pruneEmptyDirs(p, false); + } + if (!keep && !readdirSync(dir).length) rmdirSync(dir); +} + +async function pool(items, size, fn) { + const queue = [...items]; + await Promise.all(Array.from({ length: size }, async () => { + while (queue.length) await fn(queue.shift()); + })); +} + +function validate(files) { + const issues = []; + const paths = new Set(Object.keys(files)); + for (const [path, buf] of Object.entries(files)) { + if (!path.endsWith(".md")) continue; + const text = buf.toString("utf8"); + if (posix.basename(path) === "SKILL.md") { + const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/); + const folder = posix.basename(posix.dirname(path)); + const name = fm?.[1].match(/^name:\s*["']?([^"'\r\n]+?)["']?\s*$/m)?.[1]; + if (!fm) issues.push({ path, issue: "SKILL.md has no frontmatter" }); + else { + if (!name) issues.push({ path, issue: "frontmatter has no name" }); + else if (name !== folder) issues.push({ path, issue: `name '${name}' does not match folder '${folder}'` }); + if (!/^description:/m.test(fm[1])) issues.push({ path, issue: "frontmatter has no description" }); + } + } + const body = text.replace(/```[\s\S]*?```/g, ""); + for (const m of body.matchAll(/\]\(([^)\s#]+)(?:#[^)]*)?\)/g)) { + const target = m[1]; + if (/^[a-z]+:/i.test(target) || target.startsWith("/")) continue; + const resolved = posix.normalize(posix.join(posix.dirname(path), decodeURIComponent(target))).replace(/\/$/, ""); + if (resolved.startsWith("..")) issues.push({ path, issue: `link leaves the skills folder: ${target}` }); + else if (!paths.has(resolved) && ![...paths].some((p) => p.startsWith(resolved + "/"))) { + issues.push({ path, issue: `broken link: ${target}` }); + } + } + } + return issues; +} + +const results = []; +for (const fw of frameworks) { + const { repo, dest } = SOURCES[fw]; + const destAbs = join(root, dest); + const result = { framework: fw, repo, ref, dest, added: [], modified: [], deleted: [], unchanged: 0, issues: [] }; + results.push(result); + try { + result.commit = (await gh(`repos/${repo}/commits/${encodeURIComponent(ref)}`)).sha; + const tree = await gh(`repos/${repo}/git/trees/${result.commit}?recursive=1`); + if (tree.truncated) throw new Error("tree listing truncated by GitHub"); + const remote = Object.fromEntries(tree.tree + .filter((e) => e.type === "blob" && e.path.startsWith("skills/")) + .map((e) => [e.path.slice("skills/".length), e.sha])); + if (!Object.keys(remote).length) throw new Error(`no skills/ folder at ${ref}`); + + const local = new Set(walk(destAbs)); + const files = {}; + const fetchList = []; + for (const [path, sha] of Object.entries(remote)) { + const abs = join(destAbs, path); + if (local.has(path)) { + const buf = readFileSync(abs); + if (sameAsRemote(buf, sha)) { + result.unchanged++; + files[path] = buf; + continue; + } + result.modified.push(path); + } else result.added.push(path); + fetchList.push([path, sha]); + } + result.deleted = [...local].filter((p) => !(p in remote)).sort(); + await pool(fetchList, 6, async ([path, sha]) => { + const buf = await raw(repo, result.commit, path); + if (blobSha(buf) !== sha) throw new Error(`content of skills/${path} does not match its tree entry`); + files[path] = buf; + }); + result.added.sort(); + result.modified.sort(); + result.issues = validate(files); + + if (apply) { + for (const path of [...result.added, ...result.modified]) { + const abs = join(destAbs, path); + mkdirSync(dirname(abs), { recursive: true }); + writeFileSync(abs, files[path]); + } + for (const path of result.deleted) unlinkSync(join(destAbs, path)); + if (existsSync(destAbs)) pruneEmptyDirs(destAbs, true); + result.applied = true; + } + } catch (e) { + result.error = (e.stderr || e.message || String(e)).toString().trim(); + } +} + +if (asJson) { + console.log(JSON.stringify(results, null, 2)); +} else { + for (const r of results) { + console.log(`\n== ${r.framework}: ${r.repo}@${r.ref}${r.commit ? ` (${r.commit.slice(0, 9)})` : ""} -> ${r.dest}`); + if (r.error) { + console.log(` ERROR: ${r.error}`); + continue; + } + const list = (label, items) => items.forEach((p) => console.log(` ${label} ${p}`)); + list("+", r.added); + list("~", r.modified); + list("-", r.deleted); + console.log(` added=${r.added.length} modified=${r.modified.length} deleted=${r.deleted.length} unchanged=${r.unchanged}${r.applied ? " (applied)" : ""}`); + r.issues.forEach((i) => console.log(` ! ${i.path}: ${i.issue}`)); + } + if (!apply && results.some((r) => r.added.length + r.modified.length + r.deleted.length)) { + console.log("\nDry run: no files written. Rerun with --apply to mirror these changes."); + } +} +process.exitCode = results.some((r) => r.error) ? 1 : 0; diff --git a/.agents/skills/igniteui-cli-changelog/SKILL.md b/.agents/skills/igniteui-cli-changelog/SKILL.md new file mode 100644 index 000000000..1e84a154e --- /dev/null +++ b/.agents/skills/igniteui-cli-changelog/SKILL.md @@ -0,0 +1,99 @@ +--- +name: igniteui-cli-changelog +description: "Changelog section for an Ignite UI CLI release cut, drafted from the PRs merged since the previous cut. WHEN TO USE: drafting or writing a release's CHANGELOG.md section. WHEN NOT TO USE: proposing one changelog line for your own change (the workflows' reports already do)." +license: MIT +--- + +# Changelog + +Release notes for **Ignite UI CLI** live in [`CHANGELOG.md`](../../../CHANGELOG.md). A section is written when a release is cut (commits like `chore(*): cut 15.8.1`), summarizing the PRs merged since the previous cut. + +--- + +## Input + +- **Version** and **date** of the release (e.g. `15.9.0`, `2026-10-15`). If missing, stop and ask. +- **Mode**: + - `draft` (default): output the section text only; the working tree stays unchanged. + - `write`: insert the section at the top of `CHANGELOG.md`. +- Optional **range**: a commit range to use instead of "since the last cut". + +--- + +## Limits + +- Your one change is the new section at the top of `CHANGELOG.md`, in `write` mode only. Existing release sections stay as they are. +- Versioning and publishing belong to the release process: `npm run release` and `npm run version` are never run. +- Every bullet describes something visible in the commits and their diffs. + +--- + +## Steps + +### 1. Find the Range + +The previous release is the most recent commit whose subject matches `cut ` (e.g. `git log --format='%h %s' | grep -E 'cut [0-9]+\.[0-9]+'`). Not every commit touching `CHANGELOG.md` is a cut. Range = that commit (exclusive) to `HEAD`, unless the user gave one. + +### 2. Collect Changes + +`git log --format='%h %s' `. Subjects end with `(#)`. For each PR-level commit: +- read the diff summary (`git show --stat `) and, where the subject is vague, the relevant diff; +- if the `gh` CLI is available and authenticated, `gh pr view ` for the description. Descriptions are often the empty PR template; then rely on the diff. + +### 3. Select and Group + +Classify each commit by its **effect on users**, not only by its prefix: + +| Commit | Treatment | +|---|---| +| Release cut, merge commit | Skip | +| Only touches `CHANGELOG.md` (e.g. rewording an older section) | Skip | +| Dependency bump of repository tooling (dev deps, lockfile, MCP server deps) | Fold into **one** `build(deps)` bullet listing notable packages and all PR links | +| Dependency or version change that alters what scaffolded projects get (template `package.json`, `constants.ts`, template `packages`) | Its own bullet as `chore()`, even if the commit says `build(deps)` | +| No conventional prefix | Infer type and scope from the diff; mention the inference under "Needs human input" | + +Merge several PRs about the same thing into one bullet with all PR links, for example: + +```markdown +* **build(deps):** Updated workspace dependencies, including `axios` 1.20.0 and `js-yaml` 4.3.2. [#1802](https://github.com/IgniteUI/igniteui-cli/pull/1802), [#1805](https://github.com/IgniteUI/igniteui-cli/pull/1805) +``` + +### 4. Write in the Existing Style + +Study the two most recent sections **before the target version** and match them: + +```markdown +# () + +## What's Changed +* **():** [#](https://github.com/IgniteUI/igniteui-cli/pull/) +``` + +- `` follows the commit's conventional type (`feat`, `fix`, `chore`, `docs`, `build`). +- `` names the **area users see**, normalized to: `angular`, `react`, `webcomponents`, `blazor`, `templates` (several frameworks), `cli`, `schematics`, `mcp`, `cd`, `deps`. Map commit scopes accordingly: `chore(cli)` that bumps Angular templates → `chore(angular)`; `feat(breadcrumb)` for a Web Components template → `feat(webcomponents)`; `wc` / `igc-ts` → `webcomponents`; `igr-ts` → `react`; `igx-ts` → `angular`. +- Describe the effect for users (what scaffolded projects get, what a command now does), not the implementation. +- For template additions, list the component names. For version updates, name the packages and ranges, wrapped in backticks. +- Order: lead with the most user-visible changes (new features, then fixes), keep `chore` items after them, and put `build(deps)` last. Older sections are not strictly consistent; do not reorder to an exact rule. + +### 5. Surfacing Problems + +If a commit's purpose cannot be determined from its subject, diff and PR, list it under "Needs human input" in your output instead of guessing. + +### 6. Follow-up Corrections + +When the user corrects something after the report (a reworded bullet, a different scope, an answer to "Needs human input"), change only the affected bullets. Do not re-collect the whole range unless the range itself changed. + +--- + +## Final Report + +- The section text (always). +- In `write` mode: confirmation it was inserted at the top, and `git diff --stat`. +- "Needs human input" list, if any. + +## Final Self-Validation + +- [ ] Git was used only as the `AGENTS.md` git rules allow for this session. +- [ ] In `draft` mode, no file changed; in `write` mode, only `CHANGELOG.md` changed and no existing section was altered. +- [ ] Every PR in the range is either in a bullet, deliberately skipped (release cut, merge, `CHANGELOG.md`-only), or listed under "Needs human input". +- [ ] Every bullet links its PR(s) and uses a normalized scope. diff --git a/.agents/skills/igniteui-cli-component-template/SKILL.md b/.agents/skills/igniteui-cli-component-template/SKILL.md new file mode 100644 index 000000000..f06800403 --- /dev/null +++ b/.agents/skills/igniteui-cli-component-template/SKILL.md @@ -0,0 +1,116 @@ +--- +name: igniteui-cli-component-template +description: "Add a component template (ig add ) for Angular, React and/or Web Components, verified against the pinned package version and validated end to end. WHEN TO USE: adding a component template to one or more frameworks. WHEN NOT TO USE: changing package versions (igniteui-cli-template-version-update); looking up template layout only (igniteui-cli-template-anatomy)." +license: MIT +--- + +# Component Template + +Adds the **component templates** users get with `ig add `, for Angular (`igx-ts`), React (`igr-ts`) and Web Components (`igc-ts`). + +Read first: [`igniteui-cli-template-anatomy`](../igniteui-cli-template-anatomy/SKILL.md) (layout, fields, placeholders, model templates). + +--- + +## Input + +- **Component**: e.g. "Tile Manager", "Rating". +- **Frameworks**: one or more of `angular`, `react`, `webcomponents`. +- **Variant** (optional): defaults to a single `default` variant showing basic usage. + +If either the component or the frameworks are missing, stop and ask. + +--- + +## Limits + +Your changes are the new template folders and nothing else. Stop and report when the work needs more than that: + +- Template base classes, `TemplateManager`, project templates and other components' templates stay as they are. +- Use an existing `groups.json` group. When none fits, stop and ask before adding or renaming one. +- Use only package versions already pinned in the framework. When the component needs a package pinned nowhere in the framework, stop and report; version choice belongs to the `igniteui-cli-template-version-update` workflow. +- Every component, prop, event and import in the template is one you verified in Step 2. +- Angular, React and Web Components only; jQuery and Blazor are out of scope. +- `CHANGELOG.md` is written at release time: propose a line in your report. + +--- + +## Steps + +### 1. Check What Already Exists + +For every framework (not only those in scope), look for an existing template for this component: + +- folder names and `this.id` values under the three framework roots; +- near-miss ids (`breadcrumb` / `breadcrumbs`, `nav-drawer` / `navigation-drawer`, `mask` / `mask-input`). + +Outcomes: +- Already exists in a target framework: stop and report; do not duplicate. +- Exists in another framework: **reuse that exact id**, group and naming. +- Exists in two other frameworks with different ids: stop and report the conflict; ask which id to use. + +### 2. Verify the Component in the Pinned Version + +For each target framework, find the package that provides the component and the version **currently pinned** (constants or project `package.json`). + +Verify the component's class or tag, the import path, and every prop/event you plan to use, against that version: + +1. **Preferred**: the Ignite UI MCP tools, if available in this session: `list_components`, `get_api_reference` (platform + component), `resolve_import`. +2. **Otherwise**: the package's type declarations. `npm pack @ --pack-destination `, extract it, and search the `.d.ts` files. Use a temporary directory outside the repository. + - Web Components element tag names (`htmlTagName`) are in the compiled JS (e.g. `esm2015/`), not in the `.d.ts`. + - Web Components attributes are the property names in dashed form (`chartTitle` → `chart-title`). Confirm in the compiled source when unsure. + +If the component is only available in a newer version than the one pinned, stop and report: a version update must happen first. + +### 3. Choose the Model Template + +Pick the closest **model template** in the **same framework** from the anatomy skill's list and copy its structure exactly (same files, same style, same test shape). For Angular, also pick native `igx` vs wrapped Web Component as the anatomy skill describes. + +When the same component already has a template in **another** framework: +- **structure and conventions** come from the same-framework sibling: file set, test shape, how data is organized, the `components` naming style (e.g. Web Components charts use `["PieChart"]`, not `["Pie Chart"]`); +- **identity and content** come from the other framework's template: `id`, display `name`, `group`, description wording, and the demo's sample data and props, so the component looks the same across frameworks. + +Name the sibling and any cross-framework template you used in the report. + +### 4. Create the Template + +Per framework, create the component `index.ts`, a `default` variant `index.ts` and its `files/`, following the anatomy skill: "Component Template Layout" for the files and every variant field, "Generated Files Per Framework" for placeholders and the test file. On top of that: + +- `listInComponentTemplates = true`. +- `packages`: when the needed package has no constant, use the same `name@range` string already pinned elsewhere in that framework and mention it in the report. +- The test file matches the sibling's shape. +- Keep the demo minimal: the component rendered with a few meaningful, verified props, and data written inline in the component. + +### 5. Validate + +Use the [`igniteui-cli-smoke-test`](../igniteui-cli-smoke-test/SKILL.md) skill for the commands below. + +1. `npm run build`. The smoke test and the specs run the compiled templates, so a stale build does not see your new template. +2. The template specs for each framework in scope (smoke-test skill, "Specs That Guard Templates"): ids, file collisions, template contracts. +3. Angular only: `npx eslint packages/igx-templates/igx-ts//index.ts packages/igx-templates/igx-ts//default/index.ts`. React and Web Components `index.ts` files are ignored by the repository lint config; their code is linted in the kept project. +4. Per framework: `scripts/smoke-test.sh -f --templates --keep --out output/smoke--`. +5. In each kept project (`output/smoke--//`), run every check in smoke-test's "Checking a Kept Project" table for that framework. +6. Delete the kept `output/smoke--*` directories when everything passes; keep them and say so when something fails. + +Fix failures caused by your template. When a failure points elsewhere (environment, project template), report it with evidence and leave that code as it is. + +--- + +## Final Report + +1. **Created**: per framework, the id, files created, the sibling used as model, packages registered. +2. **API verification**: how each API was verified (MCP tool or `.d.ts`), with the package version. +3. **Validation**: build result, the template specs, the Angular `index.ts` lint, and per framework the smoke result plus each kept-project check. +4. **Stopped or skipped**: with reasons. +5. **Proposed CHANGELOG line**, e.g. + `* **feat(react):** Added a React \`igr-ts\` template for Tile Manager.` +6. **Diff**: `git status --short`. + +## Final Self-Validation + +- [ ] Git was used only as the `AGENTS.md` git rules allow for this session. +- [ ] The id matches the same component's id in every other framework. +- [ ] `controlGroup` and the component `group` match an existing `groups.json` key. +- [ ] Every API used was verified against the pinned version. +- [ ] A test file exists, shaped like the sibling's, and it was run in the kept project and passed. +- [ ] Template specs, smoke test and every kept-project check passed for each framework, or failures are reported with evidence. diff --git a/.agents/skills/igniteui-cli-mcp-tool/SKILL.md b/.agents/skills/igniteui-cli-mcp-tool/SKILL.md new file mode 100644 index 000000000..c3604dae3 --- /dev/null +++ b/.agents/skills/igniteui-cli-mcp-tool/SKILL.md @@ -0,0 +1,174 @@ +--- +name: igniteui-cli-mcp-tool +description: "Add a tool to the Ignite UI MCP server (@igniteui/mcp-server) or change an existing one: input schema, handler, response text, description, tests and every doc that lists the tool. WHEN TO USE: adding, changing, renaming or removing an MCP tool or its parameters. WHEN NOT TO USE: the docs pipeline, database build or API-docs generation (the package's DEVELOPMENT.md)." +license: MIT +--- + +# MCP Tool + +Adds or changes a **tool** of the Ignite UI MCP server: `packages/igniteui-mcp/igniteui-doc-mcp`, published as `@igniteui/mcp-server` and started by `ig mcp`. Paths below are relative to that folder unless they start with `packages/`, `spec/` or `.agents/`. + +--- + +## Input + +- **Change**: a new tool, or the existing tool and what changes (a parameter, behavior, response text, description). +- For a new tool, also: the question it answers and the data behind it. + +If the change, or for a new tool its question and data, is missing, stop and ask. + +--- + +## Limits + +- Your changes are in `src/`, `spec/unit/mcp-runtime-spec.ts`, the docs in Step 6 and, for a doc tool, `../docs-backend/` and `../shared-fixtures/`. A new non-TypeScript asset under `src/tools/` (like `setup_*.md`) also needs its line in `scripts/build.ts`, the only thing that copies it into `dist/`; `constants.ts` reads those files at import, so a missing copy crashes the server at startup. +- Tool and parameter names are a public contract. Tool names are written into the AI skills `ig ai-config` copies from the upstream library repos (`scripts/update-skills.ts`), and `get_api_reference` with its `platform` / `component` / `member` parameters is baked into the docs database as `mcp:get_api_reference?…` links. For a rename or removal, stop and report every reference you find (`git grep -n ` from the repository root). +- The tool reads only data the package already ships: the `files` list in `package.json` is off limits. When the tool needs a new data file, stop and report. +- A new `DocsProvider` method needs a new endpoint in the .NET backend. Confirm that scope with the user before starting. +- `docs/*-api/` is tracked build output. When API tools load 0 entries, stop and ask before running `npm run build:docs:*`. +- `CHANGELOG.md` is written at release time: propose a line in your report. + +--- + +## Tool Anatomy + +| Part | Where | +|---|---| +| Registration | `src/index.ts`: `registerApiTools` (backed by `ApiDocLoader`) or `registerDocTools` (backed by `DocsProvider`) | +| Input schema | `src/tools/schemas.ts`: an exported `z.object` and its `z.infer` type | +| Handler | `src/tools/handlers.ts`: `createHandler(deps)` returning `(params) => Promise` | +| Description | `TOOL_DESCRIPTIONS.` in `src/tools/constants.ts` | +| Usage guide | `USAGE_GUIDE` in `src/tools/constants.ts`, served as the `igniteui-usage-guide` prompt | +| Server instructions | `instructions` in `src/index.ts`: rules that span tools only (framework detection, library boundary, `resolve_import` before imports) | +| Deprecated components | `src/config/deprecations.ts` | + +The doc tools (`list_components`, `get_doc`, `search_docs`, `get_project_setup_guide`) are registered inline in `src/index.ts` with helpers in `src/tools/doc-tools.ts`; change them in place. A new tool takes the schemas + handlers shape. `resolve_import` is the model: `git show --stat d765e6dbb` lists every file its addition touched. Its `package.json` script and new `data/` files are the parts this workflow leaves to a human (see "Limits"). + +A new tool's name is snake_case, verb first, in its family's vocabulary: `get_doc`, `search_docs`, `get_api_reference`, `search_api`, `resolve_import`. + +### Inputs + +- The framework parameter is `framework` on doc tools and `platform` on API tools; a new tool uses its family's name. Values come from `PLATFORMS` in `src/config/platforms.ts`. +- Every string parameter you add gets `.trim()`, `.min(1, '')`, `.max(, '')` and a `.describe()` with two or three concrete examples. The calling agent reads `.describe()` to fill the field and the validation message when it gets it wrong. Existing parameters keep their current validation unless the change is about them. +- Agents send near-miss input: an array serialized as a JSON string, `Component#member`, a pasted import statement, `"*"` meaning "everything". When the intent is recoverable, repair it, in a `z.preprocess` (see `extractSymbolNames`) or a hardening block at the top of the handler whose comment names the observed input, and test each shape. + +### Responses + +- Return `{ content: [{ type: "text", text }] }` with markdown text. +- Something requested does not exist: `isError: true`, and the text names the input and the tool to call next (`… not found in Angular. Use search_api to find available components.`). A search with no hits is an answer: plain text, no `isError`. +- Keep responses small. Offer narrowing parameters (`section`, `member`, `group`) and say in the description which one is cheapest. +- A tool whose input or output can name a component goes through deprecations. Answer a request for a deprecated component with `findDeprecation` + `formatDeprecationNotice`; append notices with `withDeprecationNotices(platform, text, scanned)`. `scanned` is the text whose mention of a deprecated component should trigger a notice: the caller's input by default. Whether to scan the output too is a per-tool choice (`get_doc` and `search_docs` do, `search_api` does not, because its excerpts mention deprecated types incidentally); the comment at each existing call site states the reason. Choose for your tool and leave that comment. +- A read-only tool registers `annotations: { readOnlyHint: true, openWorldHint: false }`. + +### Descriptions + +A `TOOL_DESCRIPTIONS` entry is how a calling agent picks the tool. Match the existing entries, one paragraph each: + +1. What it returns, in one sentence. +2. When to use it, and which sibling tool to use instead for the neighbouring cases. +3. What the response contains and how to narrow it. +4. Input limits and when it returns `isError`. + +### Doc Tools: Two Implementations + +`list_components`, `get_doc` and `search_docs` answer through `DocsProvider`: `LocalDocsProvider` (sql.js over the bundled database) or `RemoteDocsProvider` (HTTP to the .NET backend, `../docs-backend/docs-backend/Controllers/DocsController.cs`). `list_components` text is rendered by `src/tools/render-components.ts` and by its C# port `../docs-backend/docs-backend/ComponentRenderer.cs`, and `../shared-fixtures/list-components/*/expected.txt` pins both byte for byte. A change to what these tools return is made in both languages. + +--- + +## Steps + +### 1. Read the Neighbours + +Read the closest existing tool end to end: registration, schema, handler, description, tests. Find every tool whose description or `USAGE_GUIDE` entry routes to or away from this one; these change with it in Step 4. + +### 2. Schema and Handler + +Write the schema and handler to "Inputs" and "Responses". For a new tool, export both from `schemas.ts` / `handlers.ts` and register it in the matching `register*Tools` function with its `TOOL_DESCRIPTIONS` entry. + +For a doc tool whose output changes, mirror the change in the C# backend, then regenerate the fixtures and review the diff of every `expected.txt`: + +```bash +UPDATE_LIST_FIXTURES=1 npx vitest run src/__tests__/shared +``` + +### 3. Tests + +Vitest specs under `src/__tests__/`, with the loader or resolver faked by `vi.fn()` (`makeLoader` in `tools/handlers.test.ts`): + +- `tools/schemas.test.ts`: valid input, each rejection (empty, too long, unknown enum value), each repaired near-miss shape. +- `tools/handlers.test.ts`: success, not found (`isError` and the next-tool hint), empty result, deprecated component where it applies. +- An inline doc tool is tested through the registered handler with `McpServer` mocked, as in `index.get-doc.test.ts`. +- `spec/unit/mcp-runtime-spec.ts` (root Jasmine) imports the built `dist/`: update it when the behavior or an export it asserts changes. + +Done when every branch of the handler that returns differently has a test. + +### 4. Description, Usage Guide, Instructions + +Write or update the `TOOL_DESCRIPTIONS` entry (see "Descriptions"). Update the sibling descriptions and `USAGE_GUIDE` sections found in Step 1 so they route to the tool. Touch the server `instructions` only for a rule that spans tools. + +### 5. Validate + +PR CI (`nodejs.yml`) runs only root lint, root build and root Jasmine. The vitest suite and `dotnet test` run nowhere else, so this step is their only gate. + +From `packages/igniteui-mcp/igniteui-doc-mcp`: + +```bash +npm test # vitest +``` + +From the repository root: + +```bash +npm run lint +npm run build # includes the MCP server: tsc, then copies the database and setup_*.md into dist/ +npx jasmine spec/unit/mcp-runtime-spec.js # under a second; fails when run from another folder +``` + +For a doc tool: `dotnet test` in `../docs-backend/tests-docs-backend`. When `dotnet` is unavailable, the C# change is unverified: report it as such. + +Then call the tool on the built server the way a client does, from the repository root: + +```bash +node .agents/skills/igniteui-cli-mcp-tool/scripts/call-tool.mjs --list +node .agents/skills/igniteui-cli-mcp-tool/scripts/call-tool.mjs '' +``` + +The script starts the server in local mode, so a doc tool answers through `LocalDocsProvider` only; the remote path is verified by the shared fixtures and `dotnet test`. It prints the response text as the client receives it, after an `[isError]` line when set, and exits 1 on `isError`. Call a success case, a not-found case, an input the schema rejects and each repaired near-miss shape. Read each response as the calling agent would: a not-found response names the tool to call next, and a schema rejection names the parameter and what it needs (the SDK prefixes it with `Input validation error`; your `.min` / `.max` message is the rest). + +### 6. Documentation + +Add or update the tool in every listing: + +- `README.md`: Tools table +- `DEVELOPMENT.md`: Tools table, plus the tool's own section if it has one +- `CLAUDE.md`: registered tools list under Architecture, and its count +- `../Specification.md`: Tool Summary and Tool Definitions. Its `Version` / `Date` header stays as it is; mention in the report that it may need a bump. +- root `README.md`: MCP Server tools table + +These listings already disagree with each other. Change your tool's entry only, and list any other drift you notice in the report. + +Done when each of the five files has an entry for the tool that states its current parameters and behavior. + +### 7. Follow-up Corrections + +When the user rewords a description or response text, change the text and the tests that assert it, then rerun Step 5 for that tool. When doc-tool output changes again, regenerate the fixtures and update the C# port again. + +--- + +## Final Report + +- What the tool does now, with the `call-tool.mjs` output of one success and one failure. +- `git status --short` and `git diff --stat`. +- A proposed changelog line in the package's style: `* **feat(mcp):** .` +- `dotnet test` result, or "C# change unverified" with the reason (doc tools only). +- Doc drift noticed in Step 6, and whether `Specification.md` may need a version bump. + +## Final Self-Validation + +- [ ] Git was used only as the `AGENTS.md` git rules allow for this session. +- [ ] `package.json` and `server.json` are unchanged. +- [ ] Package `npm run build` and `npm test` pass; root `npm run lint`, `npm run build` and the MCP runtime spec pass. +- [ ] `call-tool.mjs --list` shows the tool with the intended input schema. +- [ ] Every not-found response from the handler names the tool to call next. +- [ ] Every listing in Step 6 states the tool's current parameters and behavior. +- [ ] For a doc tool: the C# backend and `shared-fixtures` match the TypeScript output, and `dotnet test` passed or the report says the C# change is unverified. diff --git a/.agents/skills/igniteui-cli-mcp-tool/scripts/call-tool.mjs b/.agents/skills/igniteui-cli-mcp-tool/scripts/call-tool.mjs new file mode 100644 index 000000000..f5056bef9 --- /dev/null +++ b/.agents/skills/igniteui-cli-mcp-tool/scripts/call-tool.mjs @@ -0,0 +1,103 @@ +#!/usr/bin/env node +// Calls one tool on the built MCP server over stdio, as an MCP client would. +// node call-tool.mjs --list → every tool's name, description and input schema (JSON) +// node call-tool.mjs '' → the tool's text content as the client receives it, after an "[isError]" line when set +// Exits 0 on success, 1 on isError or a server failure, 2 on bad usage or arguments. +// Runs from any folder; build the server first. The server starts in local mode (bundled database). +import { spawn } from "child_process"; +import { dirname, resolve } from "path"; +import { fileURLToPath } from "url"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../packages/igniteui-mcp/igniteui-doc-mcp"); + +const [tool, rawArgs = "{}"] = process.argv.slice(2); +if (!tool) { + console.error("Usage: call-tool.mjs --list | ''"); + process.exit(2); +} + +let toolArgs; +if (tool !== "--list") { + try { + toolArgs = JSON.parse(rawArgs); + } catch (err) { + console.error(`Arguments are not valid JSON: ${err.message}`); + process.exit(2); + } +} + +// cwd is the package, so the server reads that package's .env as it does under `npm start`. +const server = spawn(process.execPath, ["dist/index.js"], { cwd: packageDir, stdio: ["pipe", "pipe", "inherit"] }); +const send = (msg) => server.stdin.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n"); + +// Exit through exitCode, not process.exit(): on POSIX, stdout to a pipe is async and a large result would be cut off. +let finished = false; +const stop = (code) => { + finished = true; + clearTimeout(timer); + server.kill(); + process.exitCode = code; +}; + +// A server that dies on startup (missing database, bad import) never answers; its stderr above says why. +server.on("exit", (code, signal) => { + if (finished) return; + console.error(`Server exited before answering (code ${code}${signal ? `, signal ${signal}` : ""}).`); + stop(1); +}); + +let buffer = ""; +const pending = new Map(); +server.stdout.on("data", (chunk) => { + buffer += chunk; + let nl; + while ((nl = buffer.indexOf("\n")) >= 0) { + const line = buffer.slice(0, nl).trim(); + buffer = buffer.slice(nl + 1); + if (!line) continue; + const msg = JSON.parse(line); + pending.get(msg.id)?.(msg); + } +}); + +let nextId = 1; +const request = (method, params) => + new Promise((done) => { + const id = nextId++; + pending.set(id, done); + send({ id, method, params }); + }); + +const timer = setTimeout(() => { + console.error("Timed out after 60s waiting for the server."); + stop(1); +}, 60_000); + +const init = await request("initialize", { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { name: "call-tool", version: "1.0.0" }, +}); +if (init.error) { + console.error(`initialize failed: ${JSON.stringify(init.error)}`); + stop(1); +} else { + send({ method: "notifications/initialized" }); + + const response = tool === "--list" + ? await request("tools/list", {}) + : await request("tools/call", { name: tool, arguments: toolArgs }); + + if (response.error) { + console.error(JSON.stringify(response.error, null, 2)); + stop(1); + } else if (tool === "--list") { + console.log(JSON.stringify(response.result, null, 2)); + stop(0); + } else { + const { content = [], isError } = response.result; + if (isError) console.log("[isError]"); + for (const item of content) console.log(item.type === "text" ? item.text : JSON.stringify(item, null, 2)); + stop(isError ? 1 : 0); + } +} diff --git a/.agents/skills/igniteui-cli-smoke-test/SKILL.md b/.agents/skills/igniteui-cli-smoke-test/SKILL.md new file mode 100644 index 000000000..fa9005388 --- /dev/null +++ b/.agents/skills/igniteui-cli-smoke-test/SKILL.md @@ -0,0 +1,158 @@ +--- +name: igniteui-cli-smoke-test +description: "Validating template changes: build, the Jasmine template specs (running one spec file), and the smoke test (scripts/smoke-test.sh: scaffold with the local CLI, add templates, install, build); judging results, checking kept projects, triaging failures. WHEN TO USE: verifying template or version changes, running template specs, reading or triaging output/smoke results, editing scripts/smoke-test.sh. WHEN NOT TO USE: looking up template layout or where versions are pinned (igniteui-cli-template-anatomy)." +license: MIT +--- + +# Ignite UI CLI: Build, Template Specs, Smoke Test + +`scripts/smoke-test.sh` is the **only** check that a generated project actually installs and builds. Jasmine stubs `PackageManager.installPackages`, so a green Jasmine run says nothing about real npm resolution or TypeScript compilation of generated code. Run the build and the template specs first: they are fast and catch id and pin mistakes before a slow smoke run. + +## Build and Template Specs + +- Dependencies are installed with **yarn**, scripts run with **npm**. Run `yarn install` or `yarn add` only when asked: either rewrites `yarn.lock`, which agents leave untouched. A build that fails on dependencies is reported, not "fixed" by reinstalling. +- `npm run build` after any `.ts` change. Jasmine and the smoke test run the **compiled** `.js`, so without a build they test stale code. +- `npm run lint` skips `packages/cli/templates/**` and every `**/files/**` (ignored in `eslint.config.mjs`). React, Web Components and jQuery template code is therefore only linted inside a generated project (see "Checking a Kept Project"). +- `npm run jasmine` ignores arguments and always runs every spec. To run one file, pass the compiled spec to the Jasmine CLI: `npx jasmine spec/templates/react-spec.js`. Leave `spec/jasmine-runner.ts` as it is. +- A run that reports **INCOMPLETE** only because some specs are pending (`xit`/`pending()`) still exits 0 and counts as a pass. Any failed spec is a failure. + +### Specs That Guard Templates + +| Spec | Guards | +|---|---| +| `spec/templates/angular-spec.ts` | Template ids exist; no file collisions between templates; every `igniteui-angular` pin in templates and project `package.json` equals `IGNITEUI_ANGULAR_PACKAGE` | +| `spec/templates/react-spec.ts` | Same, for all four `IGNITEUI_REACT_*` constants | +| `spec/templates/webcomponents-spec.ts` | Ids and collisions only. It does **not** check version pins against the constants | +| `spec/templates/template-contracts-spec.ts` | Every jQuery, React, Web Components and Blazor project and component template: matches its framework and project type, its template paths exist, it generates a config | +| `spec/unit/TemplateManager-spec.ts`, `spec/unit/list-spec.ts` | Template discovery and `ig list` output | +| `spec/acceptance/*` | `ig new`, `ig add`, `ig generate` end to end, with package install stubbed | + +The template specs for a framework run in about 15 seconds, after `npm run build`: + +```bash +npx jasmine spec/templates/angular-spec.js +npx jasmine spec/templates/react-spec.js spec/templates/template-contracts-spec.js +npx jasmine spec/templates/webcomponents-spec.js spec/templates/template-contracts-spec.js +``` + +A failing "no internal collisions" spec means two templates would write the same file path: give the new template a distinct `__path__`/file prefix layout and keep the spec as it is. + +## Running the Smoke Test + +Preconditions: `npm run build` has run (the script refuses otherwise); `node` and `npm` on PATH; network access to npm. + +| Goal | Command | Typical time | +|---|---|---| +| A few templates, one framework | `scripts/smoke-test.sh -f react --templates rating,grid` | ~1 min | +| All component templates, one framework | `scripts/smoke-test.sh -f webcomponents` | a few min | +| Every project template, one framework | `scripts/smoke-test.sh -f angular --all-projects` | ~2 min for Web Components; Angular is slowest | +| Bisect which template breaks a build | `scripts/smoke-test.sh -f angular --isolate --templates a,b,c` | slow | +| Keep generated projects for inspection | add `--keep` | | + +Defaults: angular, react, webcomponents; default project template; all component templates. jQuery and Blazor are opt-in (`-f jquery`, `-f blazor`): jQuery needs ProGet credentials for 13 templates and has no build; Blazor needs the .NET SDK and has no component templates. + +`--all-projects` takes its list from `ig list`, so **hidden project templates** (`isHidden = true` in the project's `index.ts`, e.g. `base`, `side-nav-auth`, `ai-config`) are not scaffolded. Most reuse the `_base` / `_base_with_home` files; a hidden project with its own `files/package.json` (e.g. Angular `side-nav-auth`) is covered only by `scripts/smoke-test.sh -f -p `. Hidden projects can also reuse another project's `files/` through `templatePaths` (Angular `side-nav-mini-auth` includes `side-nav-auth/files`, manifest included), so a change there needs a `-p` run for each project that pulls it in. + +Runs longer than a few minutes: use a long command timeout, or run it in the background with output redirected to a file and wait for the process to exit. The `Results:` line is not the end: when nothing failed and `--keep` is not set, the script deletes the generated projects after printing it. The script **wipes its `--out` directory at start**, so give each run its own directory (e.g. `--out output/smoke-react`, `--out output/smoke-react-projects`) when you need more than one run's results, and never run two at once into the same directory. + +In a cloud agent session (time-limited, see `AGENTS.md`), the build is already done by the setup workflow unless you changed `.ts` sources. Scope each run with `-f ` and `--templates` / `-p`; a full run of all three frameworks can take most of the session. + +## Results + +- `output/smoke/results.tsv`: one row per step, columns `framework step template status seconds`. Steps: `new`, `add`, `install`, `build`. Status: `PASS`, `FAIL`, `SKIP`. +- `output/smoke/logs/-[-].log`, where `` is `-` (or `-` with `--isolate`) and `` is `ng`, `rc`, `wc`, `jq`, `bz`. Examples: `rc-default-add-rating.log`, `wc-default-install.log`, `ng-default-build.log`. +- Generated projects are deleted on full success unless `--keep`; on any failure they are kept under `output/smoke//`. + +## What a PASS Proves, Per Framework + +| Framework | Generated `build` script | Type-checks template code? | +|---|---|---| +| Angular | `ng build` | Yes, for code reachable from the app | +| React | `tsc -b && vite build` | Yes | +| Web Components | `vite build` | **No.** Vite strips types without checking, and an added view that no route imports is not even bundled | + +So for **Web Components**, a smoke PASS proves the template is added and its packages install, not that its code compiles. For **Angular**, `ng build` compiles from `tsconfig.app.json`, which excludes `*.spec.ts`, so the template's spec is neither type-checked nor run. No build runs the template's test file. A **kept project** closes these gaps. + +`ig add` from the command line does not register a route for Web Components templates, so a missing entry in the generated `app-routing.ts` is expected, not a template defect. + +## Checking a Kept Project + +Run the smoke test with `--keep`, then in the kept project (`//`) check the template ``: + +| Framework | Type-check | Lint | Test file | +|---|---|---|---| +| Angular | covered by `ng build` | `npx eslint src/app/` | `npx ng test --include src/app/ --browsers=chromium --watch=false` (the `@angular/build:unit-test` builder, which uses Vitest) | +| React | covered by `tsc -b` | `npx eslint src/app/` | `npx vitest run src/app/` | +| Web Components | `npx tsc --noEmit` | `npx eslint src/app/` | `npx vitest run src/app/` | + +The test runners use browser mode with Playwright Chromium. If one fails because the browser is missing, run `npx playwright install chromium` once and retry; that is environment setup, not a template defect. + +## Judging a Step + +**Exit codes alone are not trustworthy**: `Util.error` logs and returns without setting a code. The script already judges each step on exit code **plus** a log scan (`doesn't exist|is not valid|not supported|not found|^Error: |Error installing|npm error`) **plus** an artifact check (`ignite-ui-cli.json` after `new`; more files under `src/` after `add`). Trust `results.tsv`, then read the log for the reason. + +## Classifying Failures + +| Symptom in log | Class | Usually caused by | +|---|---|---| +| `add` FAIL, "Template doesn't exist in the current library", and no folder with that id exists | Invalid input | The id passed to `--templates` is wrong; nothing to fix in code | +| `add` FAIL, "Template doesn't exist", but the folder exists | Template defect | `id` field differs from what was passed, template not discovered, missing `index.ts`, or stale build | +| `install` FAIL with `ETARGET` / `No matching version` / `404` | Version pin | A pin names a version that is not published | +| `install` FAIL with `ERESOLVE` / peer dependency conflict | Version pin | A pin is outside a peer range of another installed package. `peerOptional` conflicts **also** fail install once both packages are present | +| `build` FAIL with `TS2305` / `TS2724` "has no exported member" | Template defect or version pin | Template imports a symbol that does not exist in the pinned version | +| `build` FAIL with `TS2322` / `TS2339` on a component prop | Template defect or version pin | Prop renamed/removed, or invented | +| `build` FAIL pointing into a project file (`main.ts`, `app.config.ts`, `App.tsx`) | Project-template defect | Not caused by a component template | +| `ETIMEDOUT`, `ECONNRESET`, `EAI_AGAIN`, registry 5xx | Environment | Network; re-run before blaming code | +| jQuery `401`/`403` from ProGet | Environment | Missing credentials, expected without them | +| `dotnet: command not found` | Environment | .NET SDK missing | + +### Notes on Reading Results + +- `install` and `build` rows are recorded against the **project directory** (e.g. `ng-default`), not a template. To find the template behind an `ERESOLVE`/`ETARGET`, match the package named in the error against the `packages` of the templates added in that run. +- A failed `install` writes **no `build` row** at all (not `SKIP`). Report the build as not run because of the install failure. +- npm reports **only the first** `ERESOLVE`. After finding one conflict, check the other pins too: `npm view @ peerDependencies` against each added template's `packages`. +- A smoke run tests the **compiled** templates (`index.js`). If any template `.ts` changed since the last `npm run build`, the run tests stale pins and ids. When unsure, rebuild first. + +## Mapping an Install Error Back to Source + +A pin in an install error comes from one of: the template's variant `index.ts` (`this.packages`), the framework's `constants.ts`, or a project template's `files/package.json`. See the template-anatomy skill. + +## Mapping a Build Error Back to Template Source + +A generated file `output/smoke//src/app//.` comes from the template's `files/src/app/__path__/__filePrefix__.`: + +| Framework | Template root | +|---|---| +| angular (`ng`) | `packages/igx-templates/igx-ts///files/` | +| react (`rc`) | `packages/cli/templates/react/igr-ts///files/` | +| webcomponents (`wc`) | `packages/cli/templates/webcomponents/igc-ts///files/` | + +When one combined project fails to build and the error does not name the file clearly, re-run with `--isolate --templates `. + +## Triaging a Run + +Use this when a run failed, or when asked to make sense of an existing `output/smoke/` (or another `--out` directory). Triage changes no files. If there is no `results.tsv`, say so and stop. + +1. Read `results.tsv` and list every `FAIL` row. +2. For each, open the matching log and find the **first** meaningful error line. Quote it; do not guess a cause the log does not support. +3. Classify it with the table above. +4. Map it to source: build errors through the generated path to the template's `files/`; install errors through the package named in the error to its pin, naming the template that added it. +5. For every `ERESOLVE`, check the remaining pins against the main package's peers (npm reports only the first conflict) and report latent conflicts as advisories. +6. Report the root of a cascade (failed `install`, no `build` row), not the cascade. +7. Re-run only for an environment failure, once, and say that you did. + +Report one row per failure: + +| Framework | Step | Project / culprit template | Class | Evidence (log:line, error) | Source at fault | +|---|---|---|---|---|---| + +End with the PASS/FAIL/SKIP totals, whether the run as a whole is trustworthy (not when most failures are network), and the exact re-run command if one is warranted (e.g. `--isolate --templates `). + +## Constraints When Editing the Script + +- Every `ig` call must happen **before** `npm install` in a generated project. After install, `bin/execute.js` delegates to the project-local *published* CLI, so later calls would test the published CLI, not your local build. +- Keep the three-way step judgement (exit code, log scan, artifact) intact. + +## Related Skills + +- [`igniteui-cli-template-anatomy`](../igniteui-cli-template-anatomy/SKILL.md) diff --git a/.agents/skills/igniteui-cli-template-anatomy/SKILL.md b/.agents/skills/igniteui-cli-template-anatomy/SKILL.md new file mode 100644 index 000000000..9d420e103 --- /dev/null +++ b/.agents/skills/igniteui-cli-template-anatomy/SKILL.md @@ -0,0 +1,154 @@ +--- +name: igniteui-cli-template-anatomy +description: "Template anatomy for Angular (igx-ts), React (igr-ts) and Web Components (igc-ts): folder layout, index.ts fields, placeholders, test files, groups, and every place an Ignite UI version is pinned. WHEN TO USE: adding or changing a component template, locating version pins, auditing template parity across frameworks. WHEN NOT TO USE: running builds, template specs or the smoke test (igniteui-cli-smoke-test)." +license: MIT +--- + +# Ignite UI CLI: Template Anatomy + +## Discovery + +Templates are discovered by **scanning folders**. There is no registry file. A component appears in `ig add` / `ig list` when its folder contains a component `index.ts` and at least one variant folder with its own `index.ts`. `groups.json` only holds group descriptions; it changes only when a new group is introduced. + +## Framework Roots + +| Framework | Project type | Root | Template base class | Constants | +|---|---|---|---|---| +| Angular | `igx-ts` | `packages/igx-templates/igx-ts/` | `packages/igx-templates/IgniteUIForAngularTemplate.ts` | `packages/igx-templates/constants.ts` | +| React | `igr-ts` | `packages/cli/templates/react/igr-ts/` | `packages/cli/lib/templates/IgniteUIForReactTemplate.ts` | `packages/cli/templates/react/igr-ts/constants.ts` | +| Web Components | `igc-ts` | `packages/cli/templates/webcomponents/igc-ts/` | `packages/cli/lib/templates/IgniteUIForWebComponentsTemplate.ts` | `packages/cli/templates/webcomponents/igc-ts/constants.ts` | + +jQuery (`packages/cli/templates/jquery/js/`) is legacy and Blazor (`packages/cli/templates/blazor/igb/`) has no component templates. Both are out of scope for the template workflows unless the user asks. + +`@igniteui/angular-templates` is a separate npm package: Angular templates import only from `packages/igx-templates/` and `@igniteui/cli-core`, never from `packages/cli/templates/`. A version Angular shares with Web Components (e.g. `igniteui-webcomponents` for wrapped components) is kept on the Angular side. + +## Component Template Layout + +``` +// + index.ts ← component: BaseComponent from @igniteui/cli-core + / ← usually "default"; grids use e.g. "basic", "grid-editing" + index.ts ← template: extends the framework's template base class + files/src/app/__path__/ ← files copied into the generated project + __filePrefix__. +``` + +Component `index.ts` (same shape in all three frameworks): + +```ts +import { BaseComponent } from "@igniteui/cli-core"; + +class IgrRatingComponent extends BaseComponent { + constructor() { + super(__dirname); + this.name = "Rating"; + this.group = "Data Entry & Display"; + this.description = `represents a rating component`; + } +} +module.exports = new IgrRatingComponent(); +``` + +Variant `index.ts` fields: + +| Field | Meaning | +|---|---| +| `components` | Display names of the components used, e.g. `["Rating"]` | +| `controlGroup` | Must equal a key in the framework's `groups.json` and the component's `group` | +| `listInComponentTemplates` | `true` for component templates | +| `id` | The id used by `ig add `. Keep the **same id across frameworks** for the same component | +| `projectType` | `igx-ts`, `igr-ts` or `igc-ts` | +| `name`, `description` | Shown in `ig list` and prompts | +| `packages` | npm packages added to the project on `ig add`, as `name@range`. Use constants from `constants.ts` where one exists | +| `dependencies` | Optional extra registrations (rare) | + +Copy the closest existing sibling instead of writing from scratch. These **model templates** exist in all three frameworks: + +- simple input/display: `rating`, `slider`, `switch` +- data-bound: `combo`, `list`, `tree` +- charts and gauges: `category-chart`, `financial-chart`, `radial-gauge` + +## Generated Files Per Framework + +| Framework | Placeholders | Files under `files/src/app/__path__/` | Test runner in generated project | +|---|---|---|---| +| Angular | `<%=ClassName%>`, `<%=filePrefix%>`, `<%=igxPackage%>` | `__filePrefix__.ts`, `.html`, `.scss`, `.spec.ts` | Vitest (via `@angular/build:unit-test`) with Angular `TestBed` | +| React | `$(ClassName)`, `$(path)` | `__filePrefix__.tsx`, `__filePrefix__.test.tsx`, `style.module.css` | Vitest + Testing Library | +| Web Components | `$(ClassName)`, `$(path)` | `__filePrefix__.ts`, `__filePrefix__.test.ts` | Vitest | + +Angular has two styles. Pick the one the component library dictates: + +- **Native Ignite UI for Angular**: `import { IgxSliderComponent } from '<%=igxPackage%>';` and `imports: [...]` in a standalone component; `packages = [IGNITEUI_ANGULAR_PACKAGE]`. +- **Wrapped Web Component** (component not in `igniteui-angular`, e.g. `rating`, `chat`): `defineComponents(IgcXComponent)` from `igniteui-webcomponents`, `schemas: [CUSTOM_ELEMENTS_SCHEMA]`, and `packages` includes `igniteui-webcomponents@`. + +React imports components from `igniteui-react` (or `-grids`, `-charts`, `-gauges`) as `Igr*`. Web Components imports `Igc*Component` and calls `defineComponents(...)`. + +## Where Ignite UI Versions Are Pinned + +Pins live in three kinds of places. **Always regenerate the list with a search; never trust a remembered list.** + +1. `constants.ts` per framework (exported `name@range` strings). +2. Project-template manifests: `/projects/*/files/package.json`. Several projects carry their own copy (e.g. Angular `_base` and `side-nav-auth`; Web Components `_base` and `_base_with_home`). +3. Hard-coded `this.packages = ["name@range", ...]` in variant `index.ts` files, used where no constant exists (Angular charts/gauges/maps family, `igniteui-grid-lite`, wrapped `igniteui-webcomponents`; Web Components `igniteui-dockmanager`). Some build the name at runtime, e.g. Angular dock-manager: `` `${resolvePackage(NPM_DOCK_MANAGER)}@~2.1.1` ``. + +Run **both** search patterns (ripgrep) over `packages/cli/templates` and `packages/igx-templates`, `*.ts` and `*.json`: + +``` +(igniteui-[a-z-]+|@infragistics/[a-z-]+|@igniteui/[a-z-]+)["@]\s*:?\s*"?[~^]?[0-9]+\.[0-9]+ +``` + +``` +this\.packages.*@[~^]?[0-9]+\.[0-9]+ +``` + +The second catches pins whose package name is computed (template literals). Union the results. `igniteui-cli-template-version-update/scripts/template-pins.mjs` implements both searches; when a pin location changes here, update that script too. + +Exclude: compiled `*.js`/`*.js.map`, `node_modules`, `docs_baseline`, `ai-config/files/skills/**` (synced docs), and spec mocks such as `packages/ng-schematics/**/*_spec.ts`. + +## Component → Package Map + +Where a component's code lives is not always obvious. Verify against the pinned version, but start here: + +| Component family | Angular | React | Web Components | +|---|---|---|---| +| Most UI components | `igniteui-angular` | `igniteui-react` | `igniteui-webcomponents` | +| Grids, Pivot, Tree/Hierarchical Grid, Paginator, Action Strip, Query Builder | `igniteui-angular` | `igniteui-react-grids` | `igniteui-webcomponents-grids` | +| Grid Lite | `igniteui-angular` (`/grid-lite` entry) + peer `igniteui-grid-lite` | `igniteui-react` (`grid-lite` subpath) + peer `igniteui-grid-lite` | `igniteui-grid-lite` | +| Charts (category, financial, pie, doughnut) | `igniteui-angular-charts` + `-core` | `igniteui-react-charts` | `igniteui-webcomponents-charts` + `-core` | +| Gauges (linear, radial, bullet graph) | `igniteui-angular-gauges` + `-core` | `igniteui-react-gauges` | `igniteui-webcomponents-gauges` + `-core` | +| Geographic Map | `igniteui-angular-maps` (+ charts, core) | `igniteui-react-maps` | `igniteui-webcomponents-maps` | +| Dock Manager | `igniteui-dockmanager` | `igniteui-react-dockmanager` | `igniteui-dockmanager` | + +Whether a package has a constant is in the framework's `constants.ts`; check there before adding a pin. + +## Peer Dependencies + +A framework's main package declares peers that other template pins must satisfy. For example `igniteui-angular` 22.2.x requires `igniteui-grid-lite ~0.11.0` and `igniteui-webcomponents ~7.4.0`. Check with `npm view @ peerDependencies` and compare against every other pin in that framework. + +Packages version independently. Known release trains that do **not** share versions: + +- `igniteui-angular` vs `igniteui-angular-core` / `-charts` / `-gauges` / `-maps` +- `igniteui-webcomponents` vs `igniteui-webcomponents-core` / `-charts` / `-gauges` / `-grids` / `-inputs` / `-layouts` +- `igniteui-react` / `-grids` vs `igniteui-react-charts` / `-gauges` +- `igniteui-dockmanager`, `igniteui-grid-lite`: their own versions + +Check each package on npm (`npm view dist-tags.latest`) instead of inferring one from another. + +## Auditing Parity Across Frameworks + +To answer "which templates are missing or inconsistent?", list component folders per root (excluding `projects`, `custom-templates`, `generate`), read each variant's `id`, `controlGroup` and `listInComponentTemplates`, and check for the framework's test file in `files/`. Build a component-by-framework matrix, then classify each difference: + +- **Gap, actionable**: the framework's library ships the component (verify with the MCP `list_components` tool or the package's `.d.ts`; see the map above). +- **Gap, wrappable (Angular only)**: not in `igniteui-angular` but in `igniteui-webcomponents`; could use the wrapped pattern. Human decision. +- **Gap, not actionable**: no library for that framework ships it. +- **Id mismatch**: same component, different ids (`breadcrumb` vs `breadcrumbs`). Recommend adding the canonical id as a new variant, **not renaming**: renaming breaks `ig add ` for existing users. +- **Library naming difference**: ids differ because the libraries name the component differently (`navigation-drawer` vs `nav-drawer`). Not an error. +- **Group mismatch**, **missing test**, **composite** (e.g. `form`, not one library component; informational). + +Hand actionable gaps to the [`igniteui-cli-component-template`](../igniteui-cli-component-template/SKILL.md) workflow; for version drift, run [`igniteui-cli-template-version-update`](../igniteui-cli-template-version-update/SKILL.md) in `dry-run` mode. + +## Related Skills + +- [`igniteui-cli-component-template`](../igniteui-cli-component-template/SKILL.md): workflow that creates a template using this layout +- [`igniteui-cli-template-version-update`](../igniteui-cli-template-version-update/SKILL.md): workflow that updates the pins listed here +- [`igniteui-cli-smoke-test`](../igniteui-cli-smoke-test/SKILL.md) diff --git a/.agents/skills/igniteui-cli-template-version-update/SKILL.md b/.agents/skills/igniteui-cli-template-version-update/SKILL.md new file mode 100644 index 000000000..de38bd56f --- /dev/null +++ b/.agents/skills/igniteui-cli-template-version-update/SKILL.md @@ -0,0 +1,171 @@ +--- +name: igniteui-cli-template-version-update +description: "Update or audit the Ignite UI versions that Angular, React and Web Components templates scaffold (ig new / ig add). WHEN TO USE: updating template version pins; asking which pins are stale or conflicting (dry-run). WHEN NOT TO USE: the CLI's own dependencies or version; adding a template (igniteui-cli-component-template)." +license: MIT +--- + +# Template Version Update + +Updates the **Ignite UI package versions pinned in project and component templates**: what `ig new` and `ig add` write into a user's `package.json`. The CLI's own dependencies and version are out of scope. + +--- + +## Input + +- **Scope**: one or more of `angular`, `react`, `webcomponents`, or specific package names. +- **Targets** (optional): explicit versions, e.g. `igniteui-webcomponents 7.5.0`. If none are given, the target for each package is its npm `latest` dist-tag. +- **Mode**: + - `update` (default): Steps 1-7. + - `dry-run`: a read-only drift report. Run Steps 1-3, then stop and give the dry-run report (see Final Report). Edit no files and run no builds or smoke tests. Use it for "what is out of date?" questions; `all` is a valid scope here. + +If the scope is missing, or a scope or target is ambiguous, stop and ask. In `update` mode the scope is always named explicitly; "everything" is never assumed. + +Scope boundaries: +- A framework scope covers only that framework's root. With scope `webcomponents`, the `igniteui-webcomponents` pins in Angular's wrapped-component templates (`packages/igx-templates/.../chat`, `rating`) are **out of scope**; report them as observations. +- Packages shared by all frameworks' project templates (e.g. `@igniteui/material-icons-extended`) are updated only when the user names them or the scope covers all three frameworks, so the frameworks don't diverge. Otherwise list them as observations. + +--- + +## Limits + +Your changes are Ignite UI version strings, applied as patch or minor updates. Everything else is reported: + +- **Major** updates (see Step 3) are stop items, never applied. +- Each pin keeps its range prefix (`~` or `^`); report inconsistent prefixes as observations. +- Pins stay where they are: hard-coded pins stay hard-coded unless the user asked to move them into constants. +- Non-Ignite UI dependencies in project templates (Angular, React, Vite, TypeScript, etc.) stay as they are. If a new Ignite UI version requires them to change, that is a stop item (Step 3). +- jQuery and Blazor templates are out of scope unless explicitly named. +- `CHANGELOG.md` is written at release time: propose a line in your report. + +--- + +## Script + +Steps 2-5 are backed by [`scripts/template-pins.mjs`](./scripts/template-pins.mjs) (Node, uses the repo's `semver`; run `yarn install` first): + +```bash +node .agents/skills/igniteui-cli-template-version-update/scripts/template-pins.mjs \ + [--framework angular,react,webcomponents|jquery|all] [--package ,] \ + [--target @]... [--apply] [--json] [--all-deps] +``` + +- Defaults: frameworks `angular,react,webcomponents`; Ignite UI packages only; target = npm `latest`. +- Without `--apply` it is read-only. `--apply` rewrites only `patch`/`minor` rows of Ignite UI packages, keeping each pin's prefix and the file's formatting; every other status is left alone. +- `--all-deps` adds non-Ignite UI packages to the report as observations. They are never applied. +- It reads the registry from `npm config get registry`. + +| Status | Meaning (Step 3) | +|---|---| +| `current` | The range already admits the target. `current*`: a `^` minimum that could be raised (Observation) | +| `patch` / `minor` | Proceeds; `new range` shows the result | +| `major` | Major or `0.x` minor change: stop item | +| `peer-stop` | Would proceed, but the target's peer is not met by a template pin after the update: stop item, reason in `new range` | +| `package-stop` | Would proceed, but another occurrence of the same package has a stop status (inconsistent pins): held with the rest of the package, reason in `new range` | +| `shared-skip` | Would proceed, but the package is pinned in all three frameworks and the run selects fewer without naming it (`--package` or `--target`): Observation, not applied | +| `missing-target` | The target version is not published: stop item | +| `no-match` | No published version satisfies the current range: broken pin, report it first | +| `downgrade` / `error` | Target below the current minimum, or a registry lookup failed (including the target's peer lookups, so its peers are unchecked): stop item | + +The **Peer conflicts** section lists, per framework, every pin outside the peer range of an Ignite UI package's resolved `current` version or its update target (`after`). A failed peer lookup is listed there too, with the error under `resolves to`; for an `after` row the package becomes `error`. **Inconsistencies** lists different ranges, mixed prefixes and packages shared across frameworks. + +## Steps + +### 1. Record the Starting Point + +Run `git status --short` and keep the output. Your final report must separate your changes from anything that was already modified. + +### 2. Build the Inventory + +Run the script for the frameworks in scope (add `--package` / `--target` from the input). It applies the search patterns and exclusions from [`igniteui-cli-template-anatomy`](../igniteui-cli-template-anatomy/SKILL.md) ("Where Ignite UI Versions Are Pinned") over `packages/cli/templates` and `packages/igx-templates`, and resolves computed names such as `` `${resolvePackage(NPM_DOCK_MANAGER)}@~2.1.1` `` through `packages/core/update/package-resolve.ts`. If something about the output looks wrong, run the anatomy searches by hand and compare. + +Produce a table, one row per occurrence: + +| File:line | Package | Current range | +|---|---|---| + +Include all three kinds of locations: `constants.ts`, `projects/*/files/package.json`, and hard-coded `this.packages = [...]` in `index.ts`. If a pin appears in a form the pattern missed (you notice one while reading), add it and mention it in the report. If a pin sits in a place you are not sure you may edit, list it as a stop item. + +### 3. Resolve and Classify Targets + +The script's output covers items 1-4 below (status, target, peer conflicts) and the drift notes. Review it against these rules rather than repeating the lookups; run `npm view` yourself only to confirm a surprising row. + +For each distinct package: + +1. Determine the target: the user's explicit version, else `npm view dist-tags.latest`. +2. Confirm it exists: `npm view @ version`. A target that does not exist is a stop item for that package. +3. Classify current → target: + - **patch / minor** (same major; for `0.x`, same minor): proceed. + - **major** (or `0.x` minor change): **stop for that package**. Leave all of its pins unchanged. + - **already current** (the existing range already resolves to the target): no change. This includes `^` pins whose range admits a newer minor (e.g. `^2.1.1` admits 2.2.1). Keep their minimum unless the user asks; list them under Observations as "minimum could be raised to `^`". +4. For packages that proceed, check peers: `npm view @ peerDependencies`. + - If a peer is a **non-Ignite UI** package (e.g. a new `igniteui-angular` needing a newer `@angular/core`) and the project templates don't satisfy it, **stop for that package** and report the required change. + - If a peer is another **Ignite UI** package pinned in templates (e.g. `igniteui-angular` → `igniteui-grid-lite ~0.11.0`), that pin must satisfy the peer range after your update. Treat bringing it into range as part of this update, subject to the same patch/minor/major rule; if that would be a major (or `0.x` minor) change, report it as a stop item with the peer range as the reason, so a human can approve it explicitly. + + Do this check also for the framework's **main package even when it is not changing**: existing pins can already be out of range. + +Look up each package's version on npm by itself. Release trains are independent (see the anatomy skill), so one package's version says nothing about another's. + +While classifying, also note drift within the inventory: pins that differ from the framework's constant for the same package, the same package at different ranges in different files, and mixed `~`/`^` prefixes for the same package. + +In `dry-run` mode, stop here. + +### 4. Apply Edits + +Rerun the script with the same arguments plus `--apply`. It updates every `patch`/`minor` occurrence (`constants.ts`, project `package.json` copies, hard-coded and computed `index.ts` pins) and prints each line it changed; a `FAILED` line means the pin was not found as recorded, so edit it by hand. + +An Ignite UI peer brought into range as part of the update (Step 3.4) is applied the same way: include it in `--package`, or give its version with `--target`. + +When editing by hand: keep the existing range prefix and the formatting (tabs in `.ts`, two spaces in template `package.json`). + +### 5. Re-check + +Rerun the script without `--apply`: every package you changed should now be `current`, with no new peer conflicts. Then search for each **old** version string of every package you changed, over the same paths. Expect zero hits for that package. Any remaining hit is a missed pin: fix it, or report it if it is outside scope. + +### 6. Validate + +Use the [`igniteui-cli-smoke-test`](../igniteui-cli-smoke-test/SKILL.md) skill for the commands below. Lint is skipped: it does not check version strings. + +1. `npm run build`. The smoke test and the specs run the compiled templates, so a stale build tests the old pins. If it fails, diagnose it before going further. +2. The template specs for each framework in scope (smoke-test skill, "Specs That Guard Templates"). For Angular and React they check every main-package pin against its constant. +3. Smoke test, per framework in scope, each into its **own output directory** (a run wipes its `--out` at start, so a shared directory loses the earlier run's results). Scope the run to what changed: + - The framework's main package changed (`igniteui-angular`, `igniteui-react`, `igniteui-webcomponents`), which nearly every template pulls in: `scripts/smoke-test.sh -f --out output/smoke-` (default project + every component template). + - Otherwise, only the templates that pull in a changed package: search the framework's variant `index.ts` files for the package name or its constant, read each match's `this.id`, and run `scripts/smoke-test.sh -f --templates , --out output/smoke-`. For example, an `igniteui-dockmanager` update runs `--templates dock-manager`. The default project is always scaffolded, so a pin in its `package.json` is covered too. + - Additionally, when any `projects/*/files/package.json` changed: `scripts/smoke-test.sh -f --all-projects --out output/smoke--projects`. `--all-projects` skips hidden projects, so for each changed manifest under a hidden project (`isHidden = true` in its `index.ts`, e.g. Angular `side-nav-auth`) also run `scripts/smoke-test.sh -f -p --out output/smoke--`, and the same for every other hidden project whose `templatePaths` includes that `files/` folder (Angular `side-nav-mini-auth` reuses `side-nav-auth/files`). If a changed package appears only in project manifests (no template pulls it in, e.g. `@igniteui/material-icons-extended`), these runs alone cover it; skip the `--templates` run. + +A scoped run takes about a minute; a full run takes several. Use a long timeout or background execution and wait for completion. Run them one at a time. Classify any failure with the smoke-test skill. Environment failures (network, credentials) are re-run once, then reported; versions stay as they are for those. + +If a failure is caused by the new version (missing export, renamed prop, peer conflict), revert **that package's** edits, keep the others, and report it as a stop item with the evidence (log path and the error line). + +### 7. Follow-up Corrections + +When the user corrects something after the report, fix that issue and re-run Steps 5 and 6 for the affected frameworks only, rather than the whole update. + +--- + +## Final Report + +1. **Updated**: table `package | old range | new range | files changed`. +2. **Stopped**: each package not updated, with the reason (major, missing version, peer conflict, unclear pin location, failed smoke with evidence). +3. **Validation**: build result, template specs, and per framework each smoke command run (full, which `--templates`, `--all-projects`, which `-p`), the PASS/FAIL/SKIP summary and the path of its `results.tsv`. +4. **Observations** (not acted on): mixed range styles, pins that bypass constants, packages that are stale but out of scope. +5. **Proposed CHANGELOG line**, in the repository's style: + `* **chore():** Updated scaffolded projects to \`\` \`~x.y.z\`.` +6. **Diff**: `git status --short` and `git diff --stat`, distinguishing pre-existing changes from yours. + +### Dry-Run Report + +1. **Peer conflicts** first: pins outside a main package's peer range (these make `ig add` install an incompatible version). +2. **Drift** table: `package | locations | ranges | npm latest | within range? | update type (patch/minor/major)`. +3. **Inconsistencies**: pins that bypass or differ from constants, mixed range prefixes. +4. What an `update` run in each scope would change, and which packages it would stop on. +5. The date and commit (`git rev-parse --short HEAD`). + +## Final Self-Validation + +In `dry-run` mode only the first item applies, plus: `git status --short` is unchanged from Step 1. + +- [ ] Git was used only as the `AGENTS.md` git rules allow for this session. +- [ ] Every changed package has zero remaining occurrences of its old version in scope. +- [ ] No major update was applied. +- [ ] No range prefix changed. +- [ ] Build and template specs passed; smoke results reported for every framework in scope, covering every template that pulls in a changed package. diff --git a/.agents/skills/igniteui-cli-template-version-update/scripts/template-pins.mjs b/.agents/skills/igniteui-cli-template-version-update/scripts/template-pins.mjs new file mode 100644 index 000000000..52026e1f5 --- /dev/null +++ b/.agents/skills/igniteui-cli-template-version-update/scripts/template-pins.mjs @@ -0,0 +1,297 @@ +#!/usr/bin/env node +// Inventory, classify and (with --apply) update the package pins in igniteui-cli templates. +// Usage: node template-pins.mjs [--framework angular,react,webcomponents|all] [--package a,b] +// [--target pkg@version]... [--all-deps] [--json] [--apply] +import { execSync } from "node:child_process"; +import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { join, relative, sep } from "node:path"; + +const root = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim(); +let semver; +try { + semver = createRequire(join(root, "package.json"))("semver"); +} catch { + console.error("semver not found in the repository's node_modules; run `yarn install` first."); + process.exit(1); +} + +const args = process.argv.slice(2); +const values = (name) => args.flatMap((a, i) => (a === name && args[i + 1] ? args[i + 1].split(",") : [])); +const flag = (name) => args.includes(name); + +const ROOTS = { + angular: "packages/igx-templates", + react: "packages/cli/templates/react", + webcomponents: "packages/cli/templates/webcomponents", + jquery: "packages/cli/templates/jquery" +}; +const PRIMARY = ["angular", "react", "webcomponents"]; +const fwArg = values("--framework"); +const frameworks = !fwArg.length ? PRIMARY : fwArg.includes("all") ? Object.keys(ROOTS) : fwArg; +const unknownFw = frameworks.filter((f) => !ROOTS[f]); +if (unknownFw.length) { + console.error(`Unknown framework: ${unknownFw.join(", ")}. Use ${Object.keys(ROOTS).join(", ")} or all (Blazor has no npm pins).`); + process.exit(1); +} +const onlyPackages = values("--package"); +const targets = Object.fromEntries(values("--target").map((t) => { + const at = t.lastIndexOf("@"); + return [t.slice(0, at), t.slice(at + 1)]; +})); +const allDeps = flag("--all-deps"); +const asJson = flag("--json"); +const apply = flag("--apply"); + +const isIgnite = (name) => /^(ignite-ui$|igniteui-|@infragistics\/|@igniteui\/)/.test(name) && name !== "igniteui-cli"; +const EXCLUDED = /(^|\/)(node_modules|docs_baseline)\/|\/ai-config\/files\/skills\//; +const SPEC = /([_-]spec|Spec)\.ts$/; + +const resolveConsts = Object.fromEntries( + [...readFileSync(join(root, "packages/core/update/package-resolve.ts"), "utf8").matchAll(/export const (\w+) = "([^"]+)"/g)] + .map((m) => [m[1], m[2]]) +); + +function walk(dir, out = []) { + for (const name of readdirSync(dir)) { + const p = join(dir, name); + if (statSync(p).isDirectory()) { + if (name !== "node_modules") walk(p, out); + } else out.push(p); + } + return out; +} + +const LITERAL = /(["'`])((?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*)@([~^]?\d[\w.+-]*)\1/g; +const COMPUTED = /`\$\{(?:resolvePackage\()?(\w+)\)?\}@([~^]?\d[\w.+-]*)`/g; +const pins = []; + +// The primary roots are always scanned so packages shared by all of them are known, even when not selected. +for (const fw of new Set([...frameworks, ...PRIMARY])) { + for (const file of walk(join(root, ROOTS[fw]))) { + const rel = relative(root, file).split(sep).join("/"); + if (EXCLUDED.test(rel)) continue; + const inFiles = rel.includes("/files/"); + const lines = readFileSync(file, "utf8").split(/\r?\n/); + const add = (i, name, range, kind, token) => pins.push({ fw, file: rel, line: i + 1, name, range, kind, token }); + if (inFiles && rel.endsWith("/package.json")) { + let section = null; + lines.forEach((line, i) => { + const sec = line.match(/^\s*"(dependencies|devDependencies|peerDependencies)"\s*:/); + if (sec) return void (section = sec[1]); + if (/^\s*}/.test(line)) return void (section = null); + const m = section && line.match(/"([^"]+)"\s*:\s*"([~^]?\d[^"]*)"/); + if (m) add(i, m[1], m[2], "manifest", m[0]); + }); + } else if (!inFiles && rel.endsWith(".ts") && !SPEC.test(rel)) { + lines.forEach((line, i) => { + for (const m of line.matchAll(LITERAL)) add(i, m[2], m[3], "code", m[0]); + for (const m of line.matchAll(COMPUTED)) add(i, resolveConsts[m[1]] ?? `\${${m[1]}}`, m[2], "computed", m[0]); + }); + } + } +} + +const inScope = pins.filter((p) => frameworks.includes(p.fw) && (allDeps || isIgnite(p.name)) && + (!onlyPackages.length || onlyPackages.includes(p.name))); + +let registry = "https://registry.npmjs.org/"; +try { + registry = execSync("npm config get registry", { encoding: "utf8" }).trim().replace(/\/?$/, "/"); +} catch { /* default registry */ } + +const cache = new Map(); +function fetchJson(path, abbreviated) { + if (!cache.has(path)) { + cache.set(path, fetch(registry + path, { + headers: abbreviated ? { accept: "application/vnd.npm.install-v1+json" } : {} + }).then((r) => (r.ok ? r.json() : { error: `HTTP ${r.status}` })).catch((e) => ({ error: e.message }))); + } + return cache.get(path); +} +const encode = (name) => name.replace("/", "%2F"); +async function packument(name) { + const body = await fetchJson(encode(name), true); + if (body.error) return body; + return { latest: body["dist-tags"]?.latest, versions: Object.keys(body.versions || {}) }; +} +async function peersOf(name, version) { + const body = await fetchJson(`${encode(name)}/${version}`, false); + return body.error ? body : { peers: body.peerDependencies || {} }; +} + +const names = [...new Set(inScope.map((p) => p.name))].sort(); +const meta = Object.fromEntries(await Promise.all(names.map(async (n) => [n, await packument(n)]))); + +const prefixOf = (range) => range.match(/^[~^]/)?.[0] ?? ""; +function classify(range, target, versions) { + const resolved = semver.maxSatisfying(versions, range); + if (!resolved) return { status: "no-match", resolved }; + if (semver.satisfies(target, range)) return { status: "current", resolved }; + const min = semver.minVersion(range); + if (semver.lt(target, min)) return { status: "downgrade", resolved }; + const breaking = semver.major(target) !== min.major || (min.major === 0 && semver.minor(target) !== min.minor); + return { status: breaking ? "major" : semver.diff(min, target) === "patch" ? "patch" : "minor", resolved }; +} + +for (const p of inScope) { + const m = meta[p.name]; + if (m.error || !m.latest) { + Object.assign(p, { status: "error", error: m.error ?? "no latest tag" }); + continue; + } + p.target = targets[p.name] ?? m.latest; + if (!m.versions.includes(p.target)) { + Object.assign(p, { status: "missing-target" }); + continue; + } + Object.assign(p, classify(p.range, p.target, m.versions)); + if (isIgnite(p.name) && (p.status === "patch" || p.status === "minor")) p.newRange = prefixOf(p.range) + p.target; + if (p.status === "current" && prefixOf(p.range) === "^" && semver.gt(p.target, semver.minVersion(p.range))) p.raisable = true; +} + +const proceeds = (p) => p.status === "patch" || p.status === "minor"; +const updating = (p) => isIgnite(p.name) && proceeds(p); +const rangeAfter = (p) => (updating(p) ? p.newRange : p.range); +function stop(p, status, stopReason) { + Object.assign(p, { status, stopReason }); + delete p.newRange; +} + +// A package pinned in every primary framework is updated only when all of them are selected or it is named, +// so a single-framework run does not make the frameworks diverge. +const allPrimary = PRIMARY.every((fw) => frameworks.includes(fw)); +const named = (name) => onlyPackages.includes(name) || name in targets; +for (const p of inScope.filter(updating)) { + if (allPrimary || named(p.name) || !PRIMARY.every((fw) => pins.some((x) => x.fw === fw && x.name === p.name))) continue; + stop(p, "shared-skip", `pinned in ${PRIMARY.join(", ")}: select all three or name it`); +} + +async function findPeerConflicts() { + const found = []; + const checks = new Map(); + for (const p of inScope.filter((x) => isIgnite(x.name) && x.resolved)) { + checks.set(`${p.fw}|${p.name}|${p.resolved}|current`, { fw: p.fw, name: p.name, version: p.resolved, when: "current" }); + if (updating(p)) checks.set(`${p.fw}|${p.name}|${p.target}|after`, { fw: p.fw, name: p.name, version: p.target, when: "after" }); + } + // A failed lookup leaves the peers unchecked; it is reported as a conflict with an error so an update fails closed. + for (const c of checks.values()) { + const { peers, error } = await peersOf(c.name, c.version); + if (error) { + found.push({ ...c, error: `${c.name}@${c.version} lookup failed: ${error}` }); + continue; + } + for (const [peer, peerRange] of Object.entries(peers)) { + for (const pin of pins.filter((x) => x.fw === c.fw && x.name === peer)) { + const pm = await packument(peer); + const range = c.when === "after" ? rangeAfter(pin) : pin.range; + const at = { peer, peerRange, pin: `${pin.file}:${pin.line}`, pinRange: range, ignitePeer: isIgnite(peer) }; + if (pm.error) { + found.push({ ...c, ...at, error: `${peer} lookup failed: ${pm.error}` }); + continue; + } + const resolved = semver.maxSatisfying(pm.versions, range); + if (resolved && semver.satisfies(resolved, peerRange, { includePrerelease: true })) continue; + found.push({ ...c, ...at, resolved }); + } + } + } + return found; +} + +// Stops apply to the whole package: one stopped occurrence holds every other occurrence of that package. +// Holding a pin changes the ranges other packages' peers are checked against, so repeat until nothing changes. +let peerConflicts; +const stopCauses = new Map(); +for (let changed = true; changed;) { + changed = false; + peerConflicts = await findPeerConflicts(); + for (const c of peerConflicts.filter((x) => x.when === "after")) { + stopCauses.set(JSON.stringify(c), c); + for (const p of inScope.filter((x) => x.fw === c.fw && x.name === c.name && updating(x))) { + if (c.error) stop(p, "error", `peers unchecked: ${c.error}`); + else stop(p, "peer-stop", `${c.peer} ${c.peerRange} not met by ${c.pin} (${c.pinRange})`); + changed = true; + } + } + const blockers = {}; + for (const p of inScope) if (!proceeds(p) && p.status !== "current") blockers[p.name] ??= p; + for (const p of inScope.filter((x) => updating(x) && blockers[x.name])) { + const b = blockers[p.name]; + stop(p, "package-stop", `${b.status} at ${b.file}:${b.line}`); + changed = true; + } +} +// The last pass has no "after" conflicts left; keep the ones that caused a stop. +peerConflicts = [...peerConflicts, ...stopCauses.values()]; + +const byName = {}; +for (const p of inScope) (byName[p.name] ??= []).push(p); +const inconsistencies = Object.entries(byName).flatMap(([name, list]) => { + const out = []; + const ranges = [...new Set(list.map((p) => p.range))]; + const prefixes = [...new Set(list.map((p) => prefixOf(p.range) || "exact"))]; + const fws = [...new Set(list.map((p) => p.fw))]; + if (ranges.length > 1) out.push({ name, issue: "different ranges", detail: ranges.join(", ") }); + if (prefixes.length > 1) out.push({ name, issue: "mixed prefixes", detail: prefixes.join(", ") }); + if (fws.length > 1) out.push({ name, issue: "shared across frameworks", detail: fws.join(", ") }); + return out; +}); + +const applied = []; +if (apply) { + const byFile = {}; + for (const p of inScope.filter(updating)) (byFile[p.file] ??= []).push(p); + for (const [file, list] of Object.entries(byFile)) { + const abs = join(root, file); + const text = readFileSync(abs, "utf8"); + const eol = text.includes("\r\n") ? "\r\n" : "\n"; + const lines = text.split(/\r?\n/); + for (const p of list) { + const at = p.token.lastIndexOf(p.range); + const next = p.token.slice(0, at) + p.newRange + p.token.slice(at + p.range.length); + const before = lines[p.line - 1]; + lines[p.line - 1] = before.replace(p.token, next); + applied.push({ file, line: p.line, name: p.name, from: p.range, to: p.newRange, ok: before !== lines[p.line - 1] }); + } + writeFileSync(abs, lines.join(eol)); + } +} + +const strip = ({ token, ...rest }) => rest; +if (asJson) { + console.log(JSON.stringify({ registry, frameworks, pins: inScope.map(strip), peerConflicts, inconsistencies, applied }, null, 2)); + process.exit(0); +} + +function table(head, rows) { + const w = head.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i] ?? "").length))); + const fmt = (r) => r.map((c, i) => String(c ?? "").padEnd(w[i])).join(" ").trimEnd(); + console.log([fmt(head), w.map((n) => "-".repeat(n)).join(" "), ...rows.map(fmt)].join("\n")); +} + +console.log(`Frameworks: ${frameworks.join(", ")}${allDeps ? " (all dependencies; non-Ignite UI rows are report-only)" : ""}\n`); +console.log("Peer conflicts"); +if (peerConflicts.length) { + table(["fw", "package@version", "when", "peer", "peer range", "pin range", "resolves to", "pin location"], + peerConflicts.map((c) => [c.fw, `${c.name}@${c.version}`, c.when, c.peer, c.peerRange, c.pinRange, c.error ?? c.resolved ?? "nothing", c.pin])); +} else console.log(" none"); + +console.log("\nPins"); +table(["status", "package", "range", "target", "new range", "kind", "location"], + inScope.map((p) => [p.status + (p.raisable ? "*" : ""), p.name, p.range, p.target ?? p.error, p.newRange ?? p.stopReason ?? "", p.kind, `${p.file}:${p.line}`])); +if (inScope.some((p) => p.raisable)) console.log("* current, but the ^ minimum could be raised to the target"); + +if (inconsistencies.length) { + console.log("\nInconsistencies"); + table(["package", "issue", "detail"], inconsistencies.map((i) => [i.name, i.issue, i.detail])); +} + +if (apply) { + console.log("\nApplied"); + applied.forEach((a) => console.log(` ${a.ok ? "updated" : "FAILED "} ${a.file}:${a.line} ${a.name} ${a.from} -> ${a.to}`)); + if (!applied.length) console.log(" nothing to apply"); +} + +const counts = inScope.reduce((acc, p) => ((acc[p.status] = (acc[p.status] || 0) + 1), acc), {}); +console.log(`\n${inScope.length} pins, ${names.length} packages: ${Object.entries(counts).map(([k, v]) => `${k}=${v}`).join(" ")}`); diff --git a/.claude/agent-rules.md b/.claude/agent-rules.md new file mode 100644 index 000000000..766fffa7f --- /dev/null +++ b/.claude/agent-rules.md @@ -0,0 +1,13 @@ +# Working Rules for the Maintenance Agents + +These rules apply to every agent in [`agents/`](./agents/). The repository guide is +[`AGENTS.md`](../AGENTS.md) at the root; read it before making changes. This file only adds the boundaries that apply to agents. No tool enforces them; you are responsible for following them. + +- **Never commit.** Do not run `git add`, `git commit`, `git push`, `git stash`, `git checkout -b`, `git branch`, or open pull requests. If you changed files, leave them in the working tree and report the diff (`git status --short`, `git diff --stat`). A human reviews and commits. +- Temporary files (e.g. `npm pack` output for checking a package's type declarations) go in a temporary directory **outside the repository**. The one exception is `output/` (gitignored), which belongs to `scripts/smoke-test.sh`: smoke runs and commands inside a kept smoke project may write there. Do not edit files under `output/` directly. +- Do not edit compiled output (`*.js`, `*.js.map` next to a `.ts` source); edit the `.ts` and rebuild. +- Do not edit the repository's own manifests or lockfiles: root `package.json`, `packages/*/package.json`, `packages/igniteui-mcp/igniteui-doc-mcp/package.json` and `server.json`, `yarn.lock`, any `package-lock.json`. Project-template manifests under `**/files/package.json` are template content and may be edited when the task calls for it. +- Do not change the CLI's own version (`lerna`, `lerna.json`, `scripts/versionScript.ts`). +- Do not edit anything under `.git/`, `node_modules/` or `coverage/`. +- Do not read secrets: `.env` and `.env.*` (at any depth), `secrets/`, `*.pem`, `*.key`, `*.pfx`, `.npmrc`. +- When something is ambiguous or out of scope, stop and report instead of guessing. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000..5c33c1af5 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,34 @@ +name: Copilot Setup Steps + +# Prepares the Copilot cloud agent environment. Takes effect only from the default branch. +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Use Node.js 24.x + uses: actions/setup-node@v6 + with: + node-version: 24.x + cache: 'yarn' + - name: Install packages + run: yarn --frozen-lockfile + - name: Build + run: npm run build + # Generated projects run their tests in Chromium; the browser download and OS packages are outside the agent firewall only here + - name: Install Playwright Chromium + run: npx -y playwright@1 install --with-deps chromium diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..3336cce1e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,96 @@ +# AGENTS.md + +Guidance for AI coding agents working in this repository. + +## Build, Test, Lint + +```bash +yarn install # REQUIRED — must use yarn (not npm) for workspace resolution +npm run build # tsc + config schema generation + MCP server build +npm run test # lint + build + tests with coverage (via pretest hook) +npm run jasmine # tests only — no lint/build/coverage +npm run lint # ESLint (flat config in eslint.config.mjs) +scripts/smoke-test.sh # scaffold + add every template + install + build +``` + +Tests are Jasmine specs under `spec/` (unit + acceptance) and within individual packages. + +`scripts/smoke-test.sh` is the only check that a generated project actually installs and builds — the Jasmine specs stub `PackageManager.installPackages`. Run it after touching anything under `packages/cli/templates/` or `packages/igx-templates/`. Running one spec file, the specs that guard templates, running the smoke test, reading its results and editing the script: `igniteui-cli-smoke-test` skill. + +### MCP Server (`packages/igniteui-mcp/igniteui-doc-mcp`) + +Workspace package — part of yarn workspaces and lerna. Built automatically by `npm run build` via the `build:mcp` script. Has its own `tsconfig.json` (ES2022/ESM, separate from root CJS config). + +```bash +npm run build:mcp # build MCP server (also runs as part of npm run build) +cd packages/igniteui-mcp/igniteui-doc-mcp +npm run build:db # build SQLite DB from compressed docs (manual step) +npm run pipeline:angular # incremental doc pipeline (also: react, blazor, webcomponents) +npm run pipeline:angular:full # full rebuild pipeline +npm run inspector # test with MCP Inspector +``` + +Adding or changing an MCP tool (schema, handler, description, tests, every doc that lists the tool): `igniteui-cli-mcp-tool` skill. + +## Architecture + +**Monorepo** managed with lerna + yarn workspaces. Five packages: + +| Package | npm | Path | +|---------|-----|------| +| CLI (entry point) | `igniteui-cli` | `packages/cli` | +| Core (shared logic) | `@igniteui/cli-core` | `packages/core` | +| Angular Templates | `@igniteui/angular-templates` | `packages/igx-templates` | +| Angular Schematics | `@igniteui/angular-schematics` | `packages/ng-schematics` | +| MCP Server | `@igniteui/mcp-server` | `packages/igniteui-mcp/igniteui-doc-mcp` | + +**Core is the dependency root** — changes to `packages/core` affect all other packages. Always rebuild the full monorepo after core changes. + +### Build System + +- TypeScript compiled with `tsc` directly (no bundler). Two configs: `tsconfig.json` (dev) and `tsconfig-pack.json` (production). +- `packages/core/types/Config.ts` is source of truth for config schema. Modifying it requires `npm run config-schema` (included in `npm run build`). +- Monorepo targets ES6/CommonJS. MCP server targets ES2022 with Node16 module resolution (`"type": "module"`). +- Path aliases in tsconfig: `@igniteui/cli-core` → `packages/core`, `@igniteui/angular-templates` → `packages/igx-templates`, etc. + +### Template System + +- Placeholders are substituted by the CLI, not a template engine: `<%=ClassName%>` style in Angular templates, `$(ClassName)` style in React and Web Components templates +- Templates in `packages/cli/templates/**/files/` and `packages/igx-templates/` +- Component/template IDs (e.g., "grid", "combo") used by `ig add` command +- Default framework is **jQuery** when not specified + +### MCP Server + +- `McpServer` from `@modelcontextprotocol/sdk` with stdio transport +- Two modes: **Local** (default, uses sql.js WASM with FTS4 over the bundled database) and **Remote** (`--remote `, proxies to the .NET docs backend; `IGNITEUI_MCP_DOCS_BACKEND_URL` supplies the URL but does not enable remote mode on its own) +- Framework detection via component prefix: `Igx` → Angular, `Igr` → React, `Igc` → Web Components, `Igb` → Blazor +- DB build uses `better-sqlite3` (native, dev only); runtime uses `sql.js` (WASM) +- Doc compression uses OpenAI API (`OPENAI_API_KEY` in `.env`) +- Git submodules under `packages/igniteui-mcp/igniteui-doc-mcp/` provide documentation source repos +- Consult `packages/igniteui-mcp/igniteui-doc-mcp/docs/knowledgebase.md` before debugging pipeline problems + +## Key Conventions + +- **yarn for install, npm for scripts** +- CI runs on Node 24.x and 26.x with `yarn --frozen-lockfile`; Copilot cloud agent sessions are prepared by `.github/workflows/copilot-setup-steps.yml` (Node 24, install, build, Playwright Chromium) +- ESLint: `no-var`, `no-eval`, `prefer-const`, `no-console` (off in test files), `no-duplicate-imports` +- Test files match pattern `*[sS]pec.ts` — found in `spec/`, `packages/cli/migrations/`, `packages/igx-templates/`, `packages/ng-schematics/` + +## Maintenance Workflows + +Template maintenance procedures are skills in `.agents/skills/`. Before adding or changing a workflow, read the guide in `.agents/README.md`. + +These rules bind every run of a workflow skill, whether from the main session or a subagent. No tool enforces them. + +- **Git** depends on where the workflow runs: + - **Local session** (editor or terminal, a person at the keyboard): never commit. Leave changes in the working tree and report them with `git status --short` and `git diff --stat`; a human reviews and commits. Git stays read-only (`status`, `diff`, `log`, `show`): do not run `git add`, `git commit`, `git push`, `git stash`, `git checkout -b`, `git branch`, or any other git command that writes, and do not open pull requests. Commit only when the user explicitly asks in that session. + - **Cloud agent session** (started on GitHub, e.g. Copilot cloud agent assigned an issue or asked from the web UI, expected to deliver a pull request): commit to the session's own working branch and open or update that session's pull request. Fill the PR description from the workflow's Final Report, using the sections of `.github/PULL_REQUEST_TEMPLATE.md`; the report's diff item becomes `git diff --stat ...HEAD`. Use the workflow's proposed CHANGELOG line, in conventional form, as the PR title and commit subject (e.g. `chore(angular): update scaffolded igniteui-angular to ~22.2.1`). A stop item that blocks the whole task leaves the PR as a draft with the stop items listed first; a reviewer answers in a PR comment. A `dry-run` or other read-only request commits nothing; its report is the session's answer (the PR description if the platform opened one). + - **Never, in either mode**: push to `master`, release branches or any branch other than the session's own; force-push, rebase or otherwise rewrite pushed history; merge, approve or close pull requests; create tags or releases; change other pull requests or issues; commit submodule pointer changes. +- Temporary files (e.g. `npm pack` output for checking a package's type declarations) go in a temporary directory **outside the repository**. The one exception is `output/` (gitignored), which belongs to `scripts/smoke-test.sh`: smoke runs and commands inside a kept smoke project write there; files under `output/` change only through those runs. +- Edit the `.ts` source and rebuild; compiled output (`*.js`, `*.js.map` next to a `.ts`) is never edited by hand. +- Leave the repository's own manifests and lockfiles untouched: root `package.json`, `packages/*/package.json`, `packages/igniteui-mcp/igniteui-doc-mcp/package.json` and `server.json`, `yarn.lock`, any `package-lock.json`. Project-template manifests under `**/files/package.json` are template content and may be edited when the task calls for it. +- Leave the CLI's own version alone: do not run `lerna`, and do not edit `lerna.json` or `scripts/versionScript.ts`. Leave anything under `.git/`, `coverage/` or any `node_modules/` untouched. +- Leave secrets unread, at any depth: `.env`, `.env.*`, `.npmrc`, `secrets/`, `*.pem`, `*.key`, `*.pfx`. When a task seems to need one, stop and ask. +- When something is ambiguous or out of scope, stop and report instead of guessing. In a cloud agent session, "stop and ask" means writing the question into the PR description and leaving the PR as a draft. +- Cloud agent sessions have a time limit (59 minutes for Copilot cloud agent) and a network firewall. Scope smoke runs to what changed (`--templates`, one framework per run) instead of the full matrix, and report a blocked host as an environment failure with the host name. diff --git a/CLAUDE.md b/CLAUDE.md index adb13f529..43c994c2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,90 +1 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Build, Test, Lint - -```bash -yarn install # REQUIRED — must use yarn (not npm) for workspace resolution -npm run build # TypeScript compilation (tsc) + config schema generation -npm run test # lint + build + tests with coverage (via pretest hook) -npm run jasmine # tests only — no lint/build/coverage -npm run lint # ESLint (flat config in eslint.config.mjs) -scripts/smoke-test.sh # scaffold + add every template + install + build (see below) -``` - -To run a single test: edit `spec/jasmine-runner.ts` to filter `spec_files`, then `npm run jasmine`. Tests are Jasmine specs under `spec/` (unit + acceptance) and within individual packages. - -### Template smoke test - -`scripts/smoke-test.sh` is the only check that a generated project actually installs and builds — -the Jasmine specs stub `PackageManager.installPackages`. Run it after touching anything under -`packages/cli/templates/` or `packages/igx-templates/`. Requires `npm run build` first. - -```bash -scripts/smoke-test.sh -f angular --templates grid,combo # fast path, ~2 min -scripts/smoke-test.sh # angular+react+webcomponents, ~20-30 min -``` - -Results in `output/smoke/results.tsv`, per-step logs in `output/smoke/logs/`. Two constraints when -editing it: every `ig` call must precede `npm install` (otherwise `bin/execute.js` delegates to the -project-local *published* CLI), and exit codes alone are not trustworthy (`Util.error` returns -without setting one) — steps are also judged on a log scan plus an artifact check. - -### MCP Server (`packages/igniteui-mcp/igniteui-doc-mcp`) - -Workspace package — part of yarn workspaces and lerna. Built automatically by `npm run build` via the `build:mcp` script. Has its own `tsconfig.json` (ES2022/ESM, separate from root CJS config). - -```bash -npm run build:mcp # build MCP server (also runs as part of npm run build) -cd packages/igniteui-mcp/igniteui-doc-mcp -npm run build:db # build SQLite DB from compressed docs (manual step) -npm run pipeline:angular # incremental doc pipeline (also: react, blazor, webcomponents) -npm run pipeline:angular:full # full rebuild pipeline -npm run inspector # test with MCP Inspector -``` - -## Architecture - -**Monorepo** managed with lerna + yarn workspaces. Five packages: - -| Package | npm | Path | -|---------|-----|------| -| CLI (entry point) | `igniteui-cli` | `packages/cli` | -| Core (shared logic) | `@igniteui/cli-core` | `packages/core` | -| Angular Templates | `@igniteui/angular-templates` | `packages/igx-templates` | -| Angular Schematics | `@igniteui/angular-schematics` | `packages/ng-schematics` | -| MCP Server | `@igniteui/mcp-server` | `packages/igniteui-mcp/igniteui-doc-mcp` | - -**Core is the dependency root** — changes to `packages/core` affect all other packages. Always rebuild the full monorepo after core changes. - -### Build System - -- TypeScript compiled with `tsc` directly (no bundler). Two configs: `tsconfig.json` (dev) and `tsconfig-pack.json` (production). -- `packages/core/types/Config.ts` is source of truth for config schema. Modifying it requires `npm run config-schema` (included in `npm run build`). -- Monorepo targets ES6/CommonJS. MCP server targets ES2022 with Node16 module resolution (`"type": "module"`). -- Path aliases in tsconfig: `@igniteui/cli-core` → `packages/core`, `@igniteui/angular-templates` → `packages/igx-templates`, etc. - -### Template System - -- Handlebars for template processing -- Templates in `packages/cli/templates/**/files/` and `packages/igx-templates/` -- Component/template IDs (e.g., "grid", "combo") used by `ig add` command -- Default framework is **jQuery** when not specified - -### MCP Server - -- `McpServer` from `@modelcontextprotocol/sdk` with stdio transport -- Two modes: **Remote** (default, proxies to `DOCS_BACKEND_URL`) and **Local** (`--local` flag, uses sql.js WASM with FTS4) -- Framework detection via component prefix: `Igx` → Angular, `Igr` → React, `Igc` → Web Components, `Igb` → Blazor -- DB build uses `better-sqlite3` (native, dev only); runtime uses `sql.js` (WASM) -- Doc compression uses OpenAI API (`OPENAI_API_KEY` in `.env`) -- Git submodules under `packages/igniteui-mcp/igniteui-doc-mcp/` provide documentation source repos -- Consult `packages/igniteui-mcp/igniteui-doc-mcp/docs/knowledgebase.md` before debugging pipeline problems - -## Key Conventions - -- **yarn for install, npm for scripts** -- CI runs on Node 20 with `yarn --frozen-lockfile` -- ESLint: `no-var`, `no-eval`, `prefer-const`, `no-console` (off in test files), `no-duplicate-imports` -- Test files match pattern `*[sS]pec.ts` — found in `spec/`, `packages/cli/migrations/`, `packages/igx-templates/`, `packages/ng-schematics/` +@AGENTS.md