import type { FirstRunSetupDecision } from './first-run-setup-gate' export interface PrimaryBackendStartupOptions { connectRemote: (remote: Remote) => Promise ensureLocalRuntime: (backend: Backend) => Promise prepareLocalBackend: () => Backend | Promise resolveRemote: () => Promise waitForDecision: (backend: Backend) => Promise waitForLocalStart: () => Promise } export type PrimaryBackendStartupResult = { kind: 'local'; backend: RuntimeBackend } | { kind: 'remote'; connection: Connection } interface ResolvedPrimaryRemote { authMode?: 'oauth' | 'token' baseUrl: string connectionId?: string remoteHermesVersion?: string remoteHost?: string remoteKind?: 'cloud' | 'ssh' | 'url' source?: string ssh?: { effectiveConfigFingerprint?: string host?: string keyPath?: string port?: number remoteHermesPath?: string remoteProfile?: string user?: string } token: unknown wsUrl: string } /** * Build the renderer-facing primary remote descriptor without dropping route * identity. Tests cross this same seam, so adding a field to the resolved * remote cannot silently disappear during primary startup. */ export function createPrimaryRemoteConnection( remote: ResolvedPrimaryRemote, logs: string[], windowState: State ) { return { baseUrl: remote.baseUrl, mode: 'remote' as const, source: remote.source, authMode: remote.authMode || 'token', remoteHost: remote.remoteHost, remoteKind: remote.remoteKind, remoteHermesVersion: remote.remoteHermesVersion, ...(remote.connectionId ? { connectionId: remote.connectionId } : {}), ...(remote.ssh ? { ssh: remote.ssh } : {}), token: remote.token, wsUrl: remote.wsUrl, logs, ...windowState } } export class FirstRunSetupResetError extends Error { readonly firstRunSetupReset = true constructor() { super('First-run setup was reset before a choice completed.') this.name = 'FirstRunSetupResetError' } } // Owns the production startHermes path up to the local process spawn. Keeping // the full ordering here makes the first-run remote boundary executable in a // test: an already-saved remote wins immediately; otherwise update exclusion // and local backend resolution happen before the setup gate, and a remote Apply // re-resolves persisted config without ever entering ensureRuntime/bootstrap. export async function runPrimaryBackendStartup({ connectRemote, ensureLocalRuntime, prepareLocalBackend, resolveRemote, waitForDecision, waitForLocalStart }: PrimaryBackendStartupOptions): Promise< PrimaryBackendStartupResult > { const savedRemote = await resolveRemote() if (savedRemote) { return { kind: 'remote', connection: await connectRemote(savedRemote) } } await waitForLocalStart() const backend = await prepareLocalBackend() const decision = await waitForDecision(backend) if (decision === 'remote-applied') { const appliedRemote = await resolveRemote() if (!appliedRemote) { throw new Error('First-run remote setup completed without a saved remote backend.') } return { kind: 'remote', connection: await connectRemote(appliedRemote) } } if (decision === 'reset') { throw new FirstRunSetupResetError() } return { kind: 'local', backend: await ensureLocalRuntime(backend) } }