diff --git a/cli/src/commands/agentCommandOptions.test.ts b/cli/src/commands/agentCommandOptions.test.ts new file mode 100644 index 00000000..b8577d14 --- /dev/null +++ b/cli/src/commands/agentCommandOptions.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest' +import { GEMINI_PERMISSION_MODES, OPENCODE_PERMISSION_MODES } from '@hapi/protocol/modes' +import { parseRemoteAgentCommandOptions } from './agentCommandOptions' + +describe('parseRemoteAgentCommandOptions', () => { + it('parses common remote agent flags', () => { + expect(parseRemoteAgentCommandOptions([ + '--started-by', 'runner', + '--hapi-starting-mode', 'remote', + '--permission-mode', 'yolo', + '--resume', 'session-1', + '--model', 'model-a' + ], GEMINI_PERMISSION_MODES)).toEqual({ + startedBy: 'runner', + startingMode: 'remote', + permissionMode: 'yolo', + resumeSessionId: 'session-1', + model: 'model-a' + }) + }) + + it('does not let --yolo override an explicit permission mode that appeared first', () => { + expect(parseRemoteAgentCommandOptions([ + '--permission-mode', 'default', + '--yolo' + ], OPENCODE_PERMISSION_MODES).permissionMode).toBe('default') + }) + + it('keeps current unknown-arg behavior by ignoring unrecognized flags', () => { + expect(parseRemoteAgentCommandOptions([ + '--unknown', + 'value', + '--model', + 'model-a' + ], GEMINI_PERMISSION_MODES)).toEqual({ + model: 'model-a' + }) + }) + + it('rejects invalid constrained values', () => { + expect(() => parseRemoteAgentCommandOptions([ + '--hapi-starting-mode', + 'sideways' + ], GEMINI_PERMISSION_MODES)).toThrow('Invalid --hapi-starting-mode') + + expect(() => parseRemoteAgentCommandOptions([ + '--permission-mode', + 'bypassPermissions' + ], GEMINI_PERMISSION_MODES)).toThrow('Invalid --permission-mode value') + }) + + it('requires values for resume and model flags', () => { + expect(() => parseRemoteAgentCommandOptions(['--resume'], OPENCODE_PERMISSION_MODES)).toThrow('Missing --resume value') + expect(() => parseRemoteAgentCommandOptions(['--model'], OPENCODE_PERMISSION_MODES)).toThrow('Missing --model value') + }) +}) diff --git a/cli/src/commands/agentCommandOptions.ts b/cli/src/commands/agentCommandOptions.ts new file mode 100644 index 00000000..ecfbb9c6 --- /dev/null +++ b/cli/src/commands/agentCommandOptions.ts @@ -0,0 +1,54 @@ +import type { PermissionMode } from '@hapi/protocol/types' + +export type RemoteAgentCommandOptions = { + startedBy?: 'runner' | 'terminal' + startingMode?: 'local' | 'remote' + permissionMode?: TPermissionMode + model?: string + resumeSessionId?: string +} + +export function parseRemoteAgentCommandOptions( + args: string[], + allowedPermissionModes: readonly TPermissionMode[] +): RemoteAgentCommandOptions { + const options: RemoteAgentCommandOptions = {} + let hasExplicitPermissionMode = false + + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg === '--started-by') { + options.startedBy = args[++i] as 'runner' | 'terminal' + } else if (arg === '--hapi-starting-mode') { + const value = args[++i] + if (value === 'local' || value === 'remote') { + options.startingMode = value + } else { + throw new Error('Invalid --hapi-starting-mode (expected local or remote)') + } + } else if (arg === '--permission-mode') { + const mode = args[++i] + if (!mode || !(allowedPermissionModes as readonly string[]).includes(mode)) { + throw new Error(`Invalid --permission-mode value: ${mode ?? '(missing)'}`) + } + options.permissionMode = mode as TPermissionMode + hasExplicitPermissionMode = true + } else if (arg === '--yolo' && !hasExplicitPermissionMode) { + options.permissionMode = 'yolo' as TPermissionMode + } else if (arg === '--resume') { + const sessionId = args[++i] + if (!sessionId) { + throw new Error('Missing --resume value') + } + options.resumeSessionId = sessionId + } else if (arg === '--model') { + const model = args[++i] + if (!model) { + throw new Error('Missing --model value') + } + options.model = model + } + } + + return options +} diff --git a/cli/src/commands/gemini.ts b/cli/src/commands/gemini.ts index 1ae8c374..fb7e9bfa 100644 --- a/cli/src/commands/gemini.ts +++ b/cli/src/commands/gemini.ts @@ -4,57 +4,14 @@ import { initializeToken } from '@/ui/tokenInit' import { maybeAutoStartServer } from '@/utils/autoStartServer' import type { CommandDefinition } from './types' import { GEMINI_PERMISSION_MODES } from '@hapi/protocol/modes' -import type { GeminiPermissionMode } from '@hapi/protocol/types' +import { parseRemoteAgentCommandOptions } from './agentCommandOptions' export const geminiCommand: CommandDefinition = { name: 'gemini', requiresRuntimeAssets: true, run: async ({ commandArgs }) => { try { - const options: { - startedBy?: 'runner' | 'terminal' - startingMode?: 'local' | 'remote' - permissionMode?: GeminiPermissionMode - model?: string - resumeSessionId?: string - } = {} - - let hasExplicitPermissionMode = false - - for (let i = 0; i < commandArgs.length; i++) { - const arg = commandArgs[i] - if (arg === '--started-by') { - options.startedBy = commandArgs[++i] as 'runner' | 'terminal' - } else if (arg === '--hapi-starting-mode') { - const value = commandArgs[++i] - if (value === 'local' || value === 'remote') { - options.startingMode = value - } else { - throw new Error('Invalid --hapi-starting-mode (expected local or remote)') - } - } else if (arg === '--permission-mode') { - const mode = commandArgs[++i] - if (!mode || !(GEMINI_PERMISSION_MODES as readonly string[]).includes(mode)) { - throw new Error(`Invalid --permission-mode value: ${mode ?? '(missing)'}`) - } - options.permissionMode = mode as GeminiPermissionMode - hasExplicitPermissionMode = true - } else if (arg === '--yolo' && !hasExplicitPermissionMode) { - options.permissionMode = 'yolo' - } else if (arg === '--resume') { - const sessionId = commandArgs[++i] - if (!sessionId) { - throw new Error('Missing --resume value') - } - options.resumeSessionId = sessionId - } else if (arg === '--model') { - const model = commandArgs[++i] - if (!model) { - throw new Error('Missing --model value') - } - options.model = model - } - } + const options = parseRemoteAgentCommandOptions(commandArgs, GEMINI_PERMISSION_MODES) await initializeToken() await maybeAutoStartServer() diff --git a/cli/src/commands/opencode.ts b/cli/src/commands/opencode.ts index 67a7f58e..625c1f2e 100644 --- a/cli/src/commands/opencode.ts +++ b/cli/src/commands/opencode.ts @@ -4,57 +4,14 @@ import { initializeToken } from '@/ui/tokenInit' import { maybeAutoStartServer } from '@/utils/autoStartServer' import type { CommandDefinition } from './types' import { OPENCODE_PERMISSION_MODES } from '@hapi/protocol/modes' -import type { OpencodePermissionMode } from '@hapi/protocol/types' +import { parseRemoteAgentCommandOptions } from './agentCommandOptions' export const opencodeCommand: CommandDefinition = { name: 'opencode', requiresRuntimeAssets: true, run: async ({ commandArgs }) => { try { - const options: { - startedBy?: 'runner' | 'terminal' - startingMode?: 'local' | 'remote' - permissionMode?: OpencodePermissionMode - model?: string - resumeSessionId?: string - } = {} - - let hasExplicitPermissionMode = false - - for (let i = 0; i < commandArgs.length; i++) { - const arg = commandArgs[i] - if (arg === '--started-by') { - options.startedBy = commandArgs[++i] as 'runner' | 'terminal' - } else if (arg === '--hapi-starting-mode') { - const value = commandArgs[++i] - if (value === 'local' || value === 'remote') { - options.startingMode = value - } else { - throw new Error('Invalid --hapi-starting-mode (expected local or remote)') - } - } else if (arg === '--permission-mode') { - const mode = commandArgs[++i] - if (!mode || !(OPENCODE_PERMISSION_MODES as readonly string[]).includes(mode)) { - throw new Error(`Invalid --permission-mode value: ${mode ?? '(missing)'}`) - } - options.permissionMode = mode as OpencodePermissionMode - hasExplicitPermissionMode = true - } else if (arg === '--yolo' && !hasExplicitPermissionMode) { - options.permissionMode = 'yolo' - } else if (arg === '--resume') { - const sessionId = commandArgs[++i] - if (!sessionId) { - throw new Error('Missing --resume value') - } - options.resumeSessionId = sessionId - } else if (arg === '--model') { - const model = commandArgs[++i] - if (!model) { - throw new Error('Missing --model value') - } - options.model = model - } - } + const options = parseRemoteAgentCommandOptions(commandArgs, OPENCODE_PERMISSION_MODES) await initializeToken() await maybeAutoStartServer()