/** * A device host is a machine with simulators or emulators on it. The service * layer only ever talks to this interface, so a future SSH or cloud host slots * in beside `LocalDeviceHost` without touching discovery, the proxy, or the * MCP tools. * * Every ready host presents a loopback origin where expo-device-hub answers. * Hosts add an agent-device daemon endpoint only after agent access is granted. * For the local host both run on this machine; a remote host would forward * them here. */ import type { DeviceHostId, DeviceHostSummary, DevicePlatform, DevicePlatformAvailability, } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; export class DeviceHostError extends Schema.TaggedError()("DeviceHostError", { hostId: Schema.String, step: Schema.String, cause: Schema.Defect(), }) { override get message(): string { return `Device host ${this.hostId} failed while ${this.step}.`; } } export class DeviceHostTimeoutError extends Schema.TaggedError()( "DeviceHostTimeoutError", { hostId: Schema.String, timeoutMs: Schema.Number }, ) { override get message(): string { return `Device host ${this.hostId} did not start agent tools within ${this.timeoutMs} ms.`; } } export interface DeviceHubEndpoint { /** Loopback origin of expo-device-hub, e.g. `http://127.0.0.1:3400`. */ readonly origin: string; } export interface AgentDeviceEndpoint { readonly baseUrl: string; readonly token: string; /** Host-local path of the agent-device entry script. The provider uses a separate local CLI install. */ readonly entryPath: string; } export interface DeviceHostReady { readonly nodePath: string; readonly hub: DeviceHubEndpoint; /** * Runs a host command (`xcrun`, `adb`, or a helper bundled with the hub) * where the devices live. On the local host this is a plain spawn; a * remote host would run it over its transport. */ readonly run: ( command: string, args: ReadonlyArray, options?: { readonly timeoutMs?: number; readonly stdin?: string }, ) => Effect.Effect<{ readonly stdout: string; readonly stderr: string; readonly code: number }>; /** Absolute paths of helper binaries vendored with the hub, when present. */ readonly helpers: { readonly serveSimAxSettings: string | null; readonly serveSimCli: string | null; }; } export interface DeviceHostAgentReady extends DeviceHostReady { readonly agentDevice: AgentDeviceEndpoint; } export class DeviceHost extends Context.Service< DeviceHost, { readonly id: DeviceHostId; readonly summary: Effect.Effect; readonly platformAvailability: ( platform: DevicePlatform, ) => Effect.Effect; /** * Installs tools on first use and starts the helper processes. Idempotent: * concurrent callers share one start, and a ready host returns immediately. */ readonly ensureReady: ( onPhase: (phase: "installing" | "starting") => Effect.Effect, ) => Effect.Effect; /** Installs and starts agent-device after the user grants agent access. */ readonly ensureAgentReady: ( onPhase: (phase: "installing" | "starting") => Effect.Effect, ) => Effect.Effect; /** Current endpoints when already running, without starting anything. */ readonly current: Effect.Effect; /** Stops only agent-device. Manual viewing through the hub stays available. */ readonly stopAgent: Effect.Effect; /** Stops helpers. Devices themselves keep running; the user owns those. */ readonly stop: Effect.Effect; } >()("t3/device/DeviceHost") {}