feat(core): support shipping built-in skills with the CLI (#16300)

This commit is contained in:
N. Taylor Mullen
2026-01-12 15:44:08 -08:00
committed by GitHub
parent ca6786a28b
commit e9c9dd1d67
4 changed files with 75 additions and 3 deletions

View File

@@ -11,6 +11,15 @@ import * as path from 'node:path';
import { SkillManager } from './skillManager.js';
import { Storage } from '../config/storage.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', () => {
let testRootDir: string;
@@ -71,6 +80,8 @@ description: project-desc
vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(projectDir);
const service = new SkillManager();
// @ts-expect-error accessing private method for testing
vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined);
await service.discoverSkills(storage, [mockExtension]);
const skills = service.getSkills();
@@ -126,6 +137,8 @@ description: project-desc
vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(projectDir);
const service = new SkillManager();
// @ts-expect-error accessing private method for testing
vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined);
await service.discoverSkills(storage, [mockExtension]);
const skills = service.getSkills();
@@ -138,6 +151,34 @@ description: project-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 () => {
const skillDir = path.join(testRootDir, 'skill1');
await fs.mkdir(skillDir, { recursive: true });
@@ -156,6 +197,8 @@ description: desc1
vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue('/non-existent');
const service = new SkillManager();
// @ts-expect-error accessing private method for testing
vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined);
await service.discoverSkills(storage);
service.setDisabledSkills(['skill1']);

View File

@@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { Storage } from '../config/storage.js';
import { type SkillDefinition, loadSkillsFromDir } from './skillLoader.js';
import type { GeminiCLIExtension } from '../config/config.js';
@@ -56,9 +58,16 @@ export class SkillManager {
* Discovers built-in skills.
*/
private async discoverBuiltinSkills(): Promise<void> {
// Built-in skills can be added here.
// For now, this is a placeholder for where built-in skills will be loaded from.
// They could be loaded from a specific directory within the package.
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const builtinDir = path.join(__dirname, 'builtin');
const builtinSkills = await loadSkillsFromDir(builtinDir);
for (const skill of builtinSkills) {
skill.isBuiltin = true;
}
this.addSkillsWithPrecedence(builtinSkills);
}
private addSkillsWithPrecedence(newSkills: SkillDefinition[]): void {

View File

@@ -62,4 +62,15 @@ if (existsSync(docsSrc)) {
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/');

View File

@@ -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.');