mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 22:55:13 +00:00
feat(core): support shipping built-in skills with the CLI (#16300)
This commit is contained in:
@@ -11,6 +11,15 @@ import * as path from 'node:path';
|
|||||||
import { SkillManager } from './skillManager.js';
|
import { SkillManager } from './skillManager.js';
|
||||||
import { Storage } from '../config/storage.js';
|
import { Storage } from '../config/storage.js';
|
||||||
import { type GeminiCLIExtension } from '../config/config.js';
|
import { type GeminiCLIExtension } from '../config/config.js';
|
||||||
|
import { loadSkillsFromDir, type SkillDefinition } from './skillLoader.js';
|
||||||
|
|
||||||
|
vi.mock('./skillLoader.js', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import('./skillLoader.js')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
loadSkillsFromDir: vi.fn(actual.loadSkillsFromDir),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('SkillManager', () => {
|
describe('SkillManager', () => {
|
||||||
let testRootDir: string;
|
let testRootDir: string;
|
||||||
@@ -71,6 +80,8 @@ description: project-desc
|
|||||||
vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(projectDir);
|
vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(projectDir);
|
||||||
|
|
||||||
const service = new SkillManager();
|
const service = new SkillManager();
|
||||||
|
// @ts-expect-error accessing private method for testing
|
||||||
|
vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined);
|
||||||
await service.discoverSkills(storage, [mockExtension]);
|
await service.discoverSkills(storage, [mockExtension]);
|
||||||
|
|
||||||
const skills = service.getSkills();
|
const skills = service.getSkills();
|
||||||
@@ -126,6 +137,8 @@ description: project-desc
|
|||||||
vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(projectDir);
|
vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(projectDir);
|
||||||
|
|
||||||
const service = new SkillManager();
|
const service = new SkillManager();
|
||||||
|
// @ts-expect-error accessing private method for testing
|
||||||
|
vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined);
|
||||||
await service.discoverSkills(storage, [mockExtension]);
|
await service.discoverSkills(storage, [mockExtension]);
|
||||||
|
|
||||||
const skills = service.getSkills();
|
const skills = service.getSkills();
|
||||||
@@ -138,6 +151,34 @@ description: project-desc
|
|||||||
expect(service.getSkills()[0].description).toBe('user-desc');
|
expect(service.getSkills()[0].description).toBe('user-desc');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should discover built-in skills', async () => {
|
||||||
|
const service = new SkillManager();
|
||||||
|
const mockBuiltinSkill: SkillDefinition = {
|
||||||
|
name: 'builtin-skill',
|
||||||
|
description: 'builtin-desc',
|
||||||
|
location: 'builtin-loc',
|
||||||
|
body: 'builtin-body',
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(loadSkillsFromDir).mockImplementation(async (dir) => {
|
||||||
|
if (dir.endsWith('builtin')) {
|
||||||
|
return [{ ...mockBuiltinSkill }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const storage = new Storage('/dummy');
|
||||||
|
vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue('/non-existent');
|
||||||
|
vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue('/non-existent');
|
||||||
|
|
||||||
|
await service.discoverSkills(storage);
|
||||||
|
|
||||||
|
const skills = service.getSkills();
|
||||||
|
expect(skills).toHaveLength(1);
|
||||||
|
expect(skills[0].name).toBe('builtin-skill');
|
||||||
|
expect(skills[0].isBuiltin).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should filter disabled skills in getSkills but not in getAllSkills', async () => {
|
it('should filter disabled skills in getSkills but not in getAllSkills', async () => {
|
||||||
const skillDir = path.join(testRootDir, 'skill1');
|
const skillDir = path.join(testRootDir, 'skill1');
|
||||||
await fs.mkdir(skillDir, { recursive: true });
|
await fs.mkdir(skillDir, { recursive: true });
|
||||||
@@ -156,6 +197,8 @@ description: desc1
|
|||||||
vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue('/non-existent');
|
vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue('/non-existent');
|
||||||
|
|
||||||
const service = new SkillManager();
|
const service = new SkillManager();
|
||||||
|
// @ts-expect-error accessing private method for testing
|
||||||
|
vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined);
|
||||||
await service.discoverSkills(storage);
|
await service.discoverSkills(storage);
|
||||||
service.setDisabledSkills(['skill1']);
|
service.setDisabledSkills(['skill1']);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
import { Storage } from '../config/storage.js';
|
import { Storage } from '../config/storage.js';
|
||||||
import { type SkillDefinition, loadSkillsFromDir } from './skillLoader.js';
|
import { type SkillDefinition, loadSkillsFromDir } from './skillLoader.js';
|
||||||
import type { GeminiCLIExtension } from '../config/config.js';
|
import type { GeminiCLIExtension } from '../config/config.js';
|
||||||
@@ -56,9 +58,16 @@ export class SkillManager {
|
|||||||
* Discovers built-in skills.
|
* Discovers built-in skills.
|
||||||
*/
|
*/
|
||||||
private async discoverBuiltinSkills(): Promise<void> {
|
private async discoverBuiltinSkills(): Promise<void> {
|
||||||
// Built-in skills can be added here.
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
// For now, this is a placeholder for where built-in skills will be loaded from.
|
const builtinDir = path.join(__dirname, 'builtin');
|
||||||
// They could be loaded from a specific directory within the package.
|
|
||||||
|
const builtinSkills = await loadSkillsFromDir(builtinDir);
|
||||||
|
|
||||||
|
for (const skill of builtinSkills) {
|
||||||
|
skill.isBuiltin = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addSkillsWithPrecedence(builtinSkills);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addSkillsWithPrecedence(newSkills: SkillDefinition[]): void {
|
private addSkillsWithPrecedence(newSkills: SkillDefinition[]): void {
|
||||||
|
|||||||
@@ -62,4 +62,15 @@ if (existsSync(docsSrc)) {
|
|||||||
console.log('Copied docs to bundle/docs/');
|
console.log('Copied docs to bundle/docs/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Copy Built-in Skills (packages/core/src/skills/builtin)
|
||||||
|
const builtinSkillsSrc = join(root, 'packages/core/src/skills/builtin');
|
||||||
|
const builtinSkillsDest = join(bundleDir, 'builtin');
|
||||||
|
if (existsSync(builtinSkillsSrc)) {
|
||||||
|
cpSync(builtinSkillsSrc, builtinSkillsDest, {
|
||||||
|
recursive: true,
|
||||||
|
dereference: true,
|
||||||
|
});
|
||||||
|
console.log('Copied built-in skills to bundle/builtin/');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Assets copied to bundle/');
|
console.log('Assets copied to bundle/');
|
||||||
|
|||||||
@@ -74,4 +74,13 @@ if (packageName === 'cli') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy built-in skills for the core package.
|
||||||
|
if (packageName === 'core') {
|
||||||
|
const builtinSkillsSource = path.join(sourceDir, 'skills', 'builtin');
|
||||||
|
const builtinSkillsTarget = path.join(targetDir, 'skills', 'builtin');
|
||||||
|
if (fs.existsSync(builtinSkillsSource)) {
|
||||||
|
fs.cpSync(builtinSkillsSource, builtinSkillsTarget, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Successfully copied files.');
|
console.log('Successfully copied files.');
|
||||||
|
|||||||
Reference in New Issue
Block a user