Skip to content

feat(@angular-devkit/schematics): add schematics to generate ai context files. #30763

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

JeanMeche
Copy link
Member

@JeanMeche JeanMeche commented Jul 22, 2025

  • ng generate ai-config to prompt support tools.
  • ng generate ai-config --tool=gemini to specify the tool. (multiple args accepted)
  • ng new --ai-config gemini to create a new project with AI configuration. (multiple args accepted)

Supported ai tools: gemini, claude, copilot, windsurf, cursor.

@JeanMeche JeanMeche force-pushed the generate-ai-file branch 2 times, most recently from 70ae7ec to 288dcfd Compare July 22, 2025 16:53
throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`);
}

// Keep this file in sync with the one presented here https://angular.dev/ai/develop-with-ai
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should do something automatted or document it on the FW side, since when there is a change there, a PR needs to be opened here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Let's handle that as a follow-up when we have a better picture what we need to sync (we might have multiple schematics).

@JeanMeche JeanMeche marked this pull request as ready for review July 29, 2025 13:36
@dgp1130 dgp1130 added action: review The PR is still awaiting reviews from at least one requested reviewer target: minor This PR is targeted for the next minor release labels Jul 31, 2025
@dgp1130 dgp1130 requested a review from clydin July 31, 2025 04:16
@JeanMeche JeanMeche force-pushed the generate-ai-file branch 2 times, most recently from 3dc7023 to 5ae2b77 Compare August 1, 2025 12:11
@JeanMeche JeanMeche removed the request for review from AndrewKushnir August 1, 2025 12:12
"x-prompt": "Which type of configuration file would you like to create?",
"$default": {
"$source": "argv",
"index": 0
}
},
"tool": {
Copy link
Member

Choose a reason for hiding this comment

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

Should this be a multi-select prompt?
We could still default to having them all selected. Would need to change the type to a string array though. But it's probably unlikely that everyone would want all available tools when not specifying the option. Especially since the option may not be known to even exist.

Copy link
Member Author

Choose a reason for hiding this comment

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

As mentionned by @alan-agius4 here, the prompt would show for the others options. Or do you have an alternative in mind ?

@JeanMeche JeanMeche force-pushed the generate-ai-file branch 2 times, most recently from 048a5d7 to 19858e9 Compare August 4, 2025 14:16
@JeanMeche JeanMeche requested a review from clydin August 4, 2025 15:06
const configOptions: ConfigOptions = {
project: options.name,
type: ConfigType.Ai,
tool: options.aiConfig as unknown as AiTool,
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure why I couldn't get the enums to match. It works fine for the packageManager above for example. (Yes I also tried with removing "none" from the enum here in ng-new).

@JeanMeche JeanMeche force-pushed the generate-ai-file branch 2 times, most recently from bbe0c40 to 614ed54 Compare August 7, 2025 21:12
@lacolaco
Copy link
Contributor

lacolaco commented Aug 7, 2025

How does it work if there is already a rule file in the project? Can it merge them into the single file?

@JeanMeche
Copy link
Member Author

@lacolaco Currently it would fail with a "merge" conflict. We plan to address the "update" of those files in a follow-up PR.

@JeanMeche JeanMeche force-pushed the generate-ai-file branch 2 times, most recently from 64e512a to 49a02e0 Compare August 11, 2025 17:16
@@ -60,11 +61,17 @@ export default function (options: NgNewOptions): Rule {
zoneless: options.zoneless,
};

const configOptions: ConfigOptions = {
project: options.name,
tool: [options.aiConfig as unknown as AiTool],
Copy link
Member Author

Choose a reason for hiding this comment

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

Not exactly sure why this type assertion was necessary.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is due to the “none” value.

Copy link
Member Author

Choose a reason for hiding this comment

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

Even without it, the types mismatch. 🤔

…xt files.

* `ng generate ai-config` to prompt support tools.
* `ng generate config ai --tool=gemini` to specify the tool.
* `ng new --aiConfig=gemini` to create a new project with AI configuration.

Supported ai tools: gemini, claude, copilot, windsurf, cursor.
"items": {
"type": "string",
"enum": ["gemini", "copilot", "claude", "cursor", "windsurf"]
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Question: Do we need x-prompt here too to show up as a prompt for just ng new?

Copy link
Collaborator

Choose a reason for hiding this comment

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

If we want to show the prompt here, we'd need to remove the default and run the schematic directly and not conditionally.

Comment on lines +25 to +49
const geminiFile: ContextFileInfo = { rulesName: 'GEMINI.md', directory: '.gemini' };
const copilotFile: ContextFileInfo = {
rulesName: 'copilot-instructions.md',
directory: '.github',
};
const claudeFile: ContextFileInfo = { rulesName: 'CLAUDE.md', directory: '.claude' };
const windsurfFile: ContextFileInfo = {
rulesName: 'guidelines.md',
directory: path.join('.windsurf', 'rules'),
};

// Cursor file is a bit different, it has a front matter section.
const cursorFile: ContextFileInfo = {
rulesName: 'cursor.mdc',
directory: path.join('.cursor', 'rules'),
frontmatter: `---\ncontext: true\npriority: high\nscope: project\n---`,
};

const AI_TOOLS = {
'gemini': geminiFile,
'claude': claudeFile,
'copilot': copilotFile,
'cursor': cursorFile,
'windsurf': windsurfFile,
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

NIT, but I think it makes the code more readable.

Suggested change
const geminiFile: ContextFileInfo = { rulesName: 'GEMINI.md', directory: '.gemini' };
const copilotFile: ContextFileInfo = {
rulesName: 'copilot-instructions.md',
directory: '.github',
};
const claudeFile: ContextFileInfo = { rulesName: 'CLAUDE.md', directory: '.claude' };
const windsurfFile: ContextFileInfo = {
rulesName: 'guidelines.md',
directory: path.join('.windsurf', 'rules'),
};
// Cursor file is a bit different, it has a front matter section.
const cursorFile: ContextFileInfo = {
rulesName: 'cursor.mdc',
directory: path.join('.cursor', 'rules'),
frontmatter: `---\ncontext: true\npriority: high\nscope: project\n---`,
};
const AI_TOOLS = {
'gemini': geminiFile,
'claude': claudeFile,
'copilot': copilotFile,
'cursor': cursorFile,
'windsurf': windsurfFile,
};
const AI_TOOLS = {
gemini: {
rulesName: 'GEMINI.md',
directory: '.gemini'
},
claude: {
rulesName: 'CLAUDE.md',
directory: '.claude'
},
copilot: {
rulesName: 'copilot-instructions.md',
directory: '.github',
},
windsurf: {
rulesName: 'guidelines.md',
directory: path.join('.windsurf', 'rules'),
},
// Cursor file has a front matter section.
cursor: {
rulesName: 'cursor.mdc',
directory: path.join('.cursor', 'rules'),
frontmatter: `---\ncontext: true\npriority: high\nscope: project\n---`,
},
};

frontmatter?: string;
}

function addAiContextFile(options: ConfigOptions): Rule {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
function addAiContextFile(options: ConfigOptions): Rule {
export default function(options: ConfigOptions): Rule {

Comment on lines +51 to +54
export default function (options: ConfigOptions): Rule {
return addAiContextFile(options);
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export default function (options: ConfigOptions): Rule {
return addAiContextFile(options);
}

Comment on lines +67 to +73
return async (host) => {
const workspace = await readWorkspace(host);
const project = workspace.projects.get(options.project);
if (!project) {
throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`);
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

This workspace logic seems unused.

Suggested change
return async (host) => {
const workspace = await readWorkspace(host);
const project = workspace.projects.get(options.project);
if (!project) {
throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`);
}
return (host) => {

Comment on lines +74 to +82
const rules = files.map(({ rulesName, directory, frontmatter }) =>
mergeWith(
apply(url('./files'), [
// Keep only the single source template
filter((p) => p.endsWith('__rulesName__.template')),
applyTemplates({
...strings,
rulesName,
frontmatter: frontmatter ?? '',
Copy link
Collaborator

Choose a reason for hiding this comment

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

NIT:

Suggested change
const rules = files.map(({ rulesName, directory, frontmatter }) =>
mergeWith(
apply(url('./files'), [
// Keep only the single source template
filter((p) => p.endsWith('__rulesName__.template')),
applyTemplates({
...strings,
rulesName,
frontmatter: frontmatter ?? '',
const rules = files.map(({ rulesName, directory, frontmatter = '' }) =>
mergeWith(
apply(url('./files'), [
// Keep only the single source template
filter((p) => p.endsWith('__rulesName__.template')),
applyTemplates({
...strings,
rulesName,
frontmatter,

Comment on lines +93 to +95
expect(tree.readContent('.gemini/GEMINI.md').length).toBeGreaterThan(0);
expect(tree.readContent('.github/copilot-instructions.md').length).toBeGreaterThan(0);
expect(tree.readContent('.cursor/rules/cursor.mdc').length).toBeGreaterThan(0);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
expect(tree.readContent('.gemini/GEMINI.md').length).toBeGreaterThan(0);
expect(tree.readContent('.github/copilot-instructions.md').length).toBeGreaterThan(0);
expect(tree.readContent('.cursor/rules/cursor.mdc').length).toBeGreaterThan(0);
expect(tree.exists('.gemini/GEMINI.md')).toBeTrue();
expect(tree.exists('.github/copilot-instructions.md')).toBeTrue();
expect(tree.exists('.cursor/rules/cursor.mdc')).toBeTrue();

);
});

it('should create a GEMINI.MD file', async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

In most of the cases here, we can replace tree.readContent with tree.exists as we are not checking for text that is generated.

"additionalProperties": false,
"description": "Generates configuration files for your project. These files control various aspects of your project's build process, testing, and browser compatibility. This schematic helps you create or update essential configuration files with ease.",
"properties": {
"project": {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This option seems to be redundant, as the config is not generated for a project but for a workspace.

"title": "Angular Config File Options Schema",
"type": "object",
"additionalProperties": false,
"description": "Generates configuration files for your project. These files control various aspects of your project's build process, testing, and browser compatibility. This schematic helps you create or update essential configuration files with ease.",
Copy link
Collaborator

Choose a reason for hiding this comment

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

The description seems wrong.

const configOptions: ConfigOptions | undefined = options.aiConfig?.length
? {
project: options.name,
tool: options.aiConfig as unknown as AiTool[],
Copy link
Collaborator

Choose a reason for hiding this comment

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

casting should not longer be needed.

Suggested change
tool: options.aiConfig as unknown as AiTool[],
tool: options.aiConfig,

alan-agius4

This comment was marked as duplicate.

@alan-agius4 alan-agius4 added the action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews label Aug 12, 2025
Copy link
Collaborator

@alan-agius4 alan-agius4 left a comment

Choose a reason for hiding this comment

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

See above, and commit message has the wrong scope as it should be "@schematics/angular" and description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews action: review The PR is still awaiting reviews from at least one requested reviewer area: @angular-devkit/schematics detected: feature PR contains a feature commit target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants