From 16f555663b8c071843d6e49637a8842469fee2ff Mon Sep 17 00:00:00 2001 From: Rainer Hahnekamp Date: Sat, 11 Jul 2026 13:00:15 +0200 Subject: [PATCH] feat(@schematics/angular): install official agent skills --- .../schematics/angular/ai-config/index.ts | 10 +- .../angular/ai-config/index_spec.ts | 76 +++++++++++++- .../angular/ai-config/install-skills.ts | 98 +++++++++++++++++++ .../schematics/angular/ai-config/schema.json | 7 +- packages/schematics/angular/collection.json | 6 ++ packages/schematics/angular/ng-new/index.ts | 13 ++- .../schematics/angular/ng-new/index_spec.ts | 25 +++++ .../schematics/angular/ng-new/schema.json | 35 +++++++ 8 files changed, 265 insertions(+), 5 deletions(-) create mode 100644 packages/schematics/angular/ai-config/install-skills.ts diff --git a/packages/schematics/angular/ai-config/index.ts b/packages/schematics/angular/ai-config/index.ts index 7dab2fccbdef..7306c67769e6 100644 --- a/packages/schematics/angular/ai-config/index.ts +++ b/packages/schematics/angular/ai-config/index.ts @@ -8,6 +8,7 @@ import { Rule, chain, noop, strings } from '@angular-devkit/schematics'; import { addBestPracticesMarkdown, addJsonMcpConfig, addTomlMcpConfig } from './file_utils'; +import { createAngularSkillsTask } from './install-skills'; import { Schema as ConfigOptions, Tool } from './schema'; import { ContextFileInfo, ContextFileType, FileConfigurationHandlerOptions } from './types'; @@ -64,7 +65,7 @@ const AI_TOOLS: { [key in Exclude]: ContextFileInfo[] } = { ], }; -export default function ({ tool }: ConfigOptions): Rule { +export default function ({ tool, aiSkills }: ConfigOptions): Rule { return (tree, context) => { if (!tool) { return noop(); @@ -103,6 +104,13 @@ export default function ({ tool }: ConfigOptions): Rule { }), ); + if (aiSkills) { + const selectedTools = tool.filter((tool) => tool !== Tool.None); + if (selectedTools.length > 0) { + context.addTask(createAngularSkillsTask(selectedTools)); + } + } + return chain(rules); }; } diff --git a/packages/schematics/angular/ai-config/index_spec.ts b/packages/schematics/angular/ai-config/index_spec.ts index f4408e96444c..0d5fb7672404 100644 --- a/packages/schematics/angular/ai-config/index_spec.ts +++ b/packages/schematics/angular/ai-config/index_spec.ts @@ -9,6 +9,7 @@ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import { parse } from 'jsonc-parser'; import { Schema as WorkspaceOptions } from '../workspace/schema'; +import { getAngularSkillsInstallArguments } from './install-skills'; import { Schema as ConfigOptions, Tool as ConfigTool } from './schema'; describe('AI Config Schematic', () => { @@ -24,14 +25,57 @@ describe('AI Config Schematic', () => { }; let workspaceTree: UnitTestTree; - function runAiConfigSchematic(tool: ConfigTool[]): Promise { - return schematicRunner.runSchematic('ai-config', { tool }, workspaceTree); + function runAiConfigSchematic( + tool: ConfigTool[], + options: Partial = {}, + ): Promise { + return schematicRunner.runSchematic( + 'ai-config', + { tool, ...options }, + workspaceTree, + ); } beforeEach(async () => { workspaceTree = await schematicRunner.runSchematic('workspace', workspaceOptions); }); + it('should create a non-interactive skills command for each selected AI tool', () => { + expect( + getAngularSkillsInstallArguments( + [ + ConfigTool.ClaudeCode, + ConfigTool.Cursor, + ConfigTool.GeminiCli, + ConfigTool.OpenAiCodex, + ConfigTool.Vscode, + ], + '^22.1.0-next.0', + ), + ).toEqual([ + '--yes', + 'skills', + 'add', + 'https://github.com/angular/skills/tree/22.1.x', + '--skill', + 'angular-developer', + '--skill', + 'angular-new-app', + '--agent', + 'claude-code', + '--agent', + 'cursor', + '--agent', + 'gemini-cli', + '--agent', + 'codex', + '--agent', + 'github-copilot', + '--copy', + '--yes', + ]); + }); + it('should create Angular MCP server config and AGENTS.md for Claude Code', async () => { const tree = await runAiConfigSchematic([ConfigTool.ClaudeCode]); expect(tree.exists('AGENTS.md')).toBeTruthy(); @@ -109,6 +153,34 @@ describe('AI Config Schematic', () => { } }); + it('should schedule Angular skills installation when enabled', async () => { + await runAiConfigSchematic([ConfigTool.ClaudeCode], { aiSkills: true }); + + expect(schematicRunner.tasks.length).toBe(1); + expect(schematicRunner.tasks[0]).toEqual({ + name: 'run-schematic', + options: { + collection: null, + name: 'ai-config-install-skills', + options: { + tools: ['claude-code'], + }, + }, + }); + }); + + it('should not install Angular skills when the user declines', async () => { + await runAiConfigSchematic([ConfigTool.ClaudeCode], { aiSkills: false }); + + expect(schematicRunner.tasks).toEqual([]); + }); + + it('should not install Angular skills when no AI tool is selected', async () => { + await runAiConfigSchematic([ConfigTool.None], { aiSkills: true }); + + expect(schematicRunner.tasks).toEqual([]); + }); + it('should update JSON MCP server config, if the file exists', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const jsonConfig: Record = { diff --git a/packages/schematics/angular/ai-config/install-skills.ts b/packages/schematics/angular/ai-config/install-skills.ts new file mode 100644 index 000000000000..55d7833915fa --- /dev/null +++ b/packages/schematics/angular/ai-config/install-skills.ts @@ -0,0 +1,98 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { Rule } from '@angular-devkit/schematics'; +import { RunSchematicTask } from '@angular-devkit/schematics/tasks'; +import { execFileSync } from 'node:child_process'; +import { resolve } from 'node:path'; +import { latestVersions } from '../utility/latest-versions'; +import { Tool } from './schema'; + +type SkillsTool = Exclude; + +interface AngularSkillsInstallOptions { + tools: readonly string[]; + workingDirectory?: string; +} + +const SKILLS_AGENT: { [key in SkillsTool]: string } = { + ['claude-code']: 'claude-code', + cursor: 'cursor', + ['gemini-cli']: 'gemini-cli', + ['open-ai-codex']: 'codex', + vscode: 'github-copilot', +}; + +const ANGULAR_SKILLS_REPOSITORY = 'https://github.com/angular/skills'; + +export function getAngularSkillsRepository(angularVersion = latestVersions.Angular): string { + const version = angularVersion.match(/(\d+)\.(\d+)/); + if (!version) { + throw new Error(`Unable to determine the Angular release line from '${angularVersion}'.`); + } + + return `${ANGULAR_SKILLS_REPOSITORY}/tree/${version[1]}.${version[2]}.x`; +} + +export function getAngularSkillsInstallArguments( + tools: readonly string[], + angularVersion = latestVersions.Angular, +): string[] { + const agents = tools.flatMap((tool) => { + const agent = SKILLS_AGENT[tool as SkillsTool]; + if (!agent) { + throw new Error(`Unsupported AI tool '${tool}' for Angular Agent Skills.`); + } + + return ['--agent', agent]; + }); + + return [ + '--yes', + 'skills', + 'add', + getAngularSkillsRepository(angularVersion), + '--skill', + 'angular-developer', + '--skill', + 'angular-new-app', + ...agents, + '--copy', + '--yes', + ]; +} + +export function createAngularSkillsTask( + tools: readonly string[], + workingDirectory?: string, +): RunSchematicTask { + return new RunSchematicTask( + 'ai-config-install-skills', + workingDirectory === undefined ? { tools } : { tools, workingDirectory }, + ); +} + +export default function (options: AngularSkillsInstallOptions): Rule { + return (_tree, context) => { + const args = getAngularSkillsInstallArguments(options.tools); + + try { + execFileSync('npx', args, { + cwd: resolve(options.workingDirectory ?? '.'), + env: { ...process.env, DISABLE_TELEMETRY: '1' }, + stdio: 'inherit', + shell: process.platform === 'win32', + }); + } catch { + context.logger.warn( + 'Angular Agent Skills could not be installed.\n' + + `When you are online, install them manually with:\n npx ${args.join(' ')}`, + ); + } + }; +} diff --git a/packages/schematics/angular/ai-config/schema.json b/packages/schematics/angular/ai-config/schema.json index 4cb63468ae53..32e5de2b7d0b 100644 --- a/packages/schematics/angular/ai-config/schema.json +++ b/packages/schematics/angular/ai-config/schema.json @@ -4,7 +4,7 @@ "title": "Angular AI Config File Options Schema", "type": "object", "additionalProperties": false, - "description": "Generates AI configuration files for Angular projects. This schematic creates AGENTS.md file and Angular MCP server configuration, improving the quality of AI-generated code and suggestions.", + "description": "Generates AI configuration files for Angular projects and can install the official Angular Agent Skills.", "properties": { "tool": { "type": "array", @@ -45,6 +45,11 @@ "type": "string", "enum": ["none", "claude-code", "cursor", "gemini-cli", "open-ai-codex", "vscode"] } + }, + "aiSkills": { + "type": "boolean", + "description": "Installs the official Angular Agent Skills locally for the selected AI tools.", + "x-prompt": "Install the official Angular Agent Skills for the selected AI tools? This downloads skills from github.com/angular/skills." } } } diff --git a/packages/schematics/angular/collection.json b/packages/schematics/angular/collection.json index 29b361ccafbb..4846464b5e87 100755 --- a/packages/schematics/angular/collection.json +++ b/packages/schematics/angular/collection.json @@ -131,6 +131,12 @@ "schema": "./ai-config/schema.json", "description": "Generates an AI tool configuration file." }, + "ai-config-install-skills": { + "factory": "./ai-config/install-skills", + "hidden": true, + "private": true, + "description": "[INTERNAL] Installs the official Angular Agent Skills." + }, "tailwind": { "factory": "./tailwind", "schema": "./tailwind/schema.json", diff --git a/packages/schematics/angular/ng-new/index.ts b/packages/schematics/angular/ng-new/index.ts index 856343e82b8f..985a1f0859a3 100644 --- a/packages/schematics/angular/ng-new/index.ts +++ b/packages/schematics/angular/ng-new/index.ts @@ -22,6 +22,7 @@ import { NodePackageInstallTask, RepositoryInitializerTask, } from '@angular-devkit/schematics/tasks'; +import { createAngularSkillsTask } from '../ai-config/install-skills'; import { Schema as ApplicationOptions } from '../application/schema'; import { JSONFile } from '../utility/json-file'; import { Schema as WorkspaceOptions } from '../workspace/schema'; @@ -81,6 +82,7 @@ export default function (options: NgNewOptions): Rule { options.createApplication ? schematic('application', applicationOptions) : noop, schematic('ai-config', { tool: options.aiConfig?.length ? options.aiConfig : undefined, + aiSkills: false, }), move(options.directory), ]), @@ -95,13 +97,22 @@ export default function (options: NgNewOptions): Rule { }), ); } + + let skillsTask; + const aiTools = (options.aiConfig ?? []).filter((tool) => tool !== 'none'); + if (options.aiSkills && aiTools.length > 0) { + skillsTask = context.addTask( + createAngularSkillsTask(aiTools, options.directory), + packageTask ? [packageTask] : [], + ); + } if (!options.skipGit) { const commit = typeof options.commit == 'object' ? options.commit : options.commit ? {} : false; context.addTask( new RepositoryInitializerTask(options.directory, commit), - packageTask ? [packageTask] : [], + skillsTask ? [skillsTask] : packageTask ? [packageTask] : [], ); } }, diff --git a/packages/schematics/angular/ng-new/index_spec.ts b/packages/schematics/angular/ng-new/index_spec.ts index a9a6b4a1b6b2..812dd008431d 100644 --- a/packages/schematics/angular/ng-new/index_spec.ts +++ b/packages/schematics/angular/ng-new/index_spec.ts @@ -115,6 +115,31 @@ describe('Ng New Schematic', () => { expect(files).toContain('/bar/.gemini/settings.json'); }); + it('should install Angular skills before initializing the repository', async () => { + await schematicRunner.runSchematic('ng-new', { + ...defaultOptions, + aiConfig: ['gemini-cli', 'claude-code'], + aiSkills: true, + }); + + const skillsTask = schematicRunner.tasks.find( + (task) => (task.options as { name?: string })?.name === 'ai-config-install-skills', + ); + expect(skillsTask?.options).toEqual( + jasmine.objectContaining({ + options: { + workingDirectory: 'bar', + tools: ['gemini-cli', 'claude-code'], + }, + }), + ); + expect( + schematicRunner.tasks.findIndex( + (task) => (task.options as { name?: string })?.name === 'ai-config-install-skills', + ), + ).toBeLessThan(schematicRunner.tasks.findIndex((task) => task.name === 'repo-init')); + }); + it('should create a tailwind project when style is tailwind', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const options = { ...defaultOptions, style: 'tailwind' as any }; diff --git a/packages/schematics/angular/ng-new/schema.json b/packages/schematics/angular/ng-new/schema.json index 257628fcda9e..52701cb405ee 100644 --- a/packages/schematics/angular/ng-new/schema.json +++ b/packages/schematics/angular/ng-new/schema.json @@ -152,12 +152,47 @@ "aiConfig": { "type": "array", "uniqueItems": true, + "x-prompt": { + "message": "Which AI tools should Angular integrate with? https://angular.dev/ai/develop-with-ai", + "type": "list", + "items": [ + { + "value": "none", + "label": "None" + }, + { + "value": "claude-code", + "label": "Claude Code [ `AGENTS.md` + Angular MCP server config ]" + }, + { + "value": "cursor", + "label": "Cursor [ `AGENTS.md` + Angular MCP server config ]" + }, + { + "value": "gemini-cli", + "label": "Gemini CLI [ `GEMINI.md` + Angular MCP server config ]" + }, + { + "value": "open-ai-codex", + "label": "Open AI Codex [ `AGENTS.md` + Angular MCP server config ]" + }, + { + "value": "vscode", + "label": "VSCode [ `AGENTS.md` + Angular MCP server config ]" + } + ] + }, "description": "Specifies which AI tools to generate configuration files for. These file are used to improve the outputs of AI tools by following the best practices.", "items": { "type": "string", "enum": ["none", "claude-code", "cursor", "gemini-cli", "open-ai-codex", "vscode"] } }, + "aiSkills": { + "type": "boolean", + "description": "Installs the official Angular Agent Skills locally for the selected AI tools. When omitted with non-interactive defaults, skills are not installed.", + "x-prompt": "Install the official Angular Agent Skills for the selected AI tools? This downloads skills from github.com/angular/skills." + }, "fileNameStyleGuide": { "type": "string", "enum": ["2016", "2025"],