Files
orca/src/cli/runtime/metadata.ts
T
Neil fef3f7d2f8 refactor(cli): split runtime-client.ts and add envelope schema validation (#1090)
* refactor(cli): split runtime-client.ts into runtime/ subsystem

Break the 413-line src/cli/runtime-client.ts into focused modules under
src/cli/runtime/:

- types.ts       — RuntimeRpcSuccess/Failure, RuntimeClientError,
                   RuntimeRpcFailureError
- metadata.ts    — readMetadata / tryReadMetadata /
                   getDefaultUserDataPath
- transport.ts   — sendRequest: Unix-socket newline-framed JSON with
                   id and runtimeId verification and timeout handling
- status.ts      — getCliStatus + buildCliStatusResponse +
                   isProcessRunning
- launch.ts      — launchOrcaApp + macOS .app-bundle resolution +
                   ELECTRON_RUN_AS_NODE env handling
- client.ts      — RuntimeClient class, now a thin composer
- index.ts       — subsystem barrel

runtime-client.ts becomes a backward-compat re-export barrel so
src/cli/index.ts and the existing tests import the same symbols from
the same path. No behavior changes.

Motivation: the file had an eslint-disable max-lines override and
mixed five concerns (envelope types, wire transport, metadata I/O,
status aggregation, cross-platform app launch). Splitting them makes
each concern independently testable and unblocks adding schema
validation at the RPC boundary.

* feat(cli): validate runtime RPC envelope with Zod at decode boundary

Add RuntimeRpcEnvelopeSchema and apply it inside sendRequest so every
response frame is validated against the id/ok/result/error/_meta shape
before the CLI hands it to the caller. The payload (`result`) is left
as unknown — the TResult generic remains the caller's responsibility —
so only the envelope itself is the contract this schema enforces.

Motivation: the CLI and the Orca main runtime are separate processes
and can drift in version (older CLI vs newer app, or vice versa during
dev HMR). A malformed or partial frame used to risk mis-typed field
access downstream; it now surfaces as a single structured
`invalid_runtime_response` error.

Behavior:
- Well-formed success and failure frames continue to decode unchanged.
- Failure frames without `_meta` are accepted (the runtime may fail
  before resolving its own runtimeId).
- Valid JSON that does not match the envelope shape now rejects with
  `invalid_runtime_response`, matching the existing error code for
  non-JSON frames.

Tests: adds a pure schema test file
(src/cli/runtime/envelope-schema.test.ts) covering accept/reject cases.
The existing integration tests in runtime-client.test.ts continue to
pass unchanged.
2026-04-25 13:51:50 -07:00

67 lines
2.3 KiB
TypeScript

import { homedir } from 'os'
import { join } from 'path'
import { readFileSync } from 'fs'
import { getRuntimeMetadataPath, type RuntimeMetadata } from '../../shared/runtime-bootstrap'
import { RuntimeClientError } from './types'
export function readMetadata(userDataPath: string): RuntimeMetadata {
const metadataPath = getRuntimeMetadataPath(userDataPath)
try {
const metadata = JSON.parse(readFileSync(metadataPath, 'utf8')) as RuntimeMetadata | null
if (!metadata?.transport || !metadata.authToken) {
throw new RuntimeClientError(
'runtime_unavailable',
`Orca runtime metadata is incomplete at ${metadataPath}`
)
}
return metadata
} catch (error) {
if (error instanceof RuntimeClientError) {
throw error
}
throw new RuntimeClientError(
'runtime_unavailable',
`Could not read Orca runtime metadata at ${metadataPath}. Start the Orca app first.`
)
}
}
export function tryReadMetadata(userDataPath: string): RuntimeMetadata | null {
const metadataPath = getRuntimeMetadataPath(userDataPath)
try {
return JSON.parse(readFileSync(metadataPath, 'utf8')) as RuntimeMetadata | null
} catch {
return null
}
}
export function getDefaultUserDataPath(
platform: NodeJS.Platform = process.platform,
homeDir = homedir()
): string {
// Why: in dev mode (and for parallel Orca instances), the Electron app writes
// runtime metadata to a separate userData directory (e.g. `orca-dev`) to avoid
// clobbering the production app's metadata. The CLI needs to find the same
// metadata file, so this env var lets the CLI target a specific instance.
if (process.env.ORCA_USER_DATA_PATH) {
return process.env.ORCA_USER_DATA_PATH
}
if (platform === 'darwin') {
return join(homeDir, 'Library', 'Application Support', 'orca')
}
if (platform === 'win32') {
const appData = process.env.APPDATA
if (!appData) {
throw new RuntimeClientError(
'runtime_unavailable',
'APPDATA is not set, so the Orca runtime metadata path cannot be resolved.'
)
}
return join(appData, 'orca')
}
// Why: the CLI must find the same metadata file Electron writes in packaged
// runs, so this mirrors Electron's default userData base instead of inventing
// a CLI-specific config path.
return join(process.env.XDG_CONFIG_HOME || join(homeDir, '.config'), 'orca')
}