mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 22:02:59 -07:00
feat(cli): support skill discovery for Open Plugins
- Updated skillLoader to support discovery in skills/ subdirectories - Implemented convention-based skill discovery for Open Plugins - Enforced namespacing for plugin skills (plugin:skill-name) - Refactored skill resolution into resolvePluginSkills for better maintainability - Added comprehensive tests for Open Plugin skill discovery Fixes https://github.com/google-gemini/maintainers-gemini-cli/issues/1592
This commit is contained in:
@@ -129,11 +129,10 @@ describe('ExtensionManager - Open Plugin Support', () => {
|
||||
expect(plugin?.description).toBe(`Uses root: ${pluginDir}`);
|
||||
});
|
||||
|
||||
it('should NOT load skills or context files for Open Plugins in v1', async () => {
|
||||
it('should load skills for Open Plugins', async () => {
|
||||
const pluginDir = path.join(userExtensionsDir, 'feature-plugin');
|
||||
const hiddenDir = path.join(pluginDir, '.plugin');
|
||||
fs.mkdirSync(hiddenDir, { recursive: true });
|
||||
const skillsDir = path.join(pluginDir, 'skills', 'test');
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
const skillsDir = path.join(pluginDir, 'skills', 'test-skill');
|
||||
fs.mkdirSync(skillsDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
@@ -147,20 +146,19 @@ describe('ExtensionManager - Open Plugin Support', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(skillsDir, 'SKILL.md'),
|
||||
`---
|
||||
name: test-skill
|
||||
description: "Test"
|
||||
---
|
||||
Body`,
|
||||
name: my-skill
|
||||
description: "Test description"
|
||||
---
|
||||
Body`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(path.join(pluginDir, 'GEMINI.md'), '# Context');
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const plugin = extensions.find((ext) => ext.name === 'feature-plugin');
|
||||
|
||||
expect(plugin).toBeDefined();
|
||||
expect(plugin?.skills).toBeUndefined();
|
||||
expect(plugin?.contextFiles).toEqual([]);
|
||||
expect(plugin?.skills).toBeDefined();
|
||||
expect(plugin?.skills?.[0].name).toBe('feature-plugin:my-skill');
|
||||
expect(plugin?.skills?.[0].extensionName).toBe('feature-plugin');
|
||||
});
|
||||
|
||||
it('should prioritize gemini-extension.json over plugin.json', async () => {
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import type {
|
||||
ExtensionInstallMetadata,
|
||||
GeminiCLIExtension,
|
||||
import {
|
||||
loadSkillsFromDir,
|
||||
type ExtensionInstallMetadata,
|
||||
type GeminiCLIExtension,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
EXTENSIONS_CONFIG_FILENAME,
|
||||
@@ -156,7 +157,6 @@ export async function loadOpenPluginConfig(
|
||||
|
||||
/**
|
||||
* Creates a GeminiCLIExtension from an Open Plugin directory.
|
||||
* v1: Does not enable skills, mcp servers, context files, or settings.
|
||||
*/
|
||||
export async function createOpenPlugin(
|
||||
pluginDir: string,
|
||||
@@ -173,6 +173,20 @@ export async function createOpenPlugin(
|
||||
workspaceDir,
|
||||
);
|
||||
|
||||
const hydrationContext = {
|
||||
extensionPath: pluginDir,
|
||||
PLUGIN_ROOT: pluginDir,
|
||||
workspacePath: workspaceDir,
|
||||
'/': path.sep,
|
||||
pathSeparator: path.sep,
|
||||
};
|
||||
|
||||
const skills = await resolvePluginSkills(
|
||||
pluginDir,
|
||||
config.name,
|
||||
hydrationContext,
|
||||
);
|
||||
|
||||
return {
|
||||
name: config.name,
|
||||
version: config.version,
|
||||
@@ -193,8 +207,30 @@ export async function createOpenPlugin(
|
||||
excludeTools: undefined,
|
||||
settings: undefined,
|
||||
resolvedSettings: undefined,
|
||||
skills: undefined,
|
||||
skills,
|
||||
agents: undefined,
|
||||
themes: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers and namespaces skills for an Open Plugin.
|
||||
*/
|
||||
async function resolvePluginSkills(
|
||||
pluginDir: string,
|
||||
pluginName: string,
|
||||
hydrationContext: Record<string, string>,
|
||||
): Promise<GeminiCLIExtension['skills']> {
|
||||
const skillsDir = path.join(pluginDir, 'skills');
|
||||
const discoveredSkills = await loadSkillsFromDir(skillsDir);
|
||||
|
||||
if (discoveredSkills.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return discoveredSkills.map((skill) => ({
|
||||
...recursivelyHydrateStrings(skill, hydrationContext),
|
||||
name: `${pluginName}:${skill.name}`,
|
||||
extensionName: pluginName,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -218,6 +218,19 @@ describe('SlashCommandResolver', () => {
|
||||
expect(names).toContain('google-workspace:chat1');
|
||||
});
|
||||
|
||||
it('should NOT double-prefix when a skill name is already prefixed', () => {
|
||||
const skill = {
|
||||
...createMockCommand('google-workspace:chat', CommandKind.SKILL),
|
||||
extensionName: 'google-workspace',
|
||||
};
|
||||
|
||||
const { finalCommands } = SlashCommandResolver.resolve([skill]);
|
||||
|
||||
const names = finalCommands.map((c) => c.name);
|
||||
expect(names).toContain('google-workspace:chat');
|
||||
expect(names).not.toContain('google-workspace:google-workspace:chat');
|
||||
});
|
||||
|
||||
it('should NOT prefix skills with "skill" when extension name is missing', () => {
|
||||
const builtin = createMockCommand('chat', CommandKind.BUILT_IN);
|
||||
const skill = createMockCommand('chat', CommandKind.SKILL);
|
||||
|
||||
@@ -173,7 +173,15 @@ export class SlashCommandResolver {
|
||||
const isExtensionPrefix =
|
||||
kind === CommandKind.SKILL || kind === CommandKind.EXTENSION_FILE;
|
||||
const separator = isExtensionPrefix ? ':' : '.';
|
||||
const base = prefix ? `${prefix}${separator}${name}` : name;
|
||||
|
||||
// Check if it's already correctly prefixed
|
||||
const alreadyPrefixed = prefix && name.startsWith(`${prefix}${separator}`);
|
||||
const base = alreadyPrefixed
|
||||
? name
|
||||
: prefix
|
||||
? `${prefix}${separator}${name}`
|
||||
: name;
|
||||
|
||||
let renamedName = base;
|
||||
let suffix = 1;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user