orca/src/cli/args.ts

132 lines
3.1 KiB
TypeScript

import { RuntimeClientError } from './runtime-client'
export type ParsedArgs = {
commandPath: string[]
flags: Map<string, string | boolean>
}
export type CommandSpec = {
path: string[]
summary: string
usage: string
allowedFlags: string[]
examples?: string[]
notes?: string[]
}
export const GLOBAL_FLAGS = ['help', 'json', 'pairing-code', 'environment']
export function parseArgs(argv: string[]): ParsedArgs {
const commandPath: string[] = []
const flags = new Map<string, string | boolean>()
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i]
if (!token.startsWith('--')) {
commandPath.push(token)
continue
}
const flag = token.slice(2)
const hasNext = i + 1 < argv.length
const next = argv[i + 1]
if (!hasNext || next.startsWith('--')) {
flags.set(flag, true)
continue
}
flags.set(flag, next)
i += 1
}
return { commandPath, flags }
}
export function resolveHelpPath(parsed: ParsedArgs): string[] | null {
if (parsed.commandPath[0] === 'help') {
return parsed.commandPath.slice(1)
}
if (parsed.flags.has('help')) {
return parsed.commandPath
}
return null
}
export function matches(actual: string[], expected: string[]): boolean {
return (
actual.length === expected.length && actual.every((value, index) => value === expected[index])
)
}
export function supportsBrowserPageFlag(commandPath: string[]): boolean {
const joined = commandPath.join(' ')
if (['open', 'status'].includes(commandPath[0])) {
return false
}
if (['repo', 'worktree', 'terminal', 'computer', 'note'].includes(commandPath[0])) {
return false
}
return ![
'tab list',
'tab create',
'tab current',
'tab profile list',
'tab profile create',
'tab profile delete'
].includes(joined)
}
export function isCommandGroup(commandPath: string[]): boolean {
return (
(commandPath.length === 1 &&
[
'repo',
'worktree',
'terminal',
'tab',
'cookie',
'intercept',
'capture',
'mouse',
'set',
'clipboard',
'dialog',
'storage',
'orchestration',
'computer',
'environment'
].includes(commandPath[0])) ||
(commandPath.length === 2 &&
commandPath[0] === 'storage' &&
['local', 'session'].includes(commandPath[1]))
)
}
export function findCommandSpec(
specs: CommandSpec[],
commandPath: string[]
): CommandSpec | undefined {
return specs.find((spec) => matches(spec.path, commandPath))
}
export function validateCommandAndFlags(specs: CommandSpec[], parsed: ParsedArgs): void {
const spec = findCommandSpec(specs, parsed.commandPath)
if (!spec) {
throw new RuntimeClientError(
'invalid_argument',
`Unknown command: ${parsed.commandPath.join(' ')}`
)
}
for (const flag of parsed.flags.keys()) {
if (
!spec.allowedFlags.includes(flag) &&
!(flag === 'page' && supportsBrowserPageFlag(spec.path))
) {
throw new RuntimeClientError(
'invalid_argument',
`Unknown flag --${flag} for command: ${spec.path.join(' ')}`
)
}
}
}