diff options
Diffstat (limited to 'packages/cli/src/config')
| -rw-r--r-- | packages/cli/src/config/config.test.ts | 243 | ||||
| -rw-r--r-- | packages/cli/src/config/config.ts | 26 |
2 files changed, 227 insertions, 42 deletions
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index f002fd84..08a85e4d 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as os from 'os'; -import { loadCliConfig } from './config.js'; +import { loadCliConfig, parseArguments } from './config.js'; import { Settings } from './settings.js'; import { Extension } from './extension.js'; import * as ServerConfig from '@google/gemini-cli-core'; @@ -46,6 +46,100 @@ vi.mock('@google/gemini-cli-core', async () => { }; }); +describe('parseArguments', () => { + const originalArgv = process.argv; + + afterEach(() => { + process.argv = originalArgv; + }); + + it('should throw an error when both --prompt and --prompt-interactive are used together', async () => { + process.argv = [ + 'node', + 'script.js', + '--prompt', + 'test prompt', + '--prompt-interactive', + 'interactive prompt', + ]; + + const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit called'); + }); + + const mockConsoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + await expect(parseArguments()).rejects.toThrow('process.exit called'); + + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining( + 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together', + ), + ); + + mockExit.mockRestore(); + mockConsoleError.mockRestore(); + }); + + it('should throw an error when using short flags -p and -i together', async () => { + process.argv = [ + 'node', + 'script.js', + '-p', + 'test prompt', + '-i', + 'interactive prompt', + ]; + + const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit called'); + }); + + const mockConsoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + await expect(parseArguments()).rejects.toThrow('process.exit called'); + + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining( + 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together', + ), + ); + + mockExit.mockRestore(); + mockConsoleError.mockRestore(); + }); + + it('should allow --prompt without --prompt-interactive', async () => { + process.argv = ['node', 'script.js', '--prompt', 'test prompt']; + const argv = await parseArguments(); + expect(argv.prompt).toBe('test prompt'); + expect(argv.promptInteractive).toBeUndefined(); + }); + + it('should allow --prompt-interactive without --prompt', async () => { + process.argv = [ + 'node', + 'script.js', + '--prompt-interactive', + 'interactive prompt', + ]; + const argv = await parseArguments(); + expect(argv.promptInteractive).toBe('interactive prompt'); + expect(argv.prompt).toBeUndefined(); + }); + + it('should allow -i flag as alias for --prompt-interactive', async () => { + process.argv = ['node', 'script.js', '-i', 'interactive prompt']; + const argv = await parseArguments(); + expect(argv.promptInteractive).toBe('interactive prompt'); + expect(argv.prompt).toBeUndefined(); + }); +}); + describe('loadCliConfig', () => { const originalArgv = process.argv; const originalEnv = { ...process.env }; @@ -64,29 +158,33 @@ describe('loadCliConfig', () => { it('should set showMemoryUsage to true when --show-memory-usage flag is present', async () => { process.argv = ['node', 'script.js', '--show-memory-usage']; + const argv = await parseArguments(); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getShowMemoryUsage()).toBe(true); }); it('should set showMemoryUsage to false when --memory flag is not present', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getShowMemoryUsage()).toBe(false); }); it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { showMemoryUsage: false }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getShowMemoryUsage()).toBe(false); }); it('should prioritize CLI flag over settings for showMemoryUsage (CLI true, settings false)', async () => { process.argv = ['node', 'script.js', '--show-memory-usage']; + const argv = await parseArguments(); const settings: Settings = { showMemoryUsage: false }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getShowMemoryUsage()).toBe(true); }); }); @@ -109,59 +207,67 @@ describe('loadCliConfig telemetry', () => { it('should set telemetry to false by default when no flag or setting is present', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(false); }); it('should set telemetry to true when --telemetry flag is present', async () => { process.argv = ['node', 'script.js', '--telemetry']; + const argv = await parseArguments(); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(true); }); it('should set telemetry to false when --no-telemetry flag is present', async () => { process.argv = ['node', 'script.js', '--no-telemetry']; + const argv = await parseArguments(); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(false); }); it('should use telemetry value from settings if CLI flag is not present (settings true)', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(true); }); it('should use telemetry value from settings if CLI flag is not present (settings false)', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: false } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(false); }); it('should prioritize --telemetry CLI flag (true) over settings (false)', async () => { process.argv = ['node', 'script.js', '--telemetry']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: false } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(true); }); it('should prioritize --no-telemetry CLI flag (false) over settings (true)', async () => { process.argv = ['node', 'script.js', '--no-telemetry']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(false); }); it('should use telemetry OTLP endpoint from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { otlpEndpoint: 'http://settings.example.com' }, }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryOtlpEndpoint()).toBe( 'http://settings.example.com', ); @@ -174,26 +280,29 @@ describe('loadCliConfig telemetry', () => { '--telemetry-otlp-endpoint', 'http://cli.example.com', ]; + const argv = await parseArguments(); const settings: Settings = { telemetry: { otlpEndpoint: 'http://settings.example.com' }, }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryOtlpEndpoint()).toBe('http://cli.example.com'); }); it('should use default endpoint if no OTLP endpoint is provided via CLI or settings', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryOtlpEndpoint()).toBe('http://localhost:4317'); }); it('should use telemetry target from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET }, }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryTarget()).toBe( ServerConfig.DEFAULT_TELEMETRY_TARGET, ); @@ -201,17 +310,19 @@ describe('loadCliConfig telemetry', () => { it('should prioritize --telemetry-target CLI flag over settings', async () => { process.argv = ['node', 'script.js', '--telemetry-target', 'gcp']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET }, }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryTarget()).toBe('gcp'); }); it('should use default target if no target is provided via CLI or settings', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryTarget()).toBe( ServerConfig.DEFAULT_TELEMETRY_TARGET, ); @@ -219,29 +330,33 @@ describe('loadCliConfig telemetry', () => { it('should use telemetry log prompts from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { logPrompts: false } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(false); }); it('should prioritize --telemetry-log-prompts CLI flag (true) over settings (false)', async () => { process.argv = ['node', 'script.js', '--telemetry-log-prompts']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { logPrompts: false } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(true); }); it('should prioritize --no-telemetry-log-prompts CLI flag (false) over settings (true)', async () => { process.argv = ['node', 'script.js', '--no-telemetry-log-prompts']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { logPrompts: true } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(false); }); it('should use default log prompts (true) if no value is provided via CLI or settings', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(true); }); }); @@ -286,7 +401,8 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { ], }, ]; - await loadCliConfig(settings, extensions, 'session-id'); + const argv = await parseArguments(); + await loadCliConfig(settings, extensions, 'session-id', argv); expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith( expect.any(String), false, @@ -346,7 +462,9 @@ describe('mergeMcpServers', () => { }, ]; const originalSettings = JSON.parse(JSON.stringify(settings)); - await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + await loadCliConfig(settings, extensions, 'test-session', argv); expect(settings).toEqual(originalSettings); }); }); @@ -372,7 +490,14 @@ describe('mergeExcludeTools', () => { contextFiles: [], }, ]; - const config = await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); expect(config.getExcludeTools()).toEqual( expect.arrayContaining(['tool1', 'tool2', 'tool3', 'tool4', 'tool5']), ); @@ -391,7 +516,14 @@ describe('mergeExcludeTools', () => { contextFiles: [], }, ]; - const config = await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); expect(config.getExcludeTools()).toEqual( expect.arrayContaining(['tool1', 'tool2', 'tool3']), ); @@ -418,7 +550,14 @@ describe('mergeExcludeTools', () => { contextFiles: [], }, ]; - const config = await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); expect(config.getExcludeTools()).toEqual( expect.arrayContaining(['tool1', 'tool2', 'tool3', 'tool4']), ); @@ -428,14 +567,28 @@ describe('mergeExcludeTools', () => { it('should return an empty array when no excludeTools are specified', async () => { const settings: Settings = {}; const extensions: Extension[] = []; - const config = await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); expect(config.getExcludeTools()).toEqual([]); }); it('should handle settings with excludeTools but no extensions', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { excludeTools: ['tool1', 'tool2'] }; const extensions: Extension[] = []; - const config = await loadCliConfig(settings, extensions, 'test-session'); + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); expect(config.getExcludeTools()).toEqual( expect.arrayContaining(['tool1', 'tool2']), ); @@ -454,7 +607,14 @@ describe('mergeExcludeTools', () => { contextFiles: [], }, ]; - const config = await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); expect(config.getExcludeTools()).toEqual( expect.arrayContaining(['tool1', 'tool2']), ); @@ -474,7 +634,9 @@ describe('mergeExcludeTools', () => { }, ]; const originalSettings = JSON.parse(JSON.stringify(settings)); - await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + await loadCliConfig(settings, extensions, 'test-session', argv); expect(settings).toEqual(originalSettings); }); }); @@ -505,7 +667,8 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { it('should allow all MCP servers if the flag is not provided', async () => { process.argv = ['node', 'script.js']; - const config = await loadCliConfig(baseSettings, [], 'test-session'); + const argv = await parseArguments(); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual(baseSettings.mcpServers); }); @@ -516,7 +679,8 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { '--allowed-mcp-server-names', 'server1', ]; - const config = await loadCliConfig(baseSettings, [], 'test-session'); + const argv = await parseArguments(); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, }); @@ -531,7 +695,8 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { '--allowed-mcp-server-names', 'server3', ]; - const config = await loadCliConfig(baseSettings, [], 'test-session'); + const argv = await parseArguments(); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, server3: { url: 'http://localhost:8082' }, @@ -547,7 +712,8 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { '--allowed-mcp-server-names', 'server4', ]; - const config = await loadCliConfig(baseSettings, [], 'test-session'); + const argv = await parseArguments(); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, }); @@ -555,7 +721,8 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { it('should allow no MCP servers if the flag is provided but empty', async () => { process.argv = ['node', 'script.js', '--allowed-mcp-server-names', '']; - const config = await loadCliConfig(baseSettings, [], 'test-session'); + const argv = await parseArguments(); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({}); }); }); @@ -574,11 +741,13 @@ describe('loadCliConfig extensions', () => { it('should not filter extensions if --extensions flag is not used', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = {}; const config = await loadCliConfig( settings, mockExtensions, 'test-session', + argv, ); expect(config.getExtensionContextFilePaths()).toEqual([ '/path/to/ext1.md', @@ -588,11 +757,13 @@ describe('loadCliConfig extensions', () => { it('should filter extensions if --extensions flag is used', async () => { process.argv = ['node', 'script.js', '--extensions', 'ext1']; + const argv = await parseArguments(); const settings: Settings = {}; const config = await loadCliConfig( settings, mockExtensions, 'test-session', + argv, ); expect(config.getExtensionContextFilePaths()).toEqual(['/path/to/ext1.md']); }); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index b685f090..1c1f0746 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -34,12 +34,13 @@ const logger = { error: (...args: any[]) => console.error('[ERROR]', ...args), }; -interface CliArgs { +export interface CliArgs { model: string | undefined; sandbox: boolean | string | undefined; sandboxImage: string | undefined; debug: boolean | undefined; prompt: string | undefined; + promptInteractive: string | undefined; allFiles: boolean | undefined; all_files: boolean | undefined; showMemoryUsage: boolean | undefined; @@ -55,7 +56,7 @@ interface CliArgs { listExtensions: boolean | undefined; } -async function parseArguments(): Promise<CliArgs> { +export async function parseArguments(): Promise<CliArgs> { const yargsInstance = yargs(hideBin(process.argv)) .scriptName('gemini') .usage( @@ -73,6 +74,12 @@ async function parseArguments(): Promise<CliArgs> { type: 'string', description: 'Prompt. Appended to input on stdin (if any).', }) + .option('prompt-interactive', { + alias: 'i', + type: 'string', + description: + 'Execute the provided prompt and continue in interactive mode', + }) .option('sandbox', { alias: 's', type: 'boolean', @@ -173,10 +180,17 @@ async function parseArguments(): Promise<CliArgs> { .alias('v', 'version') .help() .alias('h', 'help') - .strict(); + .strict() + .check((argv) => { + if (argv.prompt && argv.promptInteractive) { + throw new Error( + 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together', + ); + } + return true; + }); yargsInstance.wrap(yargsInstance.terminalWidth()); - return yargsInstance.argv; } @@ -208,8 +222,8 @@ export async function loadCliConfig( settings: Settings, extensions: Extension[], sessionId: string, + argv: CliArgs, ): Promise<Config> { - const argv = await parseArguments(); const debugMode = argv.debug || [process.env.DEBUG, process.env.DEBUG_MODE].some( @@ -267,7 +281,7 @@ export async function loadCliConfig( sandbox: sandboxConfig, targetDir: process.cwd(), debugMode, - question: argv.prompt || '', + question: argv.promptInteractive || argv.prompt || '', fullContext: argv.allFiles || argv.all_files || false, coreTools: settings.coreTools || undefined, excludeTools, |
