From 63249f978ffd134910945adebda3a0c3a1672c0f Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Tue, 29 Jul 2025 11:18:32 +0000 Subject: [PATCH] feat(@angular/cli): introduce `setup-gemini-cli` command with a Gemini CLI extension Introduces `ng ai setup-gemini-cli` that will automatically install the "Angular Gemini CLI extension", wiring up Angular best practices and rules that improve code generation. In the future we need to look further into: * Documentation * How we version the extension. I.e. could `ng update` help here? * The extension could also wire up the MCP server from the CLI! --- .prettierignore | 3 ++ packages/angular/cli/src/commands/ai/cli.ts | 40 +++++++++++++++++ .../src/commands/ai/setup-gemini-cli/cli.ts | 43 +++++++++++++++++++ .../ai/setup-gemini-cli/extension/GEMINI.md | 1 + .../extension/gemini-extension.json | 6 +++ .../cli/src/commands/command-config.ts | 4 ++ .../e2e/tests/commands/ai/setup-gemini-cli.ts | 19 ++++++++ 7 files changed, 116 insertions(+) create mode 100644 packages/angular/cli/src/commands/ai/cli.ts create mode 100644 packages/angular/cli/src/commands/ai/setup-gemini-cli/cli.ts create mode 120000 packages/angular/cli/src/commands/ai/setup-gemini-cli/extension/GEMINI.md create mode 100644 packages/angular/cli/src/commands/ai/setup-gemini-cli/extension/gemini-extension.json create mode 100644 tests/legacy-cli/e2e/tests/commands/ai/setup-gemini-cli.ts diff --git a/.prettierignore b/.prettierignore index b0b71acaf241..f5eb42428a59 100644 --- a/.prettierignore +++ b/.prettierignore @@ -16,3 +16,6 @@ dist/ /tests/legacy-cli/e2e/assets/ /tools/test/*.json pnpm-lock.yaml + +# This is a symbolic link. +packages/angular/cli/src/commands/ai/setup-gemini-cli/extension/GEMINI.md diff --git a/packages/angular/cli/src/commands/ai/cli.ts b/packages/angular/cli/src/commands/ai/cli.ts new file mode 100644 index 000000000000..1bd6f7c25d48 --- /dev/null +++ b/packages/angular/cli/src/commands/ai/cli.ts @@ -0,0 +1,40 @@ +/** + * @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 { join } from 'node:path'; +import { Argv } from 'yargs'; +import { + CommandModule, + CommandModuleImplementation, + CommandScope, + Options, +} from '../../command-builder/command-module'; +import { + addCommandModuleToYargs, + demandCommandFailureMessage, +} from '../../command-builder/utilities/command'; +import SetupGeminiCliModule from './setup-gemini-cli/cli'; + +export default class AiCommandModule extends CommandModule implements CommandModuleImplementation { + command = 'ai'; + describe = 'Commands for artificial intelligence.'; + longDescriptionPath = undefined; + override scope = CommandScope.Both; + + builder(localYargs: Argv): Argv { + const subcommands = [SetupGeminiCliModule].sort(); + + for (const module of subcommands) { + addCommandModuleToYargs(module, this.context); + } + + return localYargs.demandCommand(1, demandCommandFailureMessage).strict(); + } + + run(_options: Options<{}>): void {} +} diff --git a/packages/angular/cli/src/commands/ai/setup-gemini-cli/cli.ts b/packages/angular/cli/src/commands/ai/setup-gemini-cli/cli.ts new file mode 100644 index 000000000000..9782722675fc --- /dev/null +++ b/packages/angular/cli/src/commands/ai/setup-gemini-cli/cli.ts @@ -0,0 +1,43 @@ +/** + * @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 { cp, mkdir } from 'node:fs/promises'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import { Argv } from 'yargs'; +import { + CommandModule, + CommandModuleImplementation, + CommandScope, + Options, +} from '../../../command-builder/command-module'; + +export default class SetupGeminiCliModule + extends CommandModule + implements CommandModuleImplementation +{ + command = 'setup-gemini-cli'; + describe = 'Sets up Gemini CLI with the official Angular extension.'; + longDescriptionPath?: string | undefined; + + override scope = CommandScope.Both; + + builder(localYargs: Argv): Argv { + return localYargs.strict(); + } + + async run(_options: Options<{}>): Promise { + const extensionDir = join(__dirname, './extension'); + const extensionUserDir = join(homedir(), '.gemini', 'extensions', 'angular'); + + await mkdir(extensionUserDir, { recursive: true }); + await cp(extensionDir, extensionUserDir, { recursive: true, dereference: true }); + + this.context.logger.info(`✅ Installed the Angular Gemini CLI extension.`); + } +} diff --git a/packages/angular/cli/src/commands/ai/setup-gemini-cli/extension/GEMINI.md b/packages/angular/cli/src/commands/ai/setup-gemini-cli/extension/GEMINI.md new file mode 120000 index 000000000000..7b305125f113 --- /dev/null +++ b/packages/angular/cli/src/commands/ai/setup-gemini-cli/extension/GEMINI.md @@ -0,0 +1 @@ +../../../mcp/instructions/best-practices.md \ No newline at end of file diff --git a/packages/angular/cli/src/commands/ai/setup-gemini-cli/extension/gemini-extension.json b/packages/angular/cli/src/commands/ai/setup-gemini-cli/extension/gemini-extension.json new file mode 100644 index 000000000000..7317635c37a4 --- /dev/null +++ b/packages/angular/cli/src/commands/ai/setup-gemini-cli/extension/gemini-extension.json @@ -0,0 +1,6 @@ +{ + "name": "Angular", + "version": "0.0.0-PLACEHOLDER", + "mcpServers": {}, + "contextFileName": "./GEMINI.md" +} diff --git a/packages/angular/cli/src/commands/command-config.ts b/packages/angular/cli/src/commands/command-config.ts index a74d81f5e911..d299a2f4192c 100644 --- a/packages/angular/cli/src/commands/command-config.ts +++ b/packages/angular/cli/src/commands/command-config.ts @@ -10,6 +10,7 @@ import { CommandModuleConstructor } from '../command-builder/utilities/command'; export type CommandNames = | 'add' + | 'ai' | 'analytics' | 'build' | 'cache' @@ -41,6 +42,9 @@ export const RootCommands: Record< 'add': { factory: () => import('./add/cli'), }, + 'ai': { + factory: () => import('./ai/cli'), + }, 'analytics': { factory: () => import('./analytics/cli'), }, diff --git a/tests/legacy-cli/e2e/tests/commands/ai/setup-gemini-cli.ts b/tests/legacy-cli/e2e/tests/commands/ai/setup-gemini-cli.ts new file mode 100644 index 000000000000..b5b505738fea --- /dev/null +++ b/tests/legacy-cli/e2e/tests/commands/ai/setup-gemini-cli.ts @@ -0,0 +1,19 @@ +import { join } from 'node:path'; +import { expectFileNotToExist, expectFileToExist, expectFileToMatch } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; +import assert from 'node:assert'; + +export default async function () { + assert(process.env.HOME, 'Expected HOME directory to be set.'); + + const extensionDir = join(process.env.HOME, '.gemini', 'extensions', 'angular'); + const geminiBestPracticesFile = join(extensionDir, 'GEMINI.md'); + + await expectFileNotToExist(extensionDir); + await expectFileNotToExist(geminiBestPracticesFile); + await ng('ai', 'setup-gemini-cli'); + + await expectFileToExist(extensionDir); + await expectFileToExist(geminiBestPracticesFile); + await expectFileToMatch(geminiBestPracticesFile, 'Angular Best Practices'); +}