This commit is contained in:
matt korwel 2026-04-21 09:36:12 +00:00 committed by GitHub
commit c973a8aff0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 132 additions and 75 deletions

View file

@ -78,3 +78,52 @@ This skill guides the agent in conducting thorough code reviews.
use it.
- **Body**: The Markdown body of the file contains the instructions that guide
the agent's behavior when the skill is active.
## Packaging and Distribution
To share a skill or distribute it as a standalone archive, you can package the
skill directory into a `.skill` file. This is essentially a ZIP archive that the
`gemini skills install` command can process.
### Using the `skill-creator` toolchain
The built-in `skill-creator` skill includes a packaging script that validates
your skill (checking for missing frontmatter, unresolved TODOs, etc.) before
creating the archive.
To package a skill:
1. **Activate** the `skill-creator` skill in a Gemini session.
2. **Ask** the agent to "package my skill located at `./path/to/skill`".
The agent will run the necessary validation and create a `.skill` file in your
specified output directory.
### Manual Packaging
If you prefer to package manually, you can simply create a ZIP archive of the
skill's root directory (ensuring `SKILL.md` is at the top level) and rename the
extension to `.skill`.
```bash
# Example: Package 'my-skill' directory into 'my-skill.skill'
(cd my-skill/ && zip -r ../my-skill.skill .)
```
## Installing a Packaged Skill
Once you have a `.skill` file, you can install it using the `gemini` CLI.
```bash
# Install to the user scope (global)
gemini skills install ./my-skill.skill
# Install to the workspace scope (local repository)
gemini skills install ./my-skill.skill --scope workspace
```
After installation, remember to reload your session to pick up the new
expertise:
1. In an interactive session, run `/skills reload`.
2. Verify the installation with `/skills list`.

View file

@ -106,6 +106,18 @@ gemini skills enable my-expertise
gemini skills disable my-expertise --scope workspace
```
### Activation and Reloading
After installing or linking a new skill, it is not automatically available in
currently active sessions. To pick up the new expertise:
1. **In an interactive session**: Run the `/skills reload` slash command. This
refreshes the list of discovered skills from all tiers and scopes.
2. **Verify**: Run `/skills list` to confirm the new skill is discovered and
enabled.
3. **Automatic Discovery**: New sessions will automatically discover all
available skills during startup.
## How it Works
1. **Discovery**: At the start of a session, Gemini CLI scans the discovery

View file

@ -15,7 +15,7 @@ import {
} from 'vitest';
import { handleInstall, installCommand } from './install.js';
import yargs from 'yargs';
import * as core from '@google/gemini-cli-core';
import { debugLogger } from '@google/gemini-cli-core';
import type { Stats } from 'node:fs';
import * as path from 'node:path';
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
@ -116,12 +116,8 @@ describe('handleInstall', () => {
let processSpy: MockInstance;
beforeEach(() => {
debugLogSpy = vi
.spyOn(core.debugLogger, 'log')
.mockImplementation(() => {});
debugErrorSpy = vi
.spyOn(core.debugLogger, 'error')
.mockImplementation(() => {});
debugLogSpy = vi.spyOn(debugLogger, 'log').mockImplementation(() => {});
debugErrorSpy = vi.spyOn(debugLogger, 'error').mockImplementation(() => {});
processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);

View file

@ -23,6 +23,11 @@ import {
Storage,
generalistProfile,
type ContextManagementConfig,
PolicyDecision,
createPolicyEngineConfig,
loadServerHierarchicalMemory,
type Config,
TelemetryTarget,
} from '@google/gemini-cli-core';
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
import {
@ -30,7 +35,6 @@ import {
type MergedSettings,
createTestMergedSettings,
} from './settings.js';
import * as ServerConfig from '@google/gemini-cli-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { ExtensionManager } from './extension-manager.js';
@ -150,9 +154,9 @@ vi.mock('@google/gemini-cli-core', async () => {
rules: [],
checkers: [],
defaultDecision: interactive
? ServerConfig.PolicyDecision.ASK_USER
: ServerConfig.PolicyDecision.DENY,
approvalMode: approvalMode ?? ServerConfig.ApprovalMode.DEFAULT,
? PolicyDecision.ASK_USER
: PolicyDecision.DENY,
approvalMode: approvalMode ?? ApprovalMode.DEFAULT,
nonInteractive: !interactive,
}),
),
@ -986,7 +990,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
]);
const argv = await parseArguments(createTestMergedSettings());
await loadCliConfig(settings, 'session-id', argv);
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect(loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect.any(String),
[],
expect.any(Object),
@ -1016,7 +1020,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
const argv = await parseArguments(settings);
await loadCliConfig(settings, 'session-id', argv);
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect(loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect.any(String),
[includeDir],
expect.any(Object),
@ -1045,7 +1049,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
const argv = await parseArguments(settings);
await loadCliConfig(settings, 'session-id', argv);
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect(loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect.any(String),
[],
expect.any(Object),
@ -2746,7 +2750,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should set YOLO approval mode when --yolo flag is used', async () => {
@ -2757,7 +2761,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
});
it('should set YOLO approval mode when -y flag is used', async () => {
@ -2768,7 +2772,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
});
it('should set DEFAULT approval mode when --approval-mode=default', async () => {
@ -2779,7 +2783,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should set AUTO_EDIT approval mode when --approval-mode=auto_edit', async () => {
@ -2790,7 +2794,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT);
expect(config.getApprovalMode()).toBe(ApprovalMode.AUTO_EDIT);
});
it('should set YOLO approval mode when --approval-mode=yolo', async () => {
@ -2801,7 +2805,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
});
it('should prioritize --approval-mode over --yolo when both would be valid (but validation prevents this)', async () => {
@ -2816,7 +2820,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should fall back to --yolo behavior when --approval-mode is not set', async () => {
@ -2827,7 +2831,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
});
it('should set Plan approval mode when --approval-mode=plan is used and plan is enabled', async () => {
@ -2839,7 +2843,7 @@ describe('loadCliConfig approval mode', () => {
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
expect(config.getApprovalMode()).toBe(ApprovalMode.PLAN);
});
it('should ignore "yolo" in settings.tools.approvalMode and fall back to DEFAULT', async () => {
@ -2852,7 +2856,7 @@ describe('loadCliConfig approval mode', () => {
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should throw error when --approval-mode=plan is used but plan is disabled', async () => {
@ -2909,7 +2913,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should override --approval-mode=auto_edit to DEFAULT', async () => {
@ -2920,7 +2924,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should override --yolo flag to DEFAULT', async () => {
@ -2931,7 +2935,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should remain DEFAULT when --approval-mode=default', async () => {
@ -2942,7 +2946,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
});
@ -2954,9 +2958,7 @@ describe('loadCliConfig approval mode', () => {
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(
ServerConfig.ApprovalMode.AUTO_EDIT,
);
expect(config.getApprovalMode()).toBe(ApprovalMode.AUTO_EDIT);
});
it('should prioritize --approval-mode flag over settings', async () => {
@ -2966,9 +2968,7 @@ describe('loadCliConfig approval mode', () => {
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(
ServerConfig.ApprovalMode.AUTO_EDIT,
);
expect(config.getApprovalMode()).toBe(ApprovalMode.AUTO_EDIT);
});
it('should prioritize --yolo flag over settings', async () => {
@ -2978,7 +2978,7 @@ describe('loadCliConfig approval mode', () => {
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
});
it('should respect plan mode from settings when plan is enabled', async () => {
@ -2991,7 +2991,7 @@ describe('loadCliConfig approval mode', () => {
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
expect(config.getApprovalMode()).toBe(ApprovalMode.PLAN);
});
it('should fall back to default if plan mode is in settings but disabled', async () => {
@ -3097,7 +3097,7 @@ describe('loadCliConfig fileFiltering', () => {
>;
const testCases: Array<{
property: keyof FileFilteringSettings;
getter: (config: ServerConfig.Config) => boolean;
getter: (config: Config) => boolean;
value: boolean;
}> = [
{
@ -3341,7 +3341,7 @@ describe('Telemetry configuration via environment variables', () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
telemetry: { target: ServerConfig.TelemetryTarget.LOCAL },
telemetry: { target: TelemetryTarget.LOCAL },
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getTelemetryTarget()).toBe('gcp');
@ -3352,7 +3352,7 @@ describe('Telemetry configuration via environment variables', () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
telemetry: { target: ServerConfig.TelemetryTarget.GCP },
telemetry: { target: TelemetryTarget.GCP },
});
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
/Invalid telemetry configuration: .*Invalid telemetry target/i,
@ -3430,7 +3430,7 @@ describe('Telemetry configuration via environment variables', () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
telemetry: { target: ServerConfig.TelemetryTarget.LOCAL },
telemetry: { target: TelemetryTarget.LOCAL },
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getTelemetryTarget()).toBe('local');
@ -3554,7 +3554,7 @@ describe('Policy Engine Integration in loadCliConfig', () => {
await loadCliConfig(settings, 'test-session', argv);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.objectContaining({
allowed: expect.arrayContaining(['cli-tool']),
@ -3577,7 +3577,7 @@ describe('Policy Engine Integration in loadCliConfig', () => {
await loadCliConfig(settings, 'test-session', argv);
// In non-interactive mode, only ask_user is excluded by default
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.objectContaining({
exclude: expect.arrayContaining([ASK_USER_TOOL_NAME]),
@ -3601,7 +3601,7 @@ describe('Policy Engine Integration in loadCliConfig', () => {
await loadCliConfig(settings, 'test-session', argv);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
policyPaths: [
path.normalize('/path/to/policy1.toml'),

View file

@ -6,9 +6,13 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as path from 'node:path';
import {
isHeadlessMode,
Storage,
createPolicyEngineConfig,
} from '@google/gemini-cli-core';
import { loadCliConfig, type CliArgs } from './config.js';
import { createTestMergedSettings } from './settings.js';
import * as ServerConfig from '@google/gemini-cli-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
import * as Policy from './policy.js';
@ -21,9 +25,9 @@ const mockCheckIntegrity = vi.fn();
const mockAcceptIntegrity = vi.fn();
vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual<typeof ServerConfig>(
'@google/gemini-cli-core',
);
const actual = await vi.importActual<
typeof import('@google/gemini-cli-core')
>('@google/gemini-cli-core');
return {
...actual,
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
@ -61,11 +65,11 @@ describe('Workspace-Level Policy CLI Integration', () => {
hash: 'test-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false);
vi.mocked(isHeadlessMode).mockReturnValue(false);
});
it('should have getWorkspacePoliciesDir on Storage class', () => {
const storage = new ServerConfig.Storage(MOCK_CWD);
const storage = new Storage(MOCK_CWD);
expect(storage.getWorkspacePoliciesDir).toBeDefined();
expect(typeof storage.getWorkspacePoliciesDir).toBe('function');
});
@ -81,7 +85,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
@ -104,7 +108,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
@ -130,7 +134,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
@ -150,7 +154,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
hash: 'new-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(true); // Non-interactive
vi.mocked(isHeadlessMode).mockReturnValue(true); // Non-interactive
const settings = createTestMergedSettings();
const argv = { prompt: 'do something' } as unknown as CliArgs;
@ -162,7 +166,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
MOCK_CWD,
'new-hash',
);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
@ -184,7 +188,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
hash: 'new-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
vi.mocked(isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = {
@ -202,7 +206,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
MOCK_CWD,
'new-hash',
);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
@ -224,7 +228,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
hash: 'new-hash',
fileCount: 5,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
vi.mocked(isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = { query: 'test' } as unknown as CliArgs;
@ -240,7 +244,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
'new-hash',
);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
@ -267,7 +271,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
hash: 'new-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
vi.mocked(isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = {
@ -285,7 +289,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
newHash: 'new-hash',
});
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),

View file

@ -6,7 +6,11 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { setupWorktree } from './worktreeSetup.js';
import * as coreFunctions from '@google/gemini-cli-core';
import {
getProjectRootForWorktree,
createWorktreeService,
writeToStderr,
} from '@google/gemini-cli-core';
// Mock dependencies
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
@ -47,12 +51,8 @@ describe('setupWorktree', () => {
});
// Mock successful execution of core utilities
vi.mocked(coreFunctions.getProjectRootForWorktree).mockResolvedValue(
'/mock/project',
);
vi.mocked(coreFunctions.createWorktreeService).mockResolvedValue(
mockService as never,
);
vi.mocked(getProjectRootForWorktree).mockResolvedValue('/mock/project');
vi.mocked(createWorktreeService).mockResolvedValue(mockService as never);
mockService.setup.mockResolvedValue({
name: 'my-feature',
path: '/mock/project/.gemini/worktrees/my-feature',
@ -69,12 +69,8 @@ describe('setupWorktree', () => {
it('should create and switch to a new worktree', async () => {
await setupWorktree('my-feature');
expect(coreFunctions.getProjectRootForWorktree).toHaveBeenCalledWith(
'/mock/project',
);
expect(coreFunctions.createWorktreeService).toHaveBeenCalledWith(
'/mock/project',
);
expect(getProjectRootForWorktree).toHaveBeenCalledWith('/mock/project');
expect(createWorktreeService).toHaveBeenCalledWith('/mock/project');
expect(mockService.setup).toHaveBeenCalledWith('my-feature');
expect(process.chdir).toHaveBeenCalledWith(
'/mock/project/.gemini/worktrees/my-feature',
@ -99,7 +95,7 @@ describe('setupWorktree', () => {
await setupWorktree('my-feature');
expect(coreFunctions.createWorktreeService).not.toHaveBeenCalled();
expect(createWorktreeService).not.toHaveBeenCalled();
expect(process.chdir).not.toHaveBeenCalled();
});
@ -112,7 +108,7 @@ describe('setupWorktree', () => {
await expect(setupWorktree('my-feature')).rejects.toThrow('PROCESS_EXIT');
expect(coreFunctions.writeToStderr).toHaveBeenCalledWith(
expect(writeToStderr).toHaveBeenCalledWith(
expect.stringContaining(
'Failed to create or switch to worktree: Git failure',
),