Skip to content

Commit 63249f9

Browse files
committed
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!
1 parent 13b6223 commit 63249f9

File tree

7 files changed

+116
-0
lines changed

7 files changed

+116
-0
lines changed

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ dist/
1616
/tests/legacy-cli/e2e/assets/
1717
/tools/test/*.json
1818
pnpm-lock.yaml
19+
20+
# This is a symbolic link.
21+
packages/angular/cli/src/commands/ai/setup-gemini-cli/extension/GEMINI.md
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 { join } from 'node:path';
10+
import { Argv } from 'yargs';
11+
import {
12+
CommandModule,
13+
CommandModuleImplementation,
14+
CommandScope,
15+
Options,
16+
} from '../../command-builder/command-module';
17+
import {
18+
addCommandModuleToYargs,
19+
demandCommandFailureMessage,
20+
} from '../../command-builder/utilities/command';
21+
import SetupGeminiCliModule from './setup-gemini-cli/cli';
22+
23+
export default class AiCommandModule extends CommandModule implements CommandModuleImplementation {
24+
command = 'ai';
25+
describe = 'Commands for artificial intelligence.';
26+
longDescriptionPath = undefined;
27+
override scope = CommandScope.Both;
28+
29+
builder(localYargs: Argv): Argv {
30+
const subcommands = [SetupGeminiCliModule].sort();
31+
32+
for (const module of subcommands) {
33+
addCommandModuleToYargs(module, this.context);
34+
}
35+
36+
return localYargs.demandCommand(1, demandCommandFailureMessage).strict();
37+
}
38+
39+
run(_options: Options<{}>): void {}
40+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 { cp, mkdir } from 'node:fs/promises';
10+
import { homedir } from 'node:os';
11+
import { join } from 'node:path';
12+
import { Argv } from 'yargs';
13+
import {
14+
CommandModule,
15+
CommandModuleImplementation,
16+
CommandScope,
17+
Options,
18+
} from '../../../command-builder/command-module';
19+
20+
export default class SetupGeminiCliModule
21+
extends CommandModule
22+
implements CommandModuleImplementation
23+
{
24+
command = 'setup-gemini-cli';
25+
describe = 'Sets up Gemini CLI with the official Angular extension.';
26+
longDescriptionPath?: string | undefined;
27+
28+
override scope = CommandScope.Both;
29+
30+
builder(localYargs: Argv): Argv {
31+
return localYargs.strict();
32+
}
33+
34+
async run(_options: Options<{}>): Promise<void> {
35+
const extensionDir = join(__dirname, './extension');
36+
const extensionUserDir = join(homedir(), '.gemini', 'extensions', 'angular');
37+
38+
await mkdir(extensionUserDir, { recursive: true });
39+
await cp(extensionDir, extensionUserDir, { recursive: true, dereference: true });
40+
41+
this.context.logger.info(`✅ Installed the Angular Gemini CLI extension.`);
42+
}
43+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../mcp/instructions/best-practices.md
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "Angular",
3+
"version": "0.0.0-PLACEHOLDER",
4+
"mcpServers": {},
5+
"contextFileName": "./GEMINI.md"
6+
}

packages/angular/cli/src/commands/command-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CommandModuleConstructor } from '../command-builder/utilities/command';
1010

1111
export type CommandNames =
1212
| 'add'
13+
| 'ai'
1314
| 'analytics'
1415
| 'build'
1516
| 'cache'
@@ -41,6 +42,9 @@ export const RootCommands: Record<
4142
'add': {
4243
factory: () => import('./add/cli'),
4344
},
45+
'ai': {
46+
factory: () => import('./ai/cli'),
47+
},
4448
'analytics': {
4549
factory: () => import('./analytics/cli'),
4650
},
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { join } from 'node:path';
2+
import { expectFileNotToExist, expectFileToExist, expectFileToMatch } from '../../../utils/fs';
3+
import { ng } from '../../../utils/process';
4+
import assert from 'node:assert';
5+
6+
export default async function () {
7+
assert(process.env.HOME, 'Expected HOME directory to be set.');
8+
9+
const extensionDir = join(process.env.HOME, '.gemini', 'extensions', 'angular');
10+
const geminiBestPracticesFile = join(extensionDir, 'GEMINI.md');
11+
12+
await expectFileNotToExist(extensionDir);
13+
await expectFileNotToExist(geminiBestPracticesFile);
14+
await ng('ai', 'setup-gemini-cli');
15+
16+
await expectFileToExist(extensionDir);
17+
await expectFileToExist(geminiBestPracticesFile);
18+
await expectFileToMatch(geminiBestPracticesFile, 'Angular Best Practices');
19+
}

0 commit comments

Comments
 (0)