mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
138 lines
4.4 KiB
TypeScript
138 lines
4.4 KiB
TypeScript
import { spawn } from 'node:child_process'
|
|
import type { CommandHandler } from '../dispatch'
|
|
import { formatCliStatus, formatStatus, printResult } from '../format'
|
|
import { RuntimeClientError, serveOrcaApp } from '../runtime-client'
|
|
|
|
function envRecord(): Record<string, string> {
|
|
return Object.fromEntries(
|
|
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
)
|
|
}
|
|
|
|
function withTeammateModeAuto(args: string[]): string[] {
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const arg = args[index]
|
|
if (arg === '--teammate-mode' || arg.startsWith('--teammate-mode=')) {
|
|
return args
|
|
}
|
|
}
|
|
return ['--teammate-mode', 'auto', ...args]
|
|
}
|
|
|
|
async function runClaudeAgentTeams(env: Record<string, string>, args: string[]): Promise<number> {
|
|
return await new Promise((resolve, reject) => {
|
|
const child = spawn('claude', withTeammateModeAuto(args), {
|
|
stdio: 'inherit',
|
|
env
|
|
})
|
|
child.once('error', reject)
|
|
child.once('exit', (code, signal) => {
|
|
if (typeof code === 'number') {
|
|
resolve(code)
|
|
return
|
|
}
|
|
resolve(signal ? 1 : 0)
|
|
})
|
|
})
|
|
}
|
|
|
|
function getOptionalServePort(flags: Map<string, string | boolean>): string | null {
|
|
if (!flags.has('port')) {
|
|
return null
|
|
}
|
|
const rawPort = flags.get('port')
|
|
if (typeof rawPort !== 'string' || rawPort.length === 0) {
|
|
throw new RuntimeClientError('invalid_argument', 'Missing value for --port.')
|
|
}
|
|
const port = Number(rawPort)
|
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
throw new RuntimeClientError('invalid_argument', `Invalid --port value: ${rawPort}`)
|
|
}
|
|
return rawPort
|
|
}
|
|
|
|
export const CORE_HANDLERS: Record<string, CommandHandler> = {
|
|
'claude-teams': async ({ client, rawArgs }) => {
|
|
if (process.platform === 'win32') {
|
|
throw new RuntimeClientError(
|
|
'unsupported_platform',
|
|
'Claude Agent Teams native panes are not supported on Windows.'
|
|
)
|
|
}
|
|
const paneKey = process.env.ORCA_PANE_KEY
|
|
if (!paneKey) {
|
|
throw new RuntimeClientError(
|
|
'invalid_environment',
|
|
'orca claude-teams must be run inside an Orca terminal.'
|
|
)
|
|
}
|
|
const response = await client.call<{ launch: { env: Record<string, string> } }>(
|
|
'agentTeams.prepareLaunch',
|
|
{
|
|
paneKey,
|
|
env: envRecord()
|
|
}
|
|
)
|
|
process.exitCode = await runClaudeAgentTeams(
|
|
{
|
|
...envRecord(),
|
|
...response.result.launch.env
|
|
},
|
|
rawArgs ?? []
|
|
)
|
|
},
|
|
open: async ({ client, json }) => {
|
|
const result = await client.openOrca()
|
|
printResult(result, json, formatCliStatus)
|
|
},
|
|
serve: async ({ flags, json }) => {
|
|
if (flags.get('no-pairing') === true && flags.get('mobile-pairing') === true) {
|
|
throw new RuntimeClientError(
|
|
'invalid_argument',
|
|
'Use either --mobile-pairing or --no-pairing, not both.'
|
|
)
|
|
}
|
|
if (flags.get('recipe-json') === true && flags.get('no-pairing') === true) {
|
|
throw new RuntimeClientError(
|
|
'invalid_argument',
|
|
'Recipe JSON output requires runtime pairing; remove --no-pairing.'
|
|
)
|
|
}
|
|
if (flags.get('recipe-json') === true && flags.get('mobile-pairing') === true) {
|
|
throw new RuntimeClientError(
|
|
'invalid_argument',
|
|
'Recipe JSON output requires runtime pairing; remove --mobile-pairing.'
|
|
)
|
|
}
|
|
const projectRoot =
|
|
typeof flags.get('project-root') === 'string' ? (flags.get('project-root') as string) : null
|
|
if (flags.get('recipe-json') === true && !projectRoot) {
|
|
throw new RuntimeClientError(
|
|
'invalid_argument',
|
|
'Recipe JSON output requires --project-root.'
|
|
)
|
|
}
|
|
const port = getOptionalServePort(flags)
|
|
const exitCode = await serveOrcaApp({
|
|
json,
|
|
port,
|
|
pairingAddress:
|
|
typeof flags.get('pairing-address') === 'string'
|
|
? (flags.get('pairing-address') as string)
|
|
: null,
|
|
noPairing: flags.get('no-pairing') === true,
|
|
mobilePairing: flags.get('mobile-pairing') === true,
|
|
recipeJson: flags.get('recipe-json') === true,
|
|
projectRoot
|
|
})
|
|
process.exitCode = exitCode
|
|
},
|
|
status: async ({ client, json }) => {
|
|
const result = await client.getCliStatus()
|
|
if (!json && !result.result.runtime.reachable) {
|
|
process.exitCode = 1
|
|
}
|
|
printResult(result, json, formatStatus)
|
|
}
|
|
}
|