mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-03 00:14:28 -07:00
Agent Skills: Add gemini skills CLI management command (#15837)
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { skillsCommand } from './skills.js';
|
||||||
|
|
||||||
|
vi.mock('./skills/list.js', () => ({ listCommand: { command: 'list' } }));
|
||||||
|
vi.mock('./skills/enable.js', () => ({
|
||||||
|
enableCommand: { command: 'enable <name>' },
|
||||||
|
}));
|
||||||
|
vi.mock('./skills/disable.js', () => ({
|
||||||
|
disableCommand: { command: 'disable <name>' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../gemini.js', () => ({
|
||||||
|
initializeOutputListenersAndFlush: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('skillsCommand', () => {
|
||||||
|
it('should have correct command and aliases', () => {
|
||||||
|
expect(skillsCommand.command).toBe('skills <command>');
|
||||||
|
expect(skillsCommand.aliases).toEqual(['skill']);
|
||||||
|
expect(skillsCommand.describe).toBe('Manage agent skills.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register all subcommands in builder', () => {
|
||||||
|
const mockYargs = {
|
||||||
|
middleware: vi.fn().mockReturnThis(),
|
||||||
|
command: vi.fn().mockReturnThis(),
|
||||||
|
demandCommand: vi.fn().mockReturnThis(),
|
||||||
|
version: vi.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error - Mocking yargs
|
||||||
|
skillsCommand.builder(mockYargs);
|
||||||
|
|
||||||
|
expect(mockYargs.middleware).toHaveBeenCalled();
|
||||||
|
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'list' });
|
||||||
|
expect(mockYargs.command).toHaveBeenCalledWith({
|
||||||
|
command: 'enable <name>',
|
||||||
|
});
|
||||||
|
expect(mockYargs.command).toHaveBeenCalledWith({
|
||||||
|
command: 'disable <name>',
|
||||||
|
});
|
||||||
|
expect(mockYargs.demandCommand).toHaveBeenCalledWith(1, expect.any(String));
|
||||||
|
expect(mockYargs.version).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a handler that does nothing', () => {
|
||||||
|
// @ts-expect-error - Handler doesn't take arguments in this case
|
||||||
|
expect(skillsCommand.handler()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { CommandModule } from 'yargs';
|
||||||
|
import { listCommand } from './skills/list.js';
|
||||||
|
import { enableCommand } from './skills/enable.js';
|
||||||
|
import { disableCommand } from './skills/disable.js';
|
||||||
|
import { initializeOutputListenersAndFlush } from '../gemini.js';
|
||||||
|
|
||||||
|
export const skillsCommand: CommandModule = {
|
||||||
|
command: 'skills <command>',
|
||||||
|
aliases: ['skill'],
|
||||||
|
describe: 'Manage agent skills.',
|
||||||
|
builder: (yargs) =>
|
||||||
|
yargs
|
||||||
|
.middleware(() => initializeOutputListenersAndFlush())
|
||||||
|
.command(listCommand)
|
||||||
|
.command(enableCommand)
|
||||||
|
.command(disableCommand)
|
||||||
|
.demandCommand(1, 'You need at least one command before continuing.')
|
||||||
|
.version(false),
|
||||||
|
handler: () => {
|
||||||
|
// This handler is not called when a subcommand is provided.
|
||||||
|
// Yargs will show the help menu.
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { format } from 'node:util';
|
||||||
|
import { handleDisable, disableCommand } from './disable.js';
|
||||||
|
import {
|
||||||
|
loadSettings,
|
||||||
|
SettingScope,
|
||||||
|
type LoadedSettings,
|
||||||
|
type LoadableSettingScope,
|
||||||
|
} from '../../config/settings.js';
|
||||||
|
|
||||||
|
const emitConsoleLog = vi.hoisted(() => vi.fn());
|
||||||
|
const debugLogger = vi.hoisted(() => ({
|
||||||
|
log: vi.fn((message, ...args) => {
|
||||||
|
emitConsoleLog('log', format(message, ...args));
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||||
|
const actual =
|
||||||
|
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
debugLogger,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('../../config/settings.js', async (importOriginal) => {
|
||||||
|
const actual =
|
||||||
|
await importOriginal<typeof import('../../config/settings.js')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
loadSettings: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('../utils.js', () => ({
|
||||||
|
exitCli: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('skills disable command', () => {
|
||||||
|
const mockLoadSettings = vi.mocked(loadSettings);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleDisable', () => {
|
||||||
|
it('should disable an enabled skill in user scope', async () => {
|
||||||
|
const mockSettings = {
|
||||||
|
forScope: vi.fn().mockReturnValue({
|
||||||
|
settings: { skills: { disabled: [] } },
|
||||||
|
}),
|
||||||
|
setValue: vi.fn(),
|
||||||
|
};
|
||||||
|
mockLoadSettings.mockReturnValue(
|
||||||
|
mockSettings as unknown as LoadedSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
await handleDisable({
|
||||||
|
name: 'skill1',
|
||||||
|
scope: SettingScope.User as LoadableSettingScope,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||||
|
SettingScope.User,
|
||||||
|
'skills.disabled',
|
||||||
|
['skill1'],
|
||||||
|
);
|
||||||
|
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||||
|
'log',
|
||||||
|
'Skill "skill1" successfully disabled in scope "User".',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log a message if the skill is already disabled', async () => {
|
||||||
|
const mockSettings = {
|
||||||
|
forScope: vi.fn().mockReturnValue({
|
||||||
|
settings: { skills: { disabled: ['skill1'] } },
|
||||||
|
}),
|
||||||
|
setValue: vi.fn(),
|
||||||
|
};
|
||||||
|
mockLoadSettings.mockReturnValue(
|
||||||
|
mockSettings as unknown as LoadedSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
await handleDisable({
|
||||||
|
name: 'skill1',
|
||||||
|
scope: SettingScope.User as LoadableSettingScope,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockSettings.setValue).not.toHaveBeenCalled();
|
||||||
|
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||||
|
'log',
|
||||||
|
'Skill "skill1" is already disabled in scope "User".',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('disableCommand', () => {
|
||||||
|
it('should have correct command and describe', () => {
|
||||||
|
expect(disableCommand.command).toBe('disable <name>');
|
||||||
|
expect(disableCommand.describe).toBe('Disables an agent skill.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { CommandModule } from 'yargs';
|
||||||
|
import {
|
||||||
|
loadSettings,
|
||||||
|
SettingScope,
|
||||||
|
type LoadableSettingScope,
|
||||||
|
} from '../../config/settings.js';
|
||||||
|
import { debugLogger } from '@google/gemini-cli-core';
|
||||||
|
import { exitCli } from '../utils.js';
|
||||||
|
|
||||||
|
interface DisableArgs {
|
||||||
|
name: string;
|
||||||
|
scope: LoadableSettingScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleDisable(args: DisableArgs) {
|
||||||
|
const { name, scope } = args;
|
||||||
|
const workspaceDir = process.cwd();
|
||||||
|
const settings = loadSettings(workspaceDir);
|
||||||
|
|
||||||
|
const currentDisabled =
|
||||||
|
settings.forScope(scope).settings.skills?.disabled || [];
|
||||||
|
|
||||||
|
if (currentDisabled.includes(name)) {
|
||||||
|
debugLogger.log(`Skill "${name}" is already disabled in scope "${scope}".`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDisabled = [...currentDisabled, name];
|
||||||
|
settings.setValue(scope, 'skills.disabled', newDisabled);
|
||||||
|
debugLogger.log(`Skill "${name}" successfully disabled in scope "${scope}".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const disableCommand: CommandModule = {
|
||||||
|
command: 'disable <name>',
|
||||||
|
describe: 'Disables an agent skill.',
|
||||||
|
builder: (yargs) =>
|
||||||
|
yargs
|
||||||
|
.positional('name', {
|
||||||
|
describe: 'The name of the skill to disable.',
|
||||||
|
type: 'string',
|
||||||
|
demandOption: true,
|
||||||
|
})
|
||||||
|
.option('scope', {
|
||||||
|
alias: 's',
|
||||||
|
describe: 'The scope to disable the skill in (user or project).',
|
||||||
|
type: 'string',
|
||||||
|
default: 'user',
|
||||||
|
choices: ['user', 'project'],
|
||||||
|
}),
|
||||||
|
handler: async (argv) => {
|
||||||
|
const scope: LoadableSettingScope =
|
||||||
|
argv['scope'] === 'project' ? SettingScope.Workspace : SettingScope.User;
|
||||||
|
await handleDisable({
|
||||||
|
name: argv['name'] as string,
|
||||||
|
scope,
|
||||||
|
});
|
||||||
|
await exitCli();
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { format } from 'node:util';
|
||||||
|
import { handleEnable, enableCommand } from './enable.js';
|
||||||
|
import {
|
||||||
|
loadSettings,
|
||||||
|
SettingScope,
|
||||||
|
type LoadedSettings,
|
||||||
|
type LoadableSettingScope,
|
||||||
|
} from '../../config/settings.js';
|
||||||
|
|
||||||
|
const emitConsoleLog = vi.hoisted(() => vi.fn());
|
||||||
|
const debugLogger = vi.hoisted(() => ({
|
||||||
|
log: vi.fn((message, ...args) => {
|
||||||
|
emitConsoleLog('log', format(message, ...args));
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||||
|
const actual =
|
||||||
|
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
debugLogger,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('../../config/settings.js', async (importOriginal) => {
|
||||||
|
const actual =
|
||||||
|
await importOriginal<typeof import('../../config/settings.js')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
loadSettings: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('../utils.js', () => ({
|
||||||
|
exitCli: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('skills enable command', () => {
|
||||||
|
const mockLoadSettings = vi.mocked(loadSettings);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleEnable', () => {
|
||||||
|
it('should enable a disabled skill in user scope', async () => {
|
||||||
|
const mockSettings = {
|
||||||
|
forScope: vi.fn().mockReturnValue({
|
||||||
|
settings: { skills: { disabled: ['skill1'] } },
|
||||||
|
}),
|
||||||
|
setValue: vi.fn(),
|
||||||
|
};
|
||||||
|
mockLoadSettings.mockReturnValue(
|
||||||
|
mockSettings as unknown as LoadedSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
await handleEnable({
|
||||||
|
name: 'skill1',
|
||||||
|
scope: SettingScope.User as LoadableSettingScope,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||||
|
SettingScope.User,
|
||||||
|
'skills.disabled',
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||||
|
'log',
|
||||||
|
'Skill "skill1" successfully enabled in scope "User".',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log a message if the skill is already enabled', async () => {
|
||||||
|
const mockSettings = {
|
||||||
|
forScope: vi.fn().mockReturnValue({
|
||||||
|
settings: { skills: { disabled: [] } },
|
||||||
|
}),
|
||||||
|
setValue: vi.fn(),
|
||||||
|
};
|
||||||
|
mockLoadSettings.mockReturnValue(
|
||||||
|
mockSettings as unknown as LoadedSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
await handleEnable({
|
||||||
|
name: 'skill1',
|
||||||
|
scope: SettingScope.User as LoadableSettingScope,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockSettings.setValue).not.toHaveBeenCalled();
|
||||||
|
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||||
|
'log',
|
||||||
|
'Skill "skill1" is already enabled in scope "User".',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('enableCommand', () => {
|
||||||
|
it('should have correct command and describe', () => {
|
||||||
|
expect(enableCommand.command).toBe('enable <name>');
|
||||||
|
expect(enableCommand.describe).toBe('Enables an agent skill.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { CommandModule } from 'yargs';
|
||||||
|
import {
|
||||||
|
loadSettings,
|
||||||
|
SettingScope,
|
||||||
|
type LoadableSettingScope,
|
||||||
|
} from '../../config/settings.js';
|
||||||
|
import { debugLogger } from '@google/gemini-cli-core';
|
||||||
|
import { exitCli } from '../utils.js';
|
||||||
|
|
||||||
|
interface EnableArgs {
|
||||||
|
name: string;
|
||||||
|
scope: LoadableSettingScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleEnable(args: EnableArgs) {
|
||||||
|
const { name, scope } = args;
|
||||||
|
const workspaceDir = process.cwd();
|
||||||
|
const settings = loadSettings(workspaceDir);
|
||||||
|
|
||||||
|
const currentDisabled =
|
||||||
|
settings.forScope(scope).settings.skills?.disabled || [];
|
||||||
|
const newDisabled = currentDisabled.filter((d) => d !== name);
|
||||||
|
|
||||||
|
if (currentDisabled.length === newDisabled.length) {
|
||||||
|
debugLogger.log(`Skill "${name}" is already enabled in scope "${scope}".`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.setValue(scope, 'skills.disabled', newDisabled);
|
||||||
|
debugLogger.log(`Skill "${name}" successfully enabled in scope "${scope}".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enableCommand: CommandModule = {
|
||||||
|
command: 'enable <name>',
|
||||||
|
describe: 'Enables an agent skill.',
|
||||||
|
builder: (yargs) =>
|
||||||
|
yargs
|
||||||
|
.positional('name', {
|
||||||
|
describe: 'The name of the skill to enable.',
|
||||||
|
type: 'string',
|
||||||
|
demandOption: true,
|
||||||
|
})
|
||||||
|
.option('scope', {
|
||||||
|
alias: 's',
|
||||||
|
describe: 'The scope to enable the skill in (user or project).',
|
||||||
|
type: 'string',
|
||||||
|
default: 'user',
|
||||||
|
choices: ['user', 'project'],
|
||||||
|
}),
|
||||||
|
handler: async (argv) => {
|
||||||
|
const scope: LoadableSettingScope =
|
||||||
|
argv['scope'] === 'project' ? SettingScope.Workspace : SettingScope.User;
|
||||||
|
await handleEnable({
|
||||||
|
name: argv['name'] as string,
|
||||||
|
scope,
|
||||||
|
});
|
||||||
|
await exitCli();
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { format } from 'node:util';
|
||||||
|
import { handleList, listCommand } from './list.js';
|
||||||
|
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||||
|
import { loadCliConfig } from '../../config/config.js';
|
||||||
|
import type { Config } from '@google/gemini-cli-core';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
const emitConsoleLog = vi.hoisted(() => vi.fn());
|
||||||
|
const debugLogger = vi.hoisted(() => ({
|
||||||
|
log: vi.fn((message, ...args) => {
|
||||||
|
emitConsoleLog('log', format(message, ...args));
|
||||||
|
}),
|
||||||
|
error: vi.fn((message, ...args) => {
|
||||||
|
emitConsoleLog('error', format(message, ...args));
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||||
|
const actual =
|
||||||
|
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
coreEvents: {
|
||||||
|
emitConsoleLog,
|
||||||
|
},
|
||||||
|
debugLogger,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('../../config/settings.js');
|
||||||
|
vi.mock('../../config/config.js');
|
||||||
|
vi.mock('../utils.js', () => ({
|
||||||
|
exitCli: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('skills list command', () => {
|
||||||
|
const mockLoadSettings = vi.mocked(loadSettings);
|
||||||
|
const mockLoadCliConfig = vi.mocked(loadCliConfig);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockLoadSettings.mockReturnValue({
|
||||||
|
merged: {},
|
||||||
|
} as unknown as LoadedSettings);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleList', () => {
|
||||||
|
it('should log a message if no skills are discovered', async () => {
|
||||||
|
const mockConfig = {
|
||||||
|
initialize: vi.fn().mockResolvedValue(undefined),
|
||||||
|
getSkillManager: vi.fn().mockReturnValue({
|
||||||
|
getAllSkills: vi.fn().mockReturnValue([]),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||||
|
|
||||||
|
await handleList();
|
||||||
|
|
||||||
|
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||||
|
'log',
|
||||||
|
'No skills discovered.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list all discovered skills', async () => {
|
||||||
|
const skills = [
|
||||||
|
{
|
||||||
|
name: 'skill1',
|
||||||
|
description: 'desc1',
|
||||||
|
disabled: false,
|
||||||
|
location: '/path/to/skill1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'skill2',
|
||||||
|
description: 'desc2',
|
||||||
|
disabled: true,
|
||||||
|
location: '/path/to/skill2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const mockConfig = {
|
||||||
|
initialize: vi.fn().mockResolvedValue(undefined),
|
||||||
|
getSkillManager: vi.fn().mockReturnValue({
|
||||||
|
getAllSkills: vi.fn().mockReturnValue(skills),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||||
|
|
||||||
|
await handleList();
|
||||||
|
|
||||||
|
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||||
|
'log',
|
||||||
|
chalk.bold('Discovered Agent Skills:'),
|
||||||
|
);
|
||||||
|
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||||
|
'log',
|
||||||
|
expect.stringContaining('skill1'),
|
||||||
|
);
|
||||||
|
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||||
|
'log',
|
||||||
|
expect.stringContaining(chalk.green('[Enabled]')),
|
||||||
|
);
|
||||||
|
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||||
|
'log',
|
||||||
|
expect.stringContaining('skill2'),
|
||||||
|
);
|
||||||
|
expect(emitConsoleLog).toHaveBeenCalledWith(
|
||||||
|
'log',
|
||||||
|
expect.stringContaining(chalk.red('[Disabled]')),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when listing fails', async () => {
|
||||||
|
mockLoadCliConfig.mockRejectedValue(new Error('List failed'));
|
||||||
|
|
||||||
|
await expect(handleList()).rejects.toThrow('List failed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listCommand', () => {
|
||||||
|
const command = listCommand;
|
||||||
|
|
||||||
|
it('should have correct command and describe', () => {
|
||||||
|
expect(command.command).toBe('list');
|
||||||
|
expect(command.describe).toBe('Lists discovered agent skills.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { CommandModule } from 'yargs';
|
||||||
|
import { debugLogger } from '@google/gemini-cli-core';
|
||||||
|
import { loadSettings } from '../../config/settings.js';
|
||||||
|
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||||
|
import { exitCli } from '../utils.js';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
export async function handleList() {
|
||||||
|
const workspaceDir = process.cwd();
|
||||||
|
const settings = loadSettings(workspaceDir);
|
||||||
|
|
||||||
|
const config = await loadCliConfig(
|
||||||
|
settings.merged,
|
||||||
|
'skills-list-session',
|
||||||
|
{
|
||||||
|
debug: false,
|
||||||
|
} as Partial<CliArgs> as CliArgs,
|
||||||
|
{ cwd: workspaceDir },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize to trigger extension loading and skill discovery
|
||||||
|
await config.initialize();
|
||||||
|
|
||||||
|
const skillManager = config.getSkillManager();
|
||||||
|
const skills = skillManager.getAllSkills();
|
||||||
|
|
||||||
|
if (skills.length === 0) {
|
||||||
|
debugLogger.log('No skills discovered.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLogger.log(chalk.bold('Discovered Agent Skills:'));
|
||||||
|
debugLogger.log('');
|
||||||
|
|
||||||
|
for (const skill of skills) {
|
||||||
|
const status = skill.disabled
|
||||||
|
? chalk.red('[Disabled]')
|
||||||
|
: chalk.green('[Enabled]');
|
||||||
|
|
||||||
|
debugLogger.log(`${chalk.bold(skill.name)} ${status}`);
|
||||||
|
debugLogger.log(` Description: ${skill.description}`);
|
||||||
|
debugLogger.log(` Location: ${skill.location}`);
|
||||||
|
debugLogger.log('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listCommand: CommandModule = {
|
||||||
|
command: 'list',
|
||||||
|
describe: 'Lists discovered agent skills.',
|
||||||
|
builder: (yargs) => yargs,
|
||||||
|
handler: async () => {
|
||||||
|
await handleList();
|
||||||
|
await exitCli();
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import { hideBin } from 'yargs/helpers';
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { mcpCommand } from '../commands/mcp.js';
|
import { mcpCommand } from '../commands/mcp.js';
|
||||||
import { extensionsCommand } from '../commands/extensions.js';
|
import { extensionsCommand } from '../commands/extensions.js';
|
||||||
|
import { skillsCommand } from '../commands/skills.js';
|
||||||
import { hooksCommand } from '../commands/hooks.js';
|
import { hooksCommand } from '../commands/hooks.js';
|
||||||
import {
|
import {
|
||||||
Config,
|
Config,
|
||||||
@@ -285,6 +286,10 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
|||||||
yargsInstance.command(extensionsCommand);
|
yargsInstance.command(extensionsCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings?.experimental?.skills ?? false) {
|
||||||
|
yargsInstance.command(skillsCommand);
|
||||||
|
}
|
||||||
|
|
||||||
// Register hooks command if hooks are enabled
|
// Register hooks command if hooks are enabled
|
||||||
if (settings?.tools?.enableHooks) {
|
if (settings?.tools?.enableHooks) {
|
||||||
yargsInstance.command(hooksCommand);
|
yargsInstance.command(hooksCommand);
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ const MIGRATION_MAP: Record<string, string> = {
|
|||||||
excludeTools: 'tools.exclude',
|
excludeTools: 'tools.exclude',
|
||||||
excludeMCPServers: 'mcp.excluded',
|
excludeMCPServers: 'mcp.excluded',
|
||||||
excludedProjectEnvVars: 'advanced.excludedEnvVars',
|
excludedProjectEnvVars: 'advanced.excludedEnvVars',
|
||||||
|
experimentalSkills: 'experimental.skills',
|
||||||
extensionManagement: 'experimental.extensionManagement',
|
extensionManagement: 'experimental.extensionManagement',
|
||||||
extensions: 'extensions',
|
extensions: 'extensions',
|
||||||
fileFiltering: 'context.fileFiltering',
|
fileFiltering: 'context.fileFiltering',
|
||||||
|
|||||||
Reference in New Issue
Block a user