From ba14e9e531f2331695a6c0560b13aabdd2da151e Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Tue, 20 Jan 2026 16:43:04 +0800 Subject: [PATCH] add settings command and update extension examples --- packages/cli/src/commands/extensions.tsx | 2 + .../extensions/examples/agent/agents/diary.md | 87 +++++ .../examples/agent/qwen-extension.json | 4 + .../commands/fs/grep-code.md | 0 .../examples/commands/qwen-extension.json | 4 + .../custom-commands/qwen-extension.json | 4 - .../exclude-tools/qwen-extension.json | 4 +- .../examples/mcp-server/package.json | 2 +- .../examples/skills/qwen-extension.json | 4 + .../examples/skills/skills/synonyms/SKILL.md | 48 +++ .../src/commands/extensions/settings.test.ts | 345 ++++++++++++++++++ .../cli/src/commands/extensions/settings.ts | 148 ++++++++ .../core/src/extension/extensionManager.ts | 18 +- .../core/src/extension/extensionSettings.ts | 14 + .../core/src/extension/gemini-converter.ts | 3 +- packages/core/src/extension/index.ts | 1 + packages/core/src/extension/settings.test.ts | 2 +- packages/core/src/extension/settings.ts | 2 +- scripts/prepare-package.js | 40 ++ 19 files changed, 708 insertions(+), 24 deletions(-) create mode 100644 packages/cli/src/commands/extensions/examples/agent/agents/diary.md create mode 100644 packages/cli/src/commands/extensions/examples/agent/qwen-extension.json rename packages/cli/src/commands/extensions/examples/{custom-commands => commands}/commands/fs/grep-code.md (100%) create mode 100644 packages/cli/src/commands/extensions/examples/commands/qwen-extension.json delete mode 100644 packages/cli/src/commands/extensions/examples/custom-commands/qwen-extension.json create mode 100644 packages/cli/src/commands/extensions/examples/skills/qwen-extension.json create mode 100644 packages/cli/src/commands/extensions/examples/skills/skills/synonyms/SKILL.md create mode 100644 packages/cli/src/commands/extensions/settings.test.ts create mode 100644 packages/cli/src/commands/extensions/settings.ts diff --git a/packages/cli/src/commands/extensions.tsx b/packages/cli/src/commands/extensions.tsx index 12b49e894..a69a1d85b 100644 --- a/packages/cli/src/commands/extensions.tsx +++ b/packages/cli/src/commands/extensions.tsx @@ -13,6 +13,7 @@ import { disableCommand } from './extensions/disable.js'; import { enableCommand } from './extensions/enable.js'; import { linkCommand } from './extensions/link.js'; import { newCommand } from './extensions/new.js'; +import { settingsCommand } from './extensions/settings.js'; export const extensionsCommand: CommandModule = { command: 'extensions ', @@ -27,6 +28,7 @@ export const extensionsCommand: CommandModule = { .command(enableCommand) .command(linkCommand) .command(newCommand) + .command(settingsCommand) .demandCommand(1, 'You need at least one command before continuing.') .version(false), handler: () => { diff --git a/packages/cli/src/commands/extensions/examples/agent/agents/diary.md b/packages/cli/src/commands/extensions/examples/agent/agents/diary.md new file mode 100644 index 000000000..8c0c76a91 --- /dev/null +++ b/packages/cli/src/commands/extensions/examples/agent/agents/diary.md @@ -0,0 +1,87 @@ +--- +name: diary-writer +description: generate a diary for user +color: yellow +tools: + - Glob + - Grep + - ListFiles + - ReadFile + - ReadManyFiles + - NotebookRead + - WebFetch + - TodoWrite + - WebSearch +modelConfig: + model: qwen3-coder-plus +--- + +You are a personal diary writing assistant who helps users capture their daily experiences, thoughts, and reflections in meaningful journal entries. + +## Core Mission + +Help users create thoughtful, well-structured diary entries that preserve their memories, emotions, and personal growth moments. + +## Writing Style + +**Tone & Voice** + +- Warm, personal, and authentic +- Reflective and introspective +- Supportive without being overly sentimental +- Adapt to user's preferred style (casual, formal, poetic, etc.) + +**Structure Options** + +- Free-form narrative +- Bullet-point highlights +- Gratitude-focused entries +- Goal and achievement tracking +- Emotional processing format + +## Capabilities + +**1. Daily Entry Creation** + +- Transform user's brief notes into full diary entries +- Expand on key moments with descriptive details +- Add context about weather, mood, or setting when relevant +- Include meaningful quotes or observations + +**2. Reflection Prompts** + +- Ask thoughtful questions to deepen entries +- Suggest areas worth exploring further +- Help identify patterns in thoughts and behaviors +- Encourage gratitude and positive reflection + +**3. Memory Enhancement** + +- Help recall specific details from the day +- Connect current events to past experiences +- Highlight personal growth and progress +- Preserve important conversations or interactions + +**4. Organization** + +- Suggest tags or themes for entries +- Create summaries for weekly/monthly reviews +- Track recurring topics or goals +- Maintain consistency in formatting + +## Guidelines + +- **Privacy First**: Treat all content as deeply personal and confidential +- **User's Voice**: Write in a way that sounds like the user, not generic +- **No Judgment**: Accept all emotions and experiences without criticism +- **Encourage Honesty**: Create a safe space for authentic expression +- **Balance**: Mix facts with feelings, events with reflections + +## Output Format + +When creating a diary entry, include: + +1. **Date & Title** (optional creative title) +2. **Main Content** - The narrative or bullet points +3. **Reflection** - A brief closing thought or takeaway +4. **Tags** (optional) - For organization and future reference diff --git a/packages/cli/src/commands/extensions/examples/agent/qwen-extension.json b/packages/cli/src/commands/extensions/examples/agent/qwen-extension.json new file mode 100644 index 000000000..a9a8e8a68 --- /dev/null +++ b/packages/cli/src/commands/extensions/examples/agent/qwen-extension.json @@ -0,0 +1,4 @@ +{ + "name": "agent-example", + "version": "1.0.0" +} diff --git a/packages/cli/src/commands/extensions/examples/custom-commands/commands/fs/grep-code.md b/packages/cli/src/commands/extensions/examples/commands/commands/fs/grep-code.md similarity index 100% rename from packages/cli/src/commands/extensions/examples/custom-commands/commands/fs/grep-code.md rename to packages/cli/src/commands/extensions/examples/commands/commands/fs/grep-code.md diff --git a/packages/cli/src/commands/extensions/examples/commands/qwen-extension.json b/packages/cli/src/commands/extensions/examples/commands/qwen-extension.json new file mode 100644 index 000000000..277a40548 --- /dev/null +++ b/packages/cli/src/commands/extensions/examples/commands/qwen-extension.json @@ -0,0 +1,4 @@ +{ + "name": "commands-example", + "version": "1.0.0" +} diff --git a/packages/cli/src/commands/extensions/examples/custom-commands/qwen-extension.json b/packages/cli/src/commands/extensions/examples/custom-commands/qwen-extension.json deleted file mode 100644 index d973ab8fe..000000000 --- a/packages/cli/src/commands/extensions/examples/custom-commands/qwen-extension.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "custom-commands", - "version": "1.0.0" -} diff --git a/packages/cli/src/commands/extensions/examples/exclude-tools/qwen-extension.json b/packages/cli/src/commands/extensions/examples/exclude-tools/qwen-extension.json index 5023fb7ad..584b6abc0 100644 --- a/packages/cli/src/commands/extensions/examples/exclude-tools/qwen-extension.json +++ b/packages/cli/src/commands/extensions/examples/exclude-tools/qwen-extension.json @@ -1,5 +1,5 @@ { - "name": "excludeTools", + "name": "exclude-tools-example", "version": "1.0.0", - "excludeTools": ["run_shell_command(rm -rf)"] + "excludeTools": ["Shell(rm -rf)"] } diff --git a/packages/cli/src/commands/extensions/examples/mcp-server/package.json b/packages/cli/src/commands/extensions/examples/mcp-server/package.json index d38f7ee99..59c1c45c1 100644 --- a/packages/cli/src/commands/extensions/examples/mcp-server/package.json +++ b/packages/cli/src/commands/extensions/examples/mcp-server/package.json @@ -1,7 +1,7 @@ { "name": "mcp-server-example", "version": "1.0.0", - "description": "Example MCP Server for Gemini CLI Extension", + "description": "Example MCP Server for Qwen Code Extension", "type": "module", "main": "example.js", "scripts": { diff --git a/packages/cli/src/commands/extensions/examples/skills/qwen-extension.json b/packages/cli/src/commands/extensions/examples/skills/qwen-extension.json new file mode 100644 index 000000000..2674ef9e0 --- /dev/null +++ b/packages/cli/src/commands/extensions/examples/skills/qwen-extension.json @@ -0,0 +1,4 @@ +{ + "name": "skills-example", + "version": "1.0.0" +} diff --git a/packages/cli/src/commands/extensions/examples/skills/skills/synonyms/SKILL.md b/packages/cli/src/commands/extensions/examples/skills/skills/synonyms/SKILL.md new file mode 100644 index 000000000..ed2878771 --- /dev/null +++ b/packages/cli/src/commands/extensions/examples/skills/skills/synonyms/SKILL.md @@ -0,0 +1,48 @@ +--- +name: synonyms +description: Generate synonyms for words or phrases. Use this skill when the user needs alternative words with similar meanings, wants to expand vocabulary, or seeks varied expressions for writing. +license: Complete terms in LICENSE.txt +--- + +This skill helps generate synonyms and alternative expressions for given words or phrases. It provides contextually appropriate alternatives to enhance vocabulary and improve writing variety. + +The user provides a word, phrase, or sentence where they need synonym suggestions. They may specify the context, tone, or formality level desired. + +## Synonym Generation Guidelines + +When generating synonyms, consider: + +- **Context**: The specific domain or situation where the word will be used +- **Tone**: Formal, informal, neutral, academic, conversational, etc. +- **Nuance**: Subtle differences in meaning between similar words +- **Register**: Appropriate level of formality for the intended audience + +## Output Format + +For each input word or phrase, provide: + +1. **Direct Synonyms**: Words with nearly identical meanings +2. **Related Alternatives**: Words with similar but slightly different connotations +3. **Context Examples**: Brief usage examples when helpful + +## Best Practices + +- Prioritize commonly used synonyms over obscure alternatives +- Note any subtle differences in meaning or usage +- Consider regional variations when relevant +- Indicate formality levels (formal/informal/neutral) +- Provide multiple options to give users choices + +## Example + +**Input**: "happy" + +**Synonyms**: + +- **Direct**: joyful, cheerful, delighted, pleased, content +- **Informal**: thrilled, stoked, over the moon +- **Formal**: elated, gratified, blissful +- **Subtle variations**: + - _content_ - peaceful satisfaction + - _ecstatic_ - intense, overwhelming happiness + - _cheerful_ - outwardly expressing happiness diff --git a/packages/cli/src/commands/extensions/settings.test.ts b/packages/cli/src/commands/extensions/settings.test.ts new file mode 100644 index 000000000..042965eec --- /dev/null +++ b/packages/cli/src/commands/extensions/settings.test.ts @@ -0,0 +1,345 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + describe, + it, + expect, + vi, + beforeEach, + type MockInstance, +} from 'vitest'; +import { settingsCommand } from './settings.js'; +import yargs from 'yargs'; + +const mockGetLoadedExtensions = vi.hoisted(() => vi.fn()); +const mockGetScopedEnvContents = vi.hoisted(() => vi.fn()); +const mockUpdateSetting = vi.hoisted(() => vi.fn()); +const mockPromptForSetting = vi.hoisted(() => vi.fn()); + +vi.mock('./utils.js', () => ({ + getExtensionManager: vi.fn().mockResolvedValue({ + getLoadedExtensions: mockGetLoadedExtensions, + }), +})); + +vi.mock('@qwen-code/qwen-code-core', () => ({ + ExtensionSettingScope: { + USER: 'user', + WORKSPACE: 'workspace', + }, + getScopedEnvContents: mockGetScopedEnvContents, + promptForSetting: mockPromptForSetting, + updateSetting: mockUpdateSetting, +})); + +describe('extensions settings command', () => { + it('should fail if no subcommand is provided', () => { + const validationParser = yargs([]) + .command(settingsCommand) + .fail(false) + .locale('en'); + expect(() => validationParser.parse('settings')).toThrow( + 'Not enough non-option arguments: got 0, need at least 1', + ); + }); + + it('should register set subcommand', () => { + const parser = yargs([]).command(settingsCommand).fail(false).locale('en'); + expect(() => parser.parse('settings set')).toThrow( + 'Not enough non-option arguments', + ); + }); + + it('should register list subcommand', () => { + const parser = yargs([]).command(settingsCommand).fail(false).locale('en'); + expect(() => parser.parse('settings list')).toThrow( + 'Not enough non-option arguments', + ); + }); + + it('should accept set command with name and setting', () => { + const parser = yargs([]).command(settingsCommand).fail(false).locale('en'); + expect(() => + parser.parse('settings set my-extension API_KEY'), + ).not.toThrow(); + }); + + it('should accept set command with scope option', () => { + const parser = yargs([]).command(settingsCommand).fail(false).locale('en'); + expect(() => + parser.parse('settings set my-extension API_KEY --scope=workspace'), + ).not.toThrow(); + }); + + it('should fail set command with invalid scope', () => { + const parser = yargs([]).command(settingsCommand).fail(false).locale('en'); + expect(() => + parser.parse('settings set my-extension API_KEY --scope=invalid'), + ).toThrow(); + }); + + it('should accept list command with name', () => { + const parser = yargs([]).command(settingsCommand).fail(false).locale('en'); + expect(() => parser.parse('settings list my-extension')).not.toThrow(); + }); +}); + +describe('settings set handler', () => { + let consoleLogSpy: MockInstance; + + beforeEach(() => { + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + vi.clearAllMocks(); + }); + + it('should return early if extension manager is not available', async () => { + const { getExtensionManager } = await import('./utils.js'); + vi.mocked(getExtensionManager).mockResolvedValueOnce(null as never); + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync('settings set my-extension API_KEY'); + + expect(mockUpdateSetting).not.toHaveBeenCalled(); + }); + + it('should return early if no extensions are loaded', async () => { + mockGetLoadedExtensions.mockReturnValueOnce([]); + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync('settings set my-extension API_KEY'); + + expect(mockUpdateSetting).not.toHaveBeenCalled(); + }); + + it('should log error if extension is not found', async () => { + mockGetLoadedExtensions.mockReturnValueOnce([ + { name: 'other-extension', id: 'other-id', config: {} }, + ]); + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync('settings set my-extension API_KEY'); + + expect(consoleLogSpy).toHaveBeenCalledWith( + 'Extension "my-extension" not found.', + ); + expect(mockUpdateSetting).not.toHaveBeenCalled(); + }); + + it('should call updateSetting with correct arguments for user scope', async () => { + const mockExtension = { + name: 'my-extension', + id: 'ext-id-123', + config: { name: 'my-extension', settings: [] }, + }; + mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]); + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync('settings set my-extension API_KEY'); + + expect(mockUpdateSetting).toHaveBeenCalledWith( + mockExtension.config, + mockExtension.id, + 'API_KEY', + mockPromptForSetting, + 'user', + ); + }); + + it('should call updateSetting with workspace scope when specified', async () => { + const mockExtension = { + name: 'my-extension', + id: 'ext-id-123', + config: { name: 'my-extension', settings: [] }, + }; + mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]); + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync( + 'settings set my-extension API_KEY --scope=workspace', + ); + + expect(mockUpdateSetting).toHaveBeenCalledWith( + mockExtension.config, + mockExtension.id, + 'API_KEY', + mockPromptForSetting, + 'workspace', + ); + }); +}); + +describe('settings list handler', () => { + let consoleLogSpy: MockInstance; + + beforeEach(() => { + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + vi.clearAllMocks(); + }); + + it('should return early if extension manager is not available', async () => { + const { getExtensionManager } = await import('./utils.js'); + vi.mocked(getExtensionManager).mockResolvedValueOnce(null as never); + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync('settings list my-extension'); + + expect(mockGetScopedEnvContents).not.toHaveBeenCalled(); + }); + + it('should return early if no extensions are loaded', async () => { + mockGetLoadedExtensions.mockReturnValueOnce([]); + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync('settings list my-extension'); + + expect(mockGetScopedEnvContents).not.toHaveBeenCalled(); + }); + + it('should log error if extension is not found', async () => { + mockGetLoadedExtensions.mockReturnValueOnce([ + { name: 'other-extension', id: 'other-id', config: {} }, + ]); + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync('settings list my-extension'); + + expect(consoleLogSpy).toHaveBeenCalledWith( + 'Extension "my-extension" not found.', + ); + }); + + it('should log message if extension has no settings', async () => { + const mockExtension = { + name: 'my-extension', + id: 'ext-id-123', + config: { name: 'my-extension' }, + settings: [], + }; + mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]); + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync('settings list my-extension'); + + expect(consoleLogSpy).toHaveBeenCalledWith( + 'Extension "my-extension" has no settings to configure.', + ); + }); + + it('should list settings with their values', async () => { + const mockExtension = { + name: 'my-extension', + id: 'ext-id-123', + config: { name: 'my-extension' }, + settings: [ + { + name: 'API Key', + envVar: 'API_KEY', + description: 'Your API key', + sensitive: false, + }, + { + name: 'Secret Token', + envVar: 'SECRET_TOKEN', + description: 'A secret token', + sensitive: true, + }, + ], + }; + mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]); + mockGetScopedEnvContents + .mockResolvedValueOnce({ API_KEY: 'my-api-key' }) // user scope + .mockResolvedValueOnce({}); // workspace scope + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync('settings list my-extension'); + + expect(consoleLogSpy).toHaveBeenCalledWith('Settings for "my-extension":'); + expect(consoleLogSpy).toHaveBeenCalledWith('\n- API Key (API_KEY)'); + expect(consoleLogSpy).toHaveBeenCalledWith(' Description: Your API key'); + expect(consoleLogSpy).toHaveBeenCalledWith(' Value: my-api-key (user)'); + }); + + it('should show workspace scope for workspace-scoped settings', async () => { + const mockExtension = { + name: 'my-extension', + id: 'ext-id-123', + config: { name: 'my-extension' }, + settings: [ + { + name: 'API Key', + envVar: 'API_KEY', + description: 'Your API key', + sensitive: false, + }, + ], + }; + mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]); + mockGetScopedEnvContents + .mockResolvedValueOnce({ API_KEY: 'user-value' }) // user scope + .mockResolvedValueOnce({ API_KEY: 'workspace-value' }); // workspace scope + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync('settings list my-extension'); + + // Workspace should override user, and show (workspace) scope + expect(consoleLogSpy).toHaveBeenCalledWith( + ' Value: workspace-value (workspace)', + ); + }); + + it('should show [not set] for undefined settings', async () => { + const mockExtension = { + name: 'my-extension', + id: 'ext-id-123', + config: { name: 'my-extension' }, + settings: [ + { + name: 'API Key', + envVar: 'API_KEY', + description: 'Your API key', + sensitive: false, + }, + ], + }; + mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]); + mockGetScopedEnvContents + .mockResolvedValueOnce({}) // user scope + .mockResolvedValueOnce({}); // workspace scope + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync('settings list my-extension'); + + expect(consoleLogSpy).toHaveBeenCalledWith(' Value: [not set]'); + }); + + it('should show [value stored in keychain] for sensitive settings', async () => { + const mockExtension = { + name: 'my-extension', + id: 'ext-id-123', + config: { name: 'my-extension' }, + settings: [ + { + name: 'Secret Token', + envVar: 'SECRET_TOKEN', + description: 'A secret token', + sensitive: true, + }, + ], + }; + mockGetLoadedExtensions.mockReturnValueOnce([mockExtension]); + mockGetScopedEnvContents + .mockResolvedValueOnce({ SECRET_TOKEN: 'secret-value' }) // user scope + .mockResolvedValueOnce({}); // workspace scope + + const parser = yargs([]).command(settingsCommand); + await parser.parseAsync('settings list my-extension'); + + expect(consoleLogSpy).toHaveBeenCalledWith( + ' Value: [value stored in keychain] (user)', + ); + }); +}); diff --git a/packages/cli/src/commands/extensions/settings.ts b/packages/cli/src/commands/extensions/settings.ts new file mode 100644 index 000000000..edcdadfd9 --- /dev/null +++ b/packages/cli/src/commands/extensions/settings.ts @@ -0,0 +1,148 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { CommandModule } from 'yargs'; +import { getExtensionManager } from './utils.js'; +import { + ExtensionSettingScope, + getScopedEnvContents, + promptForSetting, + updateSetting, +} from '@qwen-code/qwen-code-core'; + +// --- SET COMMAND --- +interface SetArgs { + name: string; + setting: string; + scope: string; +} + +const setCommand: CommandModule = { + command: 'set [--scope] ', + describe: 'Set a specific setting for an extension.', + builder: (yargs) => + yargs + .positional('name', { + describe: 'Name of the extension to configure.', + type: 'string', + demandOption: true, + }) + .positional('setting', { + describe: 'The setting to configure (name or env var).', + type: 'string', + demandOption: true, + }) + .option('scope', { + describe: 'The scope to set the setting in.', + type: 'string', + choices: ['user', 'workspace'], + default: 'user', + }), + handler: async (args) => { + const { name, setting, scope } = args; + const extensionManager = await getExtensionManager(); + if (!extensionManager) return; + const extensions = extensionManager.getLoadedExtensions(); + if (!extensions || extensions.length === 0) return; + const extension = extensions.find((e) => e.name === name); + if (!extension) { + console.log(`Extension "${name}" not found.`); + return; + } + await updateSetting( + extension.config, + extension.id, + setting, + promptForSetting, + scope as ExtensionSettingScope, + ); + }, +}; + +// --- LIST COMMAND --- +interface ListArgs { + name: string; +} + +const listCommand: CommandModule = { + command: 'list ', + describe: 'List all settings for an extension.', + builder: (yargs) => + yargs.positional('name', { + describe: 'Name of the extension.', + type: 'string', + demandOption: true, + }), + handler: async (args) => { + const { name } = args; + const extensionManager = await getExtensionManager(); + if (!extensionManager) return; + const extensions = extensionManager.getLoadedExtensions(); + if (!extensions || extensions.length === 0) return; + const extension = extensions.find((e) => e.name === name); + if (!extension) { + console.log(`Extension "${name}" not found.`); + return; + } + if (!extension || !extension.settings || extension.settings.length === 0) { + console.log(`Extension "${name}" has no settings to configure.`); + return; + } + + const userSettings = await getScopedEnvContents( + extension.config, + extension.id, + ExtensionSettingScope.USER, + ); + const workspaceSettings = await getScopedEnvContents( + extension.config, + extension.id, + ExtensionSettingScope.WORKSPACE, + ); + const mergedSettings = { ...userSettings, ...workspaceSettings }; + + console.log(`Settings for "${name}":`); + for (const setting of extension.settings) { + const value = mergedSettings[setting.envVar]; + let displayValue: string; + let scopeInfo = ''; + + if (workspaceSettings[setting.envVar] !== undefined) { + scopeInfo = ' (workspace)'; + } else if (userSettings[setting.envVar] !== undefined) { + scopeInfo = ' (user)'; + } + + if (value === undefined) { + displayValue = '[not set]'; + } else if (setting.sensitive) { + displayValue = '[value stored in keychain]'; + } else { + displayValue = value; + } + console.log(` +- ${setting.name} (${setting.envVar})`); + console.log(` Description: ${setting.description}`); + console.log(` Value: ${displayValue}${scopeInfo}`); + } + }, +}; + +// --- SETTINGS COMMAND --- +export const settingsCommand: CommandModule = { + command: 'settings ', + describe: 'Manage extension settings.', + builder: (yargs) => + yargs + .command(setCommand) + .command(listCommand) + .demandCommand(1, 'You need to specify a command (set or list).') + .version(false), + handler: () => { + // This handler is not called when a subcommand is provided. + // Yargs will show the help menu. + }, +}; diff --git a/packages/core/src/extension/extensionManager.ts b/packages/core/src/extension/extensionManager.ts index 6a5c88d1c..93a4d3577 100644 --- a/packages/core/src/extension/extensionManager.ts +++ b/packages/core/src/extension/extensionManager.ts @@ -50,6 +50,10 @@ import { maybePromptForSettings, promptForSetting, } from './extensionSettings.js'; +import type { + ExtensionSetting, + ResolvedExtensionSetting, +} from './extensionSettings.js'; import type { TelemetrySettings } from '../config/config.js'; import { logExtensionUpdateEvent } from '../telemetry/loggers.js'; import { @@ -106,20 +110,6 @@ export interface ExtensionConfig { settings?: ExtensionSetting[]; } -export interface ExtensionSetting { - name: string; - description: string; - envVar: string; - sensitive?: boolean; -} - -export interface ResolvedExtensionSetting { - name: string; - envVar: string; - value: string; - sensitive: boolean; -} - export interface ExtensionUpdateInfo { name: string; originalVersion: string; diff --git a/packages/core/src/extension/extensionSettings.ts b/packages/core/src/extension/extensionSettings.ts index d35573d52..e821788ba 100644 --- a/packages/core/src/extension/extensionSettings.ts +++ b/packages/core/src/extension/extensionSettings.ts @@ -14,6 +14,20 @@ import prompts from 'prompts'; import { EXTENSION_SETTINGS_FILENAME } from './variables.js'; import { KeychainTokenStorage } from '../mcp/token-storage/keychain-token-storage.js'; +export interface ExtensionSetting { + name: string; + description: string; + envVar: string; + sensitive?: boolean; +} + +export interface ResolvedExtensionSetting { + name: string; + envVar: string; + value: string; + sensitive: boolean; +} + export enum ExtensionSettingScope { USER = 'user', WORKSPACE = 'workspace', diff --git a/packages/core/src/extension/gemini-converter.ts b/packages/core/src/extension/gemini-converter.ts index c3dee4966..fa5ad499e 100644 --- a/packages/core/src/extension/gemini-converter.ts +++ b/packages/core/src/extension/gemini-converter.ts @@ -11,7 +11,8 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { glob } from 'glob'; -import type { ExtensionConfig, ExtensionSetting } from './extensionManager.js'; +import type { ExtensionConfig } from './extensionManager.js'; +import type { ExtensionSetting } from './extensionSettings.js'; import { ExtensionStorage } from './storage.js'; import { convertTomlToMarkdown } from '../utils/toml-to-markdown-converter.js'; diff --git a/packages/core/src/extension/index.ts b/packages/core/src/extension/index.ts index dd4b413fb..2940d5847 100644 --- a/packages/core/src/extension/index.ts +++ b/packages/core/src/extension/index.ts @@ -1,3 +1,4 @@ export * from './extensionManager.js'; export * from './variables.js'; export * from './github.js'; +export * from './extensionSettings.js'; diff --git a/packages/core/src/extension/settings.test.ts b/packages/core/src/extension/settings.test.ts index cef81dd01..3a2c08c57 100644 --- a/packages/core/src/extension/settings.test.ts +++ b/packages/core/src/extension/settings.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect } from 'vitest'; import { parseEnvFile, generateEnvFile, validateSettings } from './settings.js'; -import type { ExtensionSetting } from './extensionManager.js'; +import type { ExtensionSetting } from './extensionSettings.js'; describe('Extension Settings', () => { describe('parseEnvFile', () => { diff --git a/packages/core/src/extension/settings.ts b/packages/core/src/extension/settings.ts index 522ffd091..a31436c90 100644 --- a/packages/core/src/extension/settings.ts +++ b/packages/core/src/extension/settings.ts @@ -11,7 +11,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import type { ExtensionSetting } from './extensionManager.js'; +import type { ExtensionSetting } from './extensionSettings.js'; /** * Parse .env file content into key-value pairs. diff --git a/scripts/prepare-package.js b/scripts/prepare-package.js index 534f104c8..ac8862686 100644 --- a/scripts/prepare-package.js +++ b/scripts/prepare-package.js @@ -93,6 +93,46 @@ if (fs.existsSync(localesSourceDir)) { console.warn(`Warning: locales folder not found at ${localesSourceDir}`); } +// Copy extensions folder +console.log('Copying extension examples folder...'); +const extensionExamplesDir = path.join( + rootDir, + 'packages', + 'cli', + 'src', + 'commands', + 'extensions', + 'examples', +); +const extensionExamplesDestDir = path.join(distDir, 'examples'); + +if (fs.existsSync(extensionExamplesDir)) { + // Recursive copy function + function copyRecursiveSync(src, dest) { + const stats = fs.statSync(src); + if (stats.isDirectory()) { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + const entries = fs.readdirSync(src); + for (const entry of entries) { + const srcPath = path.join(src, entry); + const destPath = path.join(dest, entry); + copyRecursiveSync(srcPath, destPath); + } + } else { + fs.copyFileSync(src, dest); + } + } + + copyRecursiveSync(extensionExamplesDir, extensionExamplesDestDir); + console.log('Copied extension examples folder'); +} else { + console.warn( + `Warning: extension examples folder not found at ${extensionExamplesDir}`, + ); +} + // Copy package.json from root and modify it for publishing console.log('Creating package.json for distribution...'); const rootPackageJson = JSON.parse(