| import { NO_REAL_CONNECTION_CODE } from '@maka/core'; |
| import type { ConnectionCatalogEntry, ConnectionCatalogSnapshot } from '@maka/core'; |
| import { |
| connectOrSpawnRuntimeHost, |
| readRuntimeHostConnectionCatalog, |
| type RuntimeHostConnection, |
| } from '@maka/runtime-host/client'; |
| import { RUNTIME_HOST_PROTOCOL_VERSION, type ClientSurface } from '@maka/runtime-host/protocol'; |
| |
| export interface RuntimeHostCliConnectionContext { |
| readonly connection: RuntimeHostConnection; |
| readonly catalog: ConnectionCatalogSnapshot; |
| close(): Promise<void>; |
| } |
| |
| export interface RuntimeHostCliTarget { |
| readonly connection: ConnectionCatalogEntry; |
| readonly model: string; |
| } |
| |
| interface RuntimeHostCliContextDeps { |
| readonly connectOrSpawn: typeof connectOrSpawnRuntimeHost; |
| readonly readConnectionCatalog: typeof readRuntimeHostConnectionCatalog; |
| readonly executionCandidateEntrypoint: URL; |
| } |
| |
| export async function connectRuntimeHostCli( |
| input: { |
| readonly rootPath: string; |
| readonly surface: ClientSurface; |
| readonly legacyConfigurationRoot?: string; |
| }, |
| overrides: Partial<RuntimeHostCliContextDeps> = {}, |
| ): Promise<RuntimeHostCliConnectionContext> { |
| const deps: RuntimeHostCliContextDeps = { |
| connectOrSpawn: connectOrSpawnRuntimeHost, |
| readConnectionCatalog: readRuntimeHostConnectionCatalog, |
| executionCandidateEntrypoint: new URL( |
| import.meta.resolve('@maka/runtime-host/execution-candidate-main'), |
| ), |
| ...overrides, |
| }; |
| const connected = await deps.connectOrSpawn({ |
| rootPath: input.rootPath, |
| surface: input.surface, |
| protocol: { min: RUNTIME_HOST_PROTOCOL_VERSION, max: RUNTIME_HOST_PROTOCOL_VERSION }, |
| candidateEntrypoint: deps.executionCandidateEntrypoint, |
| ...(input.legacyConfigurationRoot |
| ? { legacyConfigurationRoot: input.legacyConfigurationRoot } |
| : {}), |
| }); |
| if (connected.kind === 'incompatible') { |
| throw new Error( |
| `Runtime Host protocol is incompatible (Host ${connected.handshake.protocolMin}-${connected.handshake.protocolMax}, CLI ${RUNTIME_HOST_PROTOCOL_VERSION})`, |
| ); |
| } |
| if (connected.kind === 'failed') { |
| throw new Error(`Runtime Host startup failed: ${connected.reason}`); |
| } |
| const connection = connected.connection; |
| try { |
| await waitForReady(connection); |
| return { |
| connection, |
| catalog: await deps.readConnectionCatalog(connection), |
| close: () => connection.close(), |
| }; |
| } catch (error) { |
| await connection.close().catch(() => undefined); |
| throw error; |
| } |
| } |
| |
| export function resolveRuntimeHostCliTarget( |
| catalog: ConnectionCatalogSnapshot, |
| input: { readonly connectionSlug?: string; readonly model?: string } = {}, |
| ): RuntimeHostCliTarget { |
| const defaultTarget = catalog.defaultTarget; |
| const connection = input.connectionSlug |
| ? catalog.connections.find((candidate) => candidate.slug === input.connectionSlug) |
| : catalog.connections.find( |
| (candidate) => candidate.connectionId === defaultTarget?.connectionId, |
| ); |
| if (!connection || !connection.enabled) { |
| throw new Error( |
| input.connectionSlug |
| ? `Runtime Host model connection is unavailable: ${input.connectionSlug}` |
| : `${NO_REAL_CONNECTION_CODE}:missing_default_connection: Runtime Host has no default model connection`, |
| ); |
| } |
| const model = |
| input.model ?? |
| (connection.connectionId === defaultTarget?.connectionId |
| ? defaultTarget.modelId |
| : connection.enabledModelIds[0]); |
| if (!model || !connection.enabledModelIds.includes(model)) { |
| throw new Error(`Runtime Host model is unavailable for ${connection.slug}: ${model ?? ''}`); |
| } |
| return { connection, model }; |
| } |
| |
| async function waitForReady(connection: RuntimeHostConnection): Promise<void> { |
| const deadline = Date.now() + 45_000; |
| while (true) { |
| const status = await connection.status(Math.max(1, deadline - Date.now())); |
| if (status.state === 'ready') return; |
| if (status.state === 'draining') throw new Error('Runtime Host drained before becoming ready'); |
| const remaining = deadline - Date.now(); |
| if (remaining <= 0) throw new Error('Runtime Host did not become ready before the deadline'); |
| await new Promise((resolve) => setTimeout(resolve, Math.min(25, remaining))); |
| } |
| } |