# apps/server/src/device/DeviceHost.ts · gitcafe/t3code

[View on GitCafe](https://git.cafe/gitcafe/t3code/blob/365aa87982a4d81cc8e0c085e8d1a40ca7daecdc/apps/server/src/device/DeviceHost.ts)

Repository: [gitcafe/t3code](https://git.cafe/gitcafe/t3code)

Visibility: public

Requested revision: 365aa87982a4d81cc8e0c085e8d1a40ca7daecdc

Requested commit: 365aa87982a4d81cc8e0c085e8d1a40ca7daecdc

Commit: 365aa87982a4d81cc8e0c085e8d1a40ca7daecdc

Blob: 14537985b9b852d12499ecc698ade49574e8e055

Size: 4177 bytes

[Immutable source](https://git.cafe/gitcafe/t3code/blob/365aa87982a4d81cc8e0c085e8d1a40ca7daecdc/apps/server/src/device/DeviceHost.ts?format=markdown)

```
/**
 * 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";
import type { NodeRuntimeUnavailableError } from "@t3tools/shared/nodeRuntime";

export class DeviceHostError extends Schema.TaggedError<DeviceHostError>()("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>()(
  "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<string>,
    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<DeviceHostSummary>;
    readonly inspect?: Effect.Effect<DeviceHostSummary, DeviceHostError>;
    readonly platformAvailability: (
      platform: DevicePlatform,
    ) => Effect.Effect<DevicePlatformAvailability>;
    /**
     * 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", detail?: string) => Effect.Effect<void>,
    ) => Effect.Effect<DeviceHostReady, DeviceHostError | NodeRuntimeUnavailableError>;
    /** Installs and starts agent-device after the user grants agent access. */
    readonly ensureAgentReady: (
      onPhase: (phase: "installing" | "starting", detail?: string) => Effect.Effect<void>,
    ) => Effect.Effect<
      DeviceHostAgentReady,
      DeviceHostError | DeviceHostTimeoutError | NodeRuntimeUnavailableError
    >;
    /** Current endpoints when already running, without starting anything. */
    readonly current: Effect.Effect<DeviceHostReady | null>;
    /** Stops only agent-device. Manual viewing through the hub stays available. */
    readonly stopAgent: Effect.Effect<void>;
    /** Stops helpers. Devices themselves keep running; the user owns those. */
    readonly stop: Effect.Effect<void>;
  }
>()("t3/device/DeviceHost") {}

```
