mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 00:03:52 +00:00
* fix(runtime): stamp a runtime's own project setups as local, and report remote status about the remote (STA-4792) Two independent frame-of-reference bugs, both from code describing one machine while labelled as another. #15366 — projectHostSetup.* persisted the caller's host id verbatim. Those `runtime:<environment-id>` ids are minted by the calling client's own pairing store, so they name a machine only relative to that client. A client sending one is addressing this runtime, and runtimes do not proxy these calls onward, so the host it names is us. Storing the client's spelling made one machine look like a different host to every other client, hid its rows from them, and defeated the (projectId, hostId) duplicate check — two laptops paired to one server each created their own setup for the same checkout. Re-spell it as `local` at the RPC boundary. Rows written earlier keep their old stamp; readers already project `local` back to `runtime:<their-id>`, so the client-visible model is unchanged and no ids are rewritten. STA-4792 defect 4 — `status --environment <name>` hardcoded app.running:false to mean "no desktop on THIS machine" while every other field in the same object described the target, including a desktopWindowStatus echoed straight from it. The result contradicted itself and read as "that run was headless" when the remote GUI was up. `app` now describes the target, keyed off the one window status that requires a live renderer, and the result names its own subject so the frame can't be misread again. The remote pid is not knowable, so it stays null. STA-4792 defect 2 gets a regression test rather than a fix: routing already made the client remote, which is what stops a Windows destination being joined to the local cwd. The test pins the exact reported invocation. * fix(status): share the remote app projection with the SSH host passthrough, and name the version gap on project host setup Two review follow-ups. The SSH host passthrough answered `app.running: true` unconditionally for the Orca host a caller reached over SSH, claiming a desktop app even for a headless `serve`. That is the same defect as the paired-server path, one transport over, so the projection moved to shared and both now answer the question the same way. `--host runtime:<id>` routes project commands to a paired server, which means a client can reach a server that predates project host setup without meaning to. That answered a raw `method_not_found`, which reads as an Orca bug rather than a version gap; the CLI now names it the way the desktop already does. Reverted a third change: making the persistence duplicate check treat `local` and `runtime:*` as one machine. That assumption holds at the RPC boundary, where a `runtime:` host means the runtime being addressed, but not in the store, which also records independent provisioning metadata for machines that are not itself. An existing test covers exactly that, and it was right. The duplicate convergence therefore stays bounded to rows written after the normalization.
196 lines
5.8 KiB
TypeScript
196 lines
5.8 KiB
TypeScript
import type { CliStatusResult } from '../shared/runtime-types'
|
|
import { computerUseErrorRecoveryData } from '../shared/computer-use-error-recovery'
|
|
import { prepareComputerCliJsonResult } from './computer-format'
|
|
import type { RuntimeRpcFailure, RuntimeRpcSuccess } from './runtime-client'
|
|
import { RuntimeClientError, RuntimeRpcFailureError } from './runtime/types'
|
|
|
|
export {
|
|
formatBrowserProfileList,
|
|
formatScreenshot,
|
|
formatSnapshot,
|
|
formatTabList,
|
|
formatTabListWithProfiles,
|
|
formatTabProfileClone,
|
|
formatTabProfileShow,
|
|
formatTabShow
|
|
} from './browser-format'
|
|
|
|
export {
|
|
formatComputerAction,
|
|
formatGetAppState,
|
|
formatListApps,
|
|
formatListWindows
|
|
} from './computer-format'
|
|
export type { ComputerActionFollowUpTarget } from './computer-format'
|
|
export {
|
|
formatProjectHostSetupCreateResult,
|
|
formatProjectHostSetupDeleteResult,
|
|
formatProjectHostSetupList,
|
|
formatProjectHostSetupResult,
|
|
formatProjectHostSetupUpdateResult,
|
|
formatProjectList
|
|
} from './project-format'
|
|
export {
|
|
formatTerminalClose,
|
|
formatTerminalCreate,
|
|
formatTerminalFocus,
|
|
formatTerminalList,
|
|
formatTerminalRead,
|
|
formatTerminalRename,
|
|
formatTerminalSend,
|
|
formatTerminalShow,
|
|
formatTerminalSplit,
|
|
formatTerminalWait
|
|
} from './terminal-format'
|
|
export {
|
|
formatAutomationList,
|
|
formatAutomationRemoved,
|
|
formatAutomationRun,
|
|
formatAutomationRuns,
|
|
formatAutomationShow,
|
|
formatEnvironment,
|
|
formatEnvironmentList,
|
|
formatMemorySnapshot,
|
|
formatRepoList,
|
|
formatRepoRefs,
|
|
formatRepoShow,
|
|
formatWorktreeList,
|
|
formatWorktreePs,
|
|
formatWorktreeShow
|
|
} from './workspace-format'
|
|
|
|
type CliErrorContext = {
|
|
commandPath?: readonly string[]
|
|
}
|
|
|
|
export function printResult<TResult>(
|
|
response: RuntimeRpcSuccess<TResult>,
|
|
json: boolean,
|
|
formatter: (value: TResult) => string
|
|
): void {
|
|
if (json) {
|
|
console.log(JSON.stringify(prepareComputerCliJsonResult(response), null, 2))
|
|
return
|
|
}
|
|
console.log(formatter(response.result))
|
|
}
|
|
|
|
export function formatCliError(error: unknown, context: CliErrorContext = {}): string {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
if (error instanceof RuntimeClientError && error.code === 'runtime_unavailable') {
|
|
if (hasOrchestrationRequestId(error.data)) {
|
|
return message
|
|
}
|
|
return `${message}\nOrca is not running. Run 'orca open' first.`
|
|
}
|
|
// Why: error-specific recovery must win over the generic computer fallback.
|
|
if (error instanceof RuntimeClientError) {
|
|
const nextSteps = nextStepsFromData(error.data)
|
|
if (nextSteps.length > 0) {
|
|
return formatMessageWithNextSteps(message, nextSteps)
|
|
}
|
|
if (error.code === 'invalid_argument' && context.commandPath?.[0] === 'computer') {
|
|
return formatMessageWithNextSteps(
|
|
message,
|
|
computerUseErrorRecoveryData('invalid_argument')?.nextSteps ?? []
|
|
)
|
|
}
|
|
}
|
|
if (
|
|
error instanceof RuntimeRpcFailureError &&
|
|
error.response.error.code === 'runtime_unavailable'
|
|
) {
|
|
return `${message}\nOrca is not running. Run 'orca open' first.`
|
|
}
|
|
if (error instanceof RuntimeRpcFailureError) {
|
|
return formatMessageWithNextSteps(message, nextStepsFromData(error.response.error.data))
|
|
}
|
|
return message
|
|
}
|
|
|
|
function hasOrchestrationRequestId(data: unknown): boolean {
|
|
return (
|
|
data !== null &&
|
|
typeof data === 'object' &&
|
|
typeof (data as { orchestrationRequestId?: unknown }).orchestrationRequestId === 'string'
|
|
)
|
|
}
|
|
|
|
export function reportCliError(error: unknown, json: boolean, context: CliErrorContext = {}): void {
|
|
if (json) {
|
|
if (error instanceof RuntimeRpcFailureError) {
|
|
console.log(JSON.stringify(error.response, null, 2))
|
|
} else {
|
|
const response: RuntimeRpcFailure = {
|
|
id: 'local',
|
|
ok: false,
|
|
error: {
|
|
code: error instanceof RuntimeClientError ? error.code : 'runtime_error',
|
|
message: error instanceof Error ? error.message : String(error),
|
|
data: localCliErrorData(error, context)
|
|
},
|
|
_meta: {
|
|
runtimeId: null
|
|
}
|
|
}
|
|
console.log(JSON.stringify(response, null, 2))
|
|
}
|
|
} else {
|
|
console.error(formatCliError(error, context))
|
|
}
|
|
}
|
|
|
|
function formatMessageWithNextSteps(message: string, nextSteps: readonly string[]): string {
|
|
if (nextSteps.length === 0) {
|
|
return message
|
|
}
|
|
return `${message}\n${nextSteps.map((step) => `Next step: ${step}`).join('\n')}`
|
|
}
|
|
|
|
function nextStepsFromData(data: unknown): string[] {
|
|
if (
|
|
data &&
|
|
typeof data === 'object' &&
|
|
Array.isArray((data as { nextSteps?: unknown }).nextSteps)
|
|
) {
|
|
return (data as { nextSteps: unknown[] }).nextSteps.filter(
|
|
(step): step is string => typeof step === 'string'
|
|
)
|
|
}
|
|
return []
|
|
}
|
|
|
|
function localCliErrorData(error: unknown, context: CliErrorContext): unknown {
|
|
// Why: error-specific recovery must win over the generic computer fallback.
|
|
if (error instanceof RuntimeClientError && error.data !== undefined) {
|
|
return error.data
|
|
}
|
|
if (
|
|
error instanceof RuntimeClientError &&
|
|
error.code === 'invalid_argument' &&
|
|
context.commandPath?.[0] === 'computer'
|
|
) {
|
|
return computerUseErrorRecoveryData('invalid_argument')
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export function formatCliStatus(status: CliStatusResult): string {
|
|
return [
|
|
...(status.target && status.target.kind === 'environment'
|
|
? [`target: environment ${status.target.environment}`]
|
|
: []),
|
|
`appRunning: ${status.app.running}`,
|
|
`pid: ${status.app.pid ?? 'none'}`,
|
|
`desktopWindowStatus: ${status.app.desktopWindowStatus ?? 'unknown'}`,
|
|
`runtimeState: ${status.runtime.state}`,
|
|
`runtimeReachable: ${status.runtime.reachable}`,
|
|
`runtimeId: ${status.runtime.runtimeId ?? 'none'}`,
|
|
`graphState: ${status.graph.state}`
|
|
].join('\n')
|
|
}
|
|
|
|
export function formatStatus(status: CliStatusResult): string {
|
|
return formatCliStatus(status)
|
|
}
|