Skip to content

Commit 16f5556

Browse files
feat(@schematics/angular): install official agent skills
1 parent 035c72a commit 16f5556

8 files changed

Lines changed: 265 additions & 5 deletions

File tree

packages/schematics/angular/ai-config/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { Rule, chain, noop, strings } from '@angular-devkit/schematics';
1010
import { addBestPracticesMarkdown, addJsonMcpConfig, addTomlMcpConfig } from './file_utils';
11+
import { createAngularSkillsTask } from './install-skills';
1112
import { Schema as ConfigOptions, Tool } from './schema';
1213
import { ContextFileInfo, ContextFileType, FileConfigurationHandlerOptions } from './types';
1314

@@ -64,7 +65,7 @@ const AI_TOOLS: { [key in Exclude<Tool, Tool.None>]: ContextFileInfo[] } = {
6465
],
6566
};
6667

67-
export default function ({ tool }: ConfigOptions): Rule {
68+
export default function ({ tool, aiSkills }: ConfigOptions): Rule {
6869
return (tree, context) => {
6970
if (!tool) {
7071
return noop();
@@ -103,6 +104,13 @@ export default function ({ tool }: ConfigOptions): Rule {
103104
}),
104105
);
105106

107+
if (aiSkills) {
108+
const selectedTools = tool.filter((tool) => tool !== Tool.None);
109+
if (selectedTools.length > 0) {
110+
context.addTask(createAngularSkillsTask(selectedTools));
111+
}
112+
}
113+
106114
return chain(rules);
107115
};
108116
}

packages/schematics/angular/ai-config/index_spec.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
1010
import { parse } from 'jsonc-parser';
1111
import { Schema as WorkspaceOptions } from '../workspace/schema';
12+
import { getAngularSkillsInstallArguments } from './install-skills';
1213
import { Schema as ConfigOptions, Tool as ConfigTool } from './schema';
1314

1415
describe('AI Config Schematic', () => {
@@ -24,14 +25,57 @@ describe('AI Config Schematic', () => {
2425
};
2526

2627
let workspaceTree: UnitTestTree;
27-
function runAiConfigSchematic(tool: ConfigTool[]): Promise<UnitTestTree> {
28-
return schematicRunner.runSchematic<ConfigOptions>('ai-config', { tool }, workspaceTree);
28+
function runAiConfigSchematic(
29+
tool: ConfigTool[],
30+
options: Partial<ConfigOptions> = {},
31+
): Promise<UnitTestTree> {
32+
return schematicRunner.runSchematic<ConfigOptions>(
33+
'ai-config',
34+
{ tool, ...options },
35+
workspaceTree,
36+
);
2937
}
3038

3139
beforeEach(async () => {
3240
workspaceTree = await schematicRunner.runSchematic('workspace', workspaceOptions);
3341
});
3442

43+
it('should create a non-interactive skills command for each selected AI tool', () => {
44+
expect(
45+
getAngularSkillsInstallArguments(
46+
[
47+
ConfigTool.ClaudeCode,
48+
ConfigTool.Cursor,
49+
ConfigTool.GeminiCli,
50+
ConfigTool.OpenAiCodex,
51+
ConfigTool.Vscode,
52+
],
53+
'^22.1.0-next.0',
54+
),
55+
).toEqual([
56+
'--yes',
57+
'skills',
58+
'add',
59+
'https://github.com/angular/skills/tree/22.1.x',
60+
'--skill',
61+
'angular-developer',
62+
'--skill',
63+
'angular-new-app',
64+
'--agent',
65+
'claude-code',
66+
'--agent',
67+
'cursor',
68+
'--agent',
69+
'gemini-cli',
70+
'--agent',
71+
'codex',
72+
'--agent',
73+
'github-copilot',
74+
'--copy',
75+
'--yes',
76+
]);
77+
});
78+
3579
it('should create Angular MCP server config and AGENTS.md for Claude Code', async () => {
3680
const tree = await runAiConfigSchematic([ConfigTool.ClaudeCode]);
3781
expect(tree.exists('AGENTS.md')).toBeTruthy();
@@ -109,6 +153,34 @@ describe('AI Config Schematic', () => {
109153
}
110154
});
111155

156+
it('should schedule Angular skills installation when enabled', async () => {
157+
await runAiConfigSchematic([ConfigTool.ClaudeCode], { aiSkills: true });
158+
159+
expect(schematicRunner.tasks.length).toBe(1);
160+
expect(schematicRunner.tasks[0]).toEqual({
161+
name: 'run-schematic',
162+
options: {
163+
collection: null,
164+
name: 'ai-config-install-skills',
165+
options: {
166+
tools: ['claude-code'],
167+
},
168+
},
169+
});
170+
});
171+
172+
it('should not install Angular skills when the user declines', async () => {
173+
await runAiConfigSchematic([ConfigTool.ClaudeCode], { aiSkills: false });
174+
175+
expect(schematicRunner.tasks).toEqual([]);
176+
});
177+
178+
it('should not install Angular skills when no AI tool is selected', async () => {
179+
await runAiConfigSchematic([ConfigTool.None], { aiSkills: true });
180+
181+
expect(schematicRunner.tasks).toEqual([]);
182+
});
183+
112184
it('should update JSON MCP server config, if the file exists', async () => {
113185
// eslint-disable-next-line @typescript-eslint/no-explicit-any
114186
const jsonConfig: Record<string, any> = {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { Rule } from '@angular-devkit/schematics';
10+
import { RunSchematicTask } from '@angular-devkit/schematics/tasks';
11+
import { execFileSync } from 'node:child_process';
12+
import { resolve } from 'node:path';
13+
import { latestVersions } from '../utility/latest-versions';
14+
import { Tool } from './schema';
15+
16+
type SkillsTool = Exclude<Tool, Tool.None>;
17+
18+
interface AngularSkillsInstallOptions {
19+
tools: readonly string[];
20+
workingDirectory?: string;
21+
}
22+
23+
const SKILLS_AGENT: { [key in SkillsTool]: string } = {
24+
['claude-code']: 'claude-code',
25+
cursor: 'cursor',
26+
['gemini-cli']: 'gemini-cli',
27+
['open-ai-codex']: 'codex',
28+
vscode: 'github-copilot',
29+
};
30+
31+
const ANGULAR_SKILLS_REPOSITORY = 'https://github.com/angular/skills';
32+
33+
export function getAngularSkillsRepository(angularVersion = latestVersions.Angular): string {
34+
const version = angularVersion.match(/(\d+)\.(\d+)/);
35+
if (!version) {
36+
throw new Error(`Unable to determine the Angular release line from '${angularVersion}'.`);
37+
}
38+
39+
return `${ANGULAR_SKILLS_REPOSITORY}/tree/${version[1]}.${version[2]}.x`;
40+
}
41+
42+
export function getAngularSkillsInstallArguments(
43+
tools: readonly string[],
44+
angularVersion = latestVersions.Angular,
45+
): string[] {
46+
const agents = tools.flatMap((tool) => {
47+
const agent = SKILLS_AGENT[tool as SkillsTool];
48+
if (!agent) {
49+
throw new Error(`Unsupported AI tool '${tool}' for Angular Agent Skills.`);
50+
}
51+
52+
return ['--agent', agent];
53+
});
54+
55+
return [
56+
'--yes',
57+
'skills',
58+
'add',
59+
getAngularSkillsRepository(angularVersion),
60+
'--skill',
61+
'angular-developer',
62+
'--skill',
63+
'angular-new-app',
64+
...agents,
65+
'--copy',
66+
'--yes',
67+
];
68+
}
69+
70+
export function createAngularSkillsTask(
71+
tools: readonly string[],
72+
workingDirectory?: string,
73+
): RunSchematicTask<AngularSkillsInstallOptions> {
74+
return new RunSchematicTask(
75+
'ai-config-install-skills',
76+
workingDirectory === undefined ? { tools } : { tools, workingDirectory },
77+
);
78+
}
79+
80+
export default function (options: AngularSkillsInstallOptions): Rule {
81+
return (_tree, context) => {
82+
const args = getAngularSkillsInstallArguments(options.tools);
83+
84+
try {
85+
execFileSync('npx', args, {
86+
cwd: resolve(options.workingDirectory ?? '.'),
87+
env: { ...process.env, DISABLE_TELEMETRY: '1' },
88+
stdio: 'inherit',
89+
shell: process.platform === 'win32',
90+
});
91+
} catch {
92+
context.logger.warn(
93+
'Angular Agent Skills could not be installed.\n' +
94+
`When you are online, install them manually with:\n npx ${args.join(' ')}`,
95+
);
96+
}
97+
};
98+
}

packages/schematics/angular/ai-config/schema.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"title": "Angular AI Config File Options Schema",
55
"type": "object",
66
"additionalProperties": false,
7-
"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.",
7+
"description": "Generates AI configuration files for Angular projects and can install the official Angular Agent Skills.",
88
"properties": {
99
"tool": {
1010
"type": "array",
@@ -45,6 +45,11 @@
4545
"type": "string",
4646
"enum": ["none", "claude-code", "cursor", "gemini-cli", "open-ai-codex", "vscode"]
4747
}
48+
},
49+
"aiSkills": {
50+
"type": "boolean",
51+
"description": "Installs the official Angular Agent Skills locally for the selected AI tools.",
52+
"x-prompt": "Install the official Angular Agent Skills for the selected AI tools? This downloads skills from github.com/angular/skills."
4853
}
4954
}
5055
}

packages/schematics/angular/collection.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@
131131
"schema": "./ai-config/schema.json",
132132
"description": "Generates an AI tool configuration file."
133133
},
134+
"ai-config-install-skills": {
135+
"factory": "./ai-config/install-skills",
136+
"hidden": true,
137+
"private": true,
138+
"description": "[INTERNAL] Installs the official Angular Agent Skills."
139+
},
134140
"tailwind": {
135141
"factory": "./tailwind",
136142
"schema": "./tailwind/schema.json",

packages/schematics/angular/ng-new/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
NodePackageInstallTask,
2323
RepositoryInitializerTask,
2424
} from '@angular-devkit/schematics/tasks';
25+
import { createAngularSkillsTask } from '../ai-config/install-skills';
2526
import { Schema as ApplicationOptions } from '../application/schema';
2627
import { JSONFile } from '../utility/json-file';
2728
import { Schema as WorkspaceOptions } from '../workspace/schema';
@@ -81,6 +82,7 @@ export default function (options: NgNewOptions): Rule {
8182
options.createApplication ? schematic('application', applicationOptions) : noop,
8283
schematic('ai-config', {
8384
tool: options.aiConfig?.length ? options.aiConfig : undefined,
85+
aiSkills: false,
8486
}),
8587
move(options.directory),
8688
]),
@@ -95,13 +97,22 @@ export default function (options: NgNewOptions): Rule {
9597
}),
9698
);
9799
}
100+
101+
let skillsTask;
102+
const aiTools = (options.aiConfig ?? []).filter((tool) => tool !== 'none');
103+
if (options.aiSkills && aiTools.length > 0) {
104+
skillsTask = context.addTask(
105+
createAngularSkillsTask(aiTools, options.directory),
106+
packageTask ? [packageTask] : [],
107+
);
108+
}
98109
if (!options.skipGit) {
99110
const commit =
100111
typeof options.commit == 'object' ? options.commit : options.commit ? {} : false;
101112

102113
context.addTask(
103114
new RepositoryInitializerTask(options.directory, commit),
104-
packageTask ? [packageTask] : [],
115+
skillsTask ? [skillsTask] : packageTask ? [packageTask] : [],
105116
);
106117
}
107118
},

packages/schematics/angular/ng-new/index_spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,31 @@ describe('Ng New Schematic', () => {
115115
expect(files).toContain('/bar/.gemini/settings.json');
116116
});
117117

118+
it('should install Angular skills before initializing the repository', async () => {
119+
await schematicRunner.runSchematic('ng-new', {
120+
...defaultOptions,
121+
aiConfig: ['gemini-cli', 'claude-code'],
122+
aiSkills: true,
123+
});
124+
125+
const skillsTask = schematicRunner.tasks.find(
126+
(task) => (task.options as { name?: string })?.name === 'ai-config-install-skills',
127+
);
128+
expect(skillsTask?.options).toEqual(
129+
jasmine.objectContaining({
130+
options: {
131+
workingDirectory: 'bar',
132+
tools: ['gemini-cli', 'claude-code'],
133+
},
134+
}),
135+
);
136+
expect(
137+
schematicRunner.tasks.findIndex(
138+
(task) => (task.options as { name?: string })?.name === 'ai-config-install-skills',
139+
),
140+
).toBeLessThan(schematicRunner.tasks.findIndex((task) => task.name === 'repo-init'));
141+
});
142+
118143
it('should create a tailwind project when style is tailwind', async () => {
119144
// eslint-disable-next-line @typescript-eslint/no-explicit-any
120145
const options = { ...defaultOptions, style: 'tailwind' as any };

packages/schematics/angular/ng-new/schema.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,47 @@
152152
"aiConfig": {
153153
"type": "array",
154154
"uniqueItems": true,
155+
"x-prompt": {
156+
"message": "Which AI tools should Angular integrate with? https://angular.dev/ai/develop-with-ai",
157+
"type": "list",
158+
"items": [
159+
{
160+
"value": "none",
161+
"label": "None"
162+
},
163+
{
164+
"value": "claude-code",
165+
"label": "Claude Code [ `AGENTS.md` + Angular MCP server config ]"
166+
},
167+
{
168+
"value": "cursor",
169+
"label": "Cursor [ `AGENTS.md` + Angular MCP server config ]"
170+
},
171+
{
172+
"value": "gemini-cli",
173+
"label": "Gemini CLI [ `GEMINI.md` + Angular MCP server config ]"
174+
},
175+
{
176+
"value": "open-ai-codex",
177+
"label": "Open AI Codex [ `AGENTS.md` + Angular MCP server config ]"
178+
},
179+
{
180+
"value": "vscode",
181+
"label": "VSCode [ `AGENTS.md` + Angular MCP server config ]"
182+
}
183+
]
184+
},
155185
"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.",
156186
"items": {
157187
"type": "string",
158188
"enum": ["none", "claude-code", "cursor", "gemini-cli", "open-ai-codex", "vscode"]
159189
}
160190
},
191+
"aiSkills": {
192+
"type": "boolean",
193+
"description": "Installs the official Angular Agent Skills locally for the selected AI tools. When omitted with non-interactive defaults, skills are not installed.",
194+
"x-prompt": "Install the official Angular Agent Skills for the selected AI tools? This downloads skills from github.com/angular/skills."
195+
},
161196
"fileNameStyleGuide": {
162197
"type": "string",
163198
"enum": ["2016", "2025"],

0 commit comments

Comments
 (0)