Files
orca/src/cli/handlers/terminal.ts
T
2026-05-12 02:09:13 -04:00

154 lines
5.9 KiB
TypeScript

import type {
RuntimeTerminalClose,
RuntimeTerminalCreate,
RuntimeTerminalFocus,
RuntimeTerminalListResult,
RuntimeTerminalRead,
RuntimeTerminalRename,
RuntimeTerminalSend,
RuntimeTerminalShow,
RuntimeTerminalSplit,
RuntimeTerminalWait
} from '../../shared/runtime-types'
import type { CommandHandler } from '../dispatch'
import {
formatTerminalClose,
formatTerminalCreate,
formatTerminalFocus,
formatTerminalList,
formatTerminalRead,
formatTerminalRename,
formatTerminalSend,
formatTerminalShow,
formatTerminalSplit,
formatTerminalWait,
printResult
} from '../format'
import {
getOptionalPositiveIntegerFlag,
getOptionalStringFlag,
getRequiredStringFlag
} from '../flags'
import { RuntimeClientError } from '../runtime-client'
import {
getBrowserWorktreeSelector,
getOptionalWorktreeSelector,
getRequiredWorktreeSelector,
getTerminalHandle
} from '../selectors'
// Why: terminal wait legitimately needs to outlive the CLI's default RPC
// timeout. Even without an explicit server timeout, the client must allow
// long waits instead of failing at the generic 15s transport cap.
const DEFAULT_TERMINAL_WAIT_RPC_TIMEOUT_MS = 5 * 60 * 1000
const terminalFocusHandler: CommandHandler = async ({ flags, client, cwd, json }) => {
const result = await client.call<{ focus: RuntimeTerminalFocus }>('terminal.focus', {
terminal: await getTerminalHandle(flags, cwd, client)
})
printResult(result, json, formatTerminalFocus)
}
export const TERMINAL_HANDLERS: Record<string, CommandHandler> = {
'terminal list': async ({ flags, client, cwd, json }) => {
const result = await client.call<RuntimeTerminalListResult>('terminal.list', {
worktree: await getOptionalWorktreeSelector(flags, 'worktree', cwd, client),
limit: getOptionalPositiveIntegerFlag(flags, 'limit')
})
printResult(result, json, formatTerminalList)
},
'terminal show': async ({ flags, client, cwd, json }) => {
const result = await client.call<{ terminal: RuntimeTerminalShow }>('terminal.show', {
terminal: await getTerminalHandle(flags, cwd, client)
})
printResult(result, json, formatTerminalShow)
},
'terminal read': async ({ flags, client, cwd, json }) => {
const cursorFlag = getOptionalStringFlag(flags, 'cursor')
const cursor =
cursorFlag !== undefined && /^\d+$/.test(cursorFlag)
? Number.parseInt(cursorFlag, 10)
: undefined
if (cursorFlag !== undefined && cursor === undefined) {
throw new RuntimeClientError('invalid_argument', '--cursor must be a non-negative integer')
}
const result = await client.call<{ terminal: RuntimeTerminalRead }>('terminal.read', {
terminal: await getTerminalHandle(flags, cwd, client),
...(cursor !== undefined ? { cursor } : {})
})
printResult(result, json, formatTerminalRead)
},
'terminal send': async ({ flags, client, cwd, json }) => {
const result = await client.call<{ send: RuntimeTerminalSend }>('terminal.send', {
terminal: await getTerminalHandle(flags, cwd, client),
text: getOptionalStringFlag(flags, 'text'),
enter: flags.get('enter') === true,
interrupt: flags.get('interrupt') === true
})
printResult(result, json, formatTerminalSend)
},
'terminal wait': async ({ flags, client, cwd, json }) => {
const timeoutMs = getOptionalPositiveIntegerFlag(flags, 'timeout-ms')
const result = await client.call<{ wait: RuntimeTerminalWait }>(
'terminal.wait',
{
terminal: await getTerminalHandle(flags, cwd, client),
for: getRequiredStringFlag(flags, 'for'),
timeoutMs
},
{
timeoutMs: timeoutMs ? timeoutMs + 5000 : DEFAULT_TERMINAL_WAIT_RPC_TIMEOUT_MS
}
)
printResult(result, json, formatTerminalWait)
},
'terminal stop': async ({ flags, client, cwd, json }) => {
const result = await client.call<{ stopped: number }>('terminal.stop', {
worktree: await getRequiredWorktreeSelector(flags, 'worktree', cwd, client)
})
printResult(result, json, (value) => `Stopped ${value.stopped} terminals.`)
},
'terminal rename': async ({ flags, client, cwd, json }) => {
const result = await client.call<{ rename: RuntimeTerminalRename }>('terminal.rename', {
terminal: await getTerminalHandle(flags, cwd, client),
title: getOptionalStringFlag(flags, 'title') ?? null
})
printResult(result, json, formatTerminalRename)
},
'terminal create': async ({ flags, client, cwd, json }) => {
const result = await client.call<{ terminal: RuntimeTerminalCreate }>('terminal.create', {
worktree: await getBrowserWorktreeSelector(flags, cwd, client),
command: getOptionalStringFlag(flags, 'command'),
title: getOptionalStringFlag(flags, 'title'),
focus: flags.get('focus') === true
})
printResult(result, json, formatTerminalCreate)
},
// Why: `focus` and `switch` are aliases — register the same handler under
// both keys so the dispatch table stays a plain command-path lookup.
'terminal focus': terminalFocusHandler,
'terminal switch': terminalFocusHandler,
'terminal close': async ({ flags, client, cwd, json }) => {
const result = await client.call<{ close: RuntimeTerminalClose }>('terminal.close', {
terminal: await getTerminalHandle(flags, cwd, client)
})
printResult(result, json, formatTerminalClose)
},
'terminal split': async ({ flags, client, cwd, json }) => {
const directionFlag = getOptionalStringFlag(flags, 'direction')
if (
directionFlag !== undefined &&
directionFlag !== 'horizontal' &&
directionFlag !== 'vertical'
) {
throw new RuntimeClientError('invalid_argument', '--direction must be horizontal or vertical')
}
const result = await client.call<{ split: RuntimeTerminalSplit }>('terminal.split', {
terminal: await getTerminalHandle(flags, cwd, client),
direction: directionFlag,
command: getOptionalStringFlag(flags, 'command')
})
printResult(result, json, formatTerminalSplit)
}
}