{"_id":"@alivelabs/mobile-react-client","_rev":"2-cf3f5d390906bc702f4c83a2ed77803a","name":"@alivelabs/mobile-react-client","dist-tags":{"latest":"0.3.0"},"versions":{"0.2.0":{"name":"@alivelabs/mobile-react-client","version":"0.2.0","license":"MIT","_id":"@alivelabs/mobile-react-client@0.2.0","maintainers":[{"name":"bram-dc","email":"bramdelcanho@gmail.com"},{"name":"eenlars","email":"eedenlars@gmail.com"}],"homepage":"https://github.com/alive-home/alive-mobile#readme","bugs":{"url":"https://github.com/alive-home/alive-mobile/issues"},"dist":{"shasum":"e92bd442e651c068165c51c87a50b22778057a18","tarball":"https://registry.npmjs.org/@alivelabs/mobile-react-client/-/mobile-react-client-0.2.0.tgz","fileCount":46,"integrity":"sha512-CpwzT17BnQx6aNnnDhNfi3WhnT/D0cOHTfP05TYfw1HqnHwO60kIJXrWaLhFLZJYn+w232yuavVD/BUTJPyGzQ==","signatures":[{"sig":"MEUCIFfjxuTJA1rAODyNL5zIoqbQ13cpBI1F4ntCXdJfbc63AiEA2Ex4MOxPnmPfJ7QHiVqnk2tjtx6bOLY3iVL5lieQO5Q=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":259345},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"bun":"./src/index.ts","types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"0e6c96f851ed3bbd2b28853a5fc9c7dcbe28d04c","scripts":{"build":"tsc","typecheck":"tsc --noEmit","prepublishOnly":"bun run build"},"_npmUser":{"name":"bram-dc","email":"bramdelcanho@gmail.com"},"repository":{"url":"git+https://github.com/alive-home/alive-mobile.git","type":"git","directory":"packages/react-client"},"_npmVersion":"11.17.0","description":"React client for Alive Mobile: REST client, polling hooks, streaming logs, and a live, drivable simulator or emulator.","directories":{},"_nodeVersion":"26.5.0","dependencies":{"@alivelabs/mobile-schemas":"^0.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.2.8","react-dom":"^19.2.8","typescript":"^7.0.2","@types/react":"^19.2.18","@types/react-dom":"^19.2.4"},"peerDependencies":{"react":">=18.0.0","react-dom":">=18.0.0"},"_npmOperationalInternal":{"tmp":"tmp/mobile-react-client_0.2.0_1788194653055_0.18417224716224312","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@alivelabs/mobile-react-client","version":"0.3.0","description":"React client for Alive Mobile: REST client, polling hooks, streaming logs, and a live, drivable simulator or emulator.","type":"module","license":"MIT","repository":{"type":"git","url":"git+https://github.com/alive-home/alive-mobile.git","directory":"packages/react-client"},"main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"bun":"./src/index.ts","types":"./dist/index.d.ts","import":"./dist/index.js"}},"publishConfig":{"access":"public"},"scripts":{"build":"tsc","prepublishOnly":"bun run build","typecheck":"tsc --noEmit"},"dependencies":{"@alivelabs/mobile-schemas":"^0.2.0"},"peerDependencies":{"react":">=18.0.0","react-dom":">=18.0.0"},"devDependencies":{"@types/react":"^19.2.18","@types/react-dom":"^19.2.4","react":"^19.2.8","react-dom":"^19.2.8","typescript":"^7.0.2"},"gitHead":"40019be27312d35f3eb41220e61958c36c886565","_id":"@alivelabs/mobile-react-client@0.3.0","bugs":{"url":"https://github.com/alive-home/alive-mobile/issues"},"homepage":"https://github.com/alive-home/alive-mobile#readme","_nodeVersion":"26.5.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-Jg5tVKKyTtcMZjiyvwBXS4JdVNEo5UJ+F0Zi7d/YgVQQrwjW3zmXmFDRvtu0ZTW+4r6s3mcFsUBimTwrWKKrqA==","shasum":"0b15099370e34c44bf91b6e5a97f2709eb4eb60b","tarball":"https://registry.npmjs.org/@alivelabs/mobile-react-client/-/mobile-react-client-0.3.0.tgz","fileCount":46,"unpackedSize":285741,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDBB/uJFQsUKtUZhjsG1E4ZQ7lOz4PEc38teH501uu7jAiBptTzYLnUe3dH9SqVV9Dsvmldvk2zwzpNMp8JZBGfzNQ=="}]},"_npmUser":{"name":"bram-dc","email":"bramdelcanho@gmail.com"},"directories":{},"maintainers":[{"name":"bram-dc","email":"bramdelcanho@gmail.com"},{"name":"eenlars","email":"eedenlars@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mobile-react-client_0.3.0_1788517200268_0.8678433490687394"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-31T16:44:12.848Z","modified":"2026-09-04T10:20:00.584Z","0.2.0":"2026-08-31T16:44:13.231Z","0.3.0":"2026-09-04T10:20:00.420Z"},"bugs":{"url":"https://github.com/alive-home/alive-mobile/issues"},"license":"MIT","homepage":"https://github.com/alive-home/alive-mobile#readme","repository":{"type":"git","url":"git+https://github.com/alive-home/alive-mobile.git","directory":"packages/react-client"},"description":"React client for Alive Mobile: REST client, polling hooks, streaming logs, and a live, drivable simulator or emulator.","maintainers":[{"name":"bram-dc","email":"bramdelcanho@gmail.com"},{"name":"eenlars","email":"eedenlars@gmail.com"}],"readme":"# @alivelabs/mobile-react-client\n\nReact client for Alive Mobile's two-stage **build/simulation** API. Watch a build to\ncompletion, then render the live simulation: streaming logs, the live iOS simulator\n(H.264 decoded with WebCodecs onto a `<canvas>`), status, and interaction\n(click to tap, drag to swipe, type to type).\n\nNo orchestration lives here: this package talks to the API and renders. Status,\nlogs and capacity arrive over the API's event stream rather than a poll; see\n[Live updates](#live-updates).\n\n## Breaking change: `Run` is now `Simulation`\n\nThe second stage was called a *run*; it is now a **simulation**: booting a built\nartifact on a native iOS simulator. Everywhere else in the system a *run* is an\nexecution inside a Tart VM, which is exactly what this stage is not. Every export\nbelow is a mechanical rename with no behaviour change, except `SimulatorScreen`,\nwhich is now `SimulatorCanvas`.\n\n| Was | Now |\n|---|---|\n| `Run` | `Simulation` |\n| `RunList` | `SimulationList` |\n| `RunStatus` | `SimulationStatus` |\n| `RunStream` | `SimulationStream` |\n| `CreateRunBody` | `CreateSimulationBody` |\n| `ListRunsQuery` | `ListSimulationsQuery` |\n| `useRun` | `useSimulation`; its result field `run` is now `simulation` |\n| `useRuns` | `useSimulations`; its result field `runs` is now `simulations` |\n| `UseRunResult` | `UseSimulationResult` |\n| `UseRunsResult` | `UseSimulationsResult` |\n| `RunScreen` / `RunScreenProps` | `SimulationScreen` / `SimulationScreenProps` |\n| `SimulatorScreen` / `SimulatorScreenProps` | `SimulatorCanvas` / `SimulatorCanvasProps` |\n| `client.createRun` | `client.createSimulation` |\n| `client.getRun` | `client.getSimulation` |\n| `client.listRuns` | `client.listSimulations` |\n| `client.getRunLogs` | `client.getSimulationLogs` |\n| `client.cancelRun` | `client.cancelSimulation` |\n| `client.setRunOrientation` | `client.setSimulationOrientation` |\n\nThe wire follows: the API is now `/api/simulations/…`, the id field is\n`simulationId` (prefix `sim_`), and the error codes are `SIMULATION_NOT_FOUND` /\n`SIMULATION_NOT_STREAMING`. **Status values are unchanged**: `queued`,\n`dispatched`, `booting`, `streaming`, `ended`, `failed`, `canceled`. There are no\ndeprecated aliases; the old names are gone.\n\n## Install\n\n```bash\nbun add @alivelabs/mobile-react-client   # peers: react, react-dom (>=18)\n```\n\n## Quick start\n\n```tsx\nimport {\n  OrchestratorClient, SimulationScreen, LogConsole, StatusBadge, useBuild, useSimulation,\n} from \"@alivelabs/mobile-react-client\";\n\nconst client = new OrchestratorClient({\n  baseUrl: \"https://your-orchestrator.example.com\",\n  token: import.meta.env.VITE_API_TOKEN,\n});\n\nfunction Pipeline({ buildId, simulationId }: { buildId: string; simulationId: string | null }) {\n  const { build, logs: buildLogs } = useBuild(client, buildId);\n  const { simulation, logs: simulationLogs, stream } = useSimulation(client, simulationId);\n\n  return (\n    <>\n      <StatusBadge status={build?.status ?? null} />\n      <LogConsole logs={buildLogs} />\n      {simulation?.status === \"streaming\" && stream && (\n        <SimulationScreen\n          stream={stream}\n          onRotate={(orientation) =>\n            client.setSimulationOrientation(simulation.simulationId, orientation)\n          }\n        />\n      )}\n      <LogConsole logs={simulationLogs} />\n    </>\n  );\n}\n```\n\n`SimulationScreen` owns the WebSocket to the simulation's stream, the WebCodecs\ndecode, and the input wiring. Give it `stream` (from `useSimulation().stream` or\n`Simulation.stream`) and it renders a drivable simulator. Create the\nbuild/simulation with the `client` (`createBuild` / `createSimulation`) and pass\nthe ids in; the hooks keep the rest current. Rotation is the one control it cannot do\nalone: it travels REST rather than the stream socket, so pass `onRotate` (omit it\nand the rotate buttons are hidden).\n\nWebCodecs needs a modern browser (Chrome/Edge, Safari 16.4+, recent Firefox) and a\nsecure context (HTTPS or localhost). For full control, drive your own canvas with\n`SimulatorCanvas` + `SimulatorDecoder`.\n\n## Live updates\n\nThe hooks read the API's SSE event stream (`GET /api/events`) instead of polling it\nonce a second. **No hook signature changed**: `useBuild(client, id)` still returns\n`{ build, logs, error }` and still keeps itself current. What changed is the cost.\nOver one 133-second build, the old client would have issued 266 requests (status\nand logs at 1 Hz); the new one held a single connection carrying 3 job events and\n24 log frames (4,987 log lines), plus 8 safety-net reads.\n\nThree things matter if you build on this:\n\n- **Polling is the fallback, not the design.** A client that cannot hold a stream\n  falls back to 2 s for a job page and 5 s for a list. The log cursor is shared\n  between both paths, so a stream that drops mid-build carries on from where it\n  stopped instead of rewinding to line one, and recovering to the stream resumes\n  from the same number.\n- **A resync runs even while the stream is healthy**, about every 30 s, at the\n  cadence the server's `hello` frame asks for. It is the backstop for an event that\n  was never emitted, which is the one failure this design can have, and it is why a\n  hook cannot sit permanently on a stale status.\n- **Hooks that need no log subscription share one connection per client.** An\n  overview with three lists and worker health holds one socket rather than four,\n  which matters because browsers cap HTTP/1.1 at six per origin. The sharing key is\n  the `OrchestratorClient` instance, so memoise it rather than constructing one per\n  render.\n\n`client.openEvents(handlers)` is that stream unwrapped, for code that is not a hook:\n\n```ts\nconst handle = client.openEvents({\n  logOwner: \"build\",                       // with logId: subscribe to one job's logs\n  logId: buildId,\n  onJob: (e) => { if (e.kind === \"build\") setBuild(e.entity); },   // whole entity\n  onLog: (e) => append(e.logs),            // { logs, nextSince, ownerType, ownerId }\n  onWorkers: (list) => setWorkers(list.workers),\n  onStatus: (s) => setStatus(s),           // \"connecting\" | \"open\" | \"degraded\"\n  onFatal: (err) => setError(err),         // refused outright (401/403/404)\n});\nhandle.close();                            // or its retry loop outlives the caller\n```\n\n`StreamStatus`, `EventStreamHandlers` and `ClientEventStreamOptions` are exported\nfor it. Job and worker events reach every stream, so filter `onJob` by id. A\ndropped connection is retried with jittered backoff and is not an error; `onFatal`\nfires only for a response the server refused, which is a configuration problem\nrather than a blip.\n\n## Theming\n\nThe components style themselves with inline `CSSProperties` and ship no stylesheet, so there is\nnothing to import and nothing to override with a selector. They are still themeable: every colour\nis written `var(--alive-x, <default>)`, where the default is the dark value they have always used.\n\n**Define nothing and nothing changes.** Define these on `:root` (or on any ancestor of the\ncomponents) and they follow, including under `prefers-color-scheme` since a custom property can\nbe redefined in a media query where an inline style cannot.\n\n| Variable | What it colours |\n|---|---|\n| `--alive-bg` · `--alive-surface` | the console body, the controls panel |\n| `--alive-border` · `--alive-border-strong` · `--alive-border-focus` | panel edges, the text input, the focus ring |\n| `--alive-fg` · `--alive-fg-muted` · `--alive-fg-dim` | body text, headings, timestamps |\n| `--alive-accent` · `--alive-ok` | the primary button, the \"live\" dot |\n| `--alive-danger-surface` · `--alive-danger-line` · `--alive-danger-fg` | the error banner and the rotate error |\n| `--alive-log-{stdout,stderr,system}` and each `-tag` | a log line's message, and its brighter level tag |\n| `--alive-pill-idle-bg` · `--alive-pill-idle-fg` | `StatusBadge` in its two colourless states |\n\nTwo things are deliberately **not** themeable. `SimulatorCanvas` draws a phone: its bezel gradient\nand the black behind the video stay dark, because a phone is a dark object on any desk and a white\none reads as a rendering fault rather than as a light theme. And the nine saturated status pills\nkeep their own colours, since white on a solid red or green is legible against either background.\n\n## Exports\n\n| Export | Kind | Description |\n|---|---|---|\n| `OrchestratorClient` | Class | REST client for the API (below). |\n| `OrchestratorError` | Class | Thrown by every client method on a non-2xx: `status`, `code`, `message`, `details`. |\n| `useBuild` | Hook | `(client, buildId \\| null)` → `{ build, logs, error }`; a build + its logs, live, until terminal. |\n| `useSimulation` | Hook | `(client, simulationId \\| null)` → `{ simulation, logs, stream, error }`; a simulation + its logs, live, surfacing `stream` once `streaming`. |\n| `useBuilds` | Hook | `(client, query?)` → `{ builds, nextCursor, error }`; a page of builds, kept current, forever. |\n| `useSimulations` | Hook | `(client, query?)` → `{ simulations, nextCursor, error }`. |\n| `useGhaJobs` | Hook | `(client, query?)` → `{ jobs, nextCursor, error }`. |\n| `useWorkers` | Hook | `(client)` → `{ workers, error }`; worker health and slot usage. |\n| `SimulationScreen` | Component | All-in-one live simulation: connects to `stream`, decodes video, renders a drivable canvas. |\n| `SimulatorCanvas` | Component | Lower-level canvas renderer (click = tap, drag = swipe); used by `SimulationScreen`. |\n| `SimulatorDecoder` | Class | WebCodecs H.264/AVCC decoder painting to a `<canvas>` (advanced). |\n| `LogConsole` | Component | Scrollable, color-coded, auto-scrolling log viewer. |\n| `StatusBadge` | Component | Colored pill for any `PipelineStatus`: build, simulation, or GitHub Actions job. |\n\nThe list hooks never stop: a list has no terminal state, since new jobs keep\narriving. A matching job event invalidates the page and schedules a coalesced\nrefetch, so a burst of jobs changing at once is one request rather than one each,\nand an idle system makes none. A failed read sets `error` and keeps the last good\npage on screen.\n\n### `<SimulationScreen>` props (`SimulationScreenProps`)\n\n| Prop | Type | Required | Default | Description |\n|---|---|---|---|---|\n| `stream` | `SimulationStream` | yes | n/a | The simulation's direct-stream descriptor (`Simulation.stream` / `useSimulation().stream`). |\n| `onRotate` | `(o: Orientation) => void \\| Promise<void>` | no | n/a | Rotate the device; omit and the rotate controls are not rendered. |\n| `orientation` | `Orientation` | no | n/a | The server's truth for where the device is turned; without it a rotated device paints sideways on reload. |\n| `fps` | `number` | no | `10` | Target frame rate. |\n| `bitrate` | `number` | no | `4_000_000` | Target bitrate in bits/sec. |\n| `maxHeight` | `number \\| string` | no | `\"80vh\"` | Cap on the rendered screen height. Ignored by `chrome=\"compact\"`, which sizes itself. |\n| `layout` | `\"stacked\" \\| \"split\"` | no | `\"stacked\"` | Controls under the screen, or beside it. Ignored by `chrome=\"compact\"`. |\n| `chrome` | `boolean \\| \"compact\"` | no | `true` | `true`: full controls panel. `false`: bare stream. `\"compact\"`: fill a fixed-height container with the picture plus a one-row toolbar (status, Home, App Switcher, rotate arrows, type-to-send) and a \"More\" popover holding the preset groups and orientation grid. The picture is sized from the container and the stream's live aspect ratio — nothing scrolls, nothing is clipped. Give the container a definite height. |\n| `className` | `string` | no | n/a | CSS class on the root element. |\n\n### `<LogConsole>` props (`LogConsoleProps`)\n\n| Prop | Type | Required | Default | Description |\n|---|---|---|---|---|\n| `logs` | `Log[]` | yes | n/a | Log entries (`{ level, message, timestamp }`). |\n| `title` | `string` | no | `\"Logs\"` | Heading text. |\n| `maxHeight` | `string \\| number` | no | `400` | Max-height of the scroll area. |\n\n### `<StatusBadge>` props (`StatusBadgeProps`)\n\n| Prop | Type | Required | Description |\n|---|---|---|---|\n| `status` | `PipelineStatus \\| null` | yes | Build, simulation, or GHA job status to display. |\n\n### `OrchestratorClient`\n\n```ts\nconst client = new OrchestratorClient({ baseUrl, token });\n\n// Build stage\nclient.createBuild({ source: { type: \"git\", repoUrl, branch }, platform, appRoot, env });\nclient.uploadBuildSource(buildId, gzipBytes);          // for { type: \"tarball\" } sources\nclient.getBuild(buildId);                              // → Build\nclient.listBuilds({ limit, cursor, status });          // → BuildList { items, nextCursor }\nclient.getBuildLogs(buildId, since);                   // → LogPage { logs, nextSince }\nclient.cancelBuild(buildId);\n\n// Simulation stage\nclient.createSimulation({ buildId /* or artifactId */, simulatorName, osVersion, devServerUrl });\nclient.getSimulation(simulationId);                    // → Simulation (with `stream` once streaming)\nclient.listSimulations({ limit, cursor, status });     // → SimulationList\nclient.getSimulationLogs(simulationId, since);         // → LogPage\nclient.cancelSimulation(simulationId);\n\n// Driving a simulation over REST (a streaming viewer should use its own socket instead)\nclient.setSimulationOrientation(simulationId, \"landscape-left\");\nclient.sendInput(simulationId, { type: \"tap\", x, y, width, height });\nclient.screenshot(simulationId, { scale, quality });   // → Blob\nclient.describeUi(simulationId, { x, y });             // → { tree }\n\n// Overview\nclient.listGhaJobs({ limit, cursor, status });         // → GhaJobList\nclient.getWorkers();                                   // → { workers }\nclient.getDeviceCatalog();                             // → DeviceCatalog: what the simulator host can boot\n\n// Live events (see above): returns a handle you must close\nclient.openEvents({ logOwner, logId, since, onJob, onLog, onWorkers, onStatus, onFatal });\n```\n\nPass `getBuildLogs`/`getSimulationLogs` the previous response's `nextSince` to fetch\nonly newer lines, and carry it forward even when a page came back empty. Dropping\nit and the next read rewinds to the start of the log. The `id` on a `log` event is\nthe same cursor, so the two ways of reading logs interoperate exactly.\n\nEvery method throws `OrchestratorError` on failure, carrying the API's own\n`code` so a caller can branch on it instead of matching an English sentence.\n\nThe wire types (`Build`, `Simulation`, `SimulationStream`, `Log`, `DeviceInput`,\n`Orientation`, …) are re-exported from `@alivelabs/mobile-schemas`.","readmeFilename":"README.md"}