diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 8d3ca8814..383ceb89f 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -31,15 +31,19 @@ jobs: node-version: "24" # ships npm >= 11.5.1 — required for trusted publishing registry-url: "https://registry.npmjs.org" - # Install workspace deps (root + sdk + host workspaces, hoisted - # into js/node_modules/) so the host bundles can be built. - - name: Install JS workspace dependencies + # Install deps for the single npm package + # (@pollen-robotics/reachy-mini-sdk). The SDK runtime is plain + # JS at js/reachy-mini-sdk.js; the optional host shell lives + # under js/host/ and is bundled by `npm run build` into + # js/host/dist/, surfaced via the `./host*` subpath exports. + - name: Install JS package dependencies run: npm ci - # Build host bundles into js/host/dist/. Required for the host - # package's main/exports to resolve in its published tarball. + # Build the host bundles into host/dist/. Required for the + # `./host`, `./host/auto`, `./host/embed`, and `./host/protocol` + # subpath exports to resolve in the published tarball. - name: Build host bundles - run: npm run build:host + run: npm run build # pyproject.toml is the single source of truth for the version. # Release → publish on the `latest` dist-tag. @@ -55,28 +59,19 @@ jobs: ver="${py_ver}" fi echo "Computed version: $ver (event=${{ github.event_name }})" - npm version "$ver" --no-git-tag-version --workspace=sdk - npm version "$ver" --no-git-tag-version --workspace=host + npm version "$ver" --no-git-tag-version - # PR: pack both workspaces to confirm the publish would succeed - # (tarball layout, manifest validity, no missing files) without + # PR: pack to confirm the publish would succeed (tarball + # layout, manifest validity, no missing files) without # actually publishing. - - name: Verify tarballs (PR dry-run) + - name: Verify tarball (PR dry-run) if: github.event_name == 'pull_request' - run: | - npm pack --workspace=sdk --dry-run - npm pack --workspace=host --dry-run + run: npm pack --dry-run - - name: Publish SDK (latest) - if: github.event_name == 'release' - run: npm publish --workspace=sdk - - name: Publish Host (latest) + - name: Publish (latest) if: github.event_name == 'release' - run: npm publish --workspace=host + run: npm publish - - name: Publish SDK (main) - if: github.event_name == 'push' - run: npm publish --workspace=sdk --tag main - - name: Publish Host (main) + - name: Publish (main) if: github.event_name == 'push' - run: npm publish --workspace=host --tag main + run: npm publish --tag main diff --git a/.gitignore b/.gitignore index 29cd2bf7f..e50f26b92 100644 --- a/.gitignore +++ b/.gitignore @@ -262,9 +262,8 @@ src/reachy_mini/tools/camera_calibration/images_for_crop_analysis/* src/reachy_mini/tools/camera_calibration/images_for_crop_analysis_lite/* src/reachy_mini/tools/camera_calibration/*.yaml -# JavaScript (js/ workspace: SDK + host) +# JavaScript (single npm package at js/) js/node_modules/ -js/host/node_modules/ js/host/dist/ js/.vite/ # The unanchored `lib/` rule above (Python template, intended for diff --git a/AGENTS.md b/AGENTS.md index edf3448df..72600df96 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -279,7 +279,7 @@ Lifecycle (advanced — most consumers should just use `autoConnect()`): `authen Events: `connected`, `disconnected`, `robotsChanged`, `streaming`, `sessionStopped`, `sessionRejected` (robot busy — inspect `e.detail.activeApp`), `state` (every ~500 ms), `videoTrack`, `micSupported`, `error`. -**Full API:** read the top ~90 lines of [`js/sdk/reachy-mini-sdk.js`](js/sdk/reachy-mini-sdk.js) — the file header is a complete reference. +**Full API:** read the top ~90 lines of [`js/reachy-mini-sdk.js`](js/reachy-mini-sdk.js) — the file header is a complete reference. ### Iframe-embedded apps (mobile shell, vibe-coder preview) diff --git a/docs/source/SDK/javascript-sdk.md b/docs/source/SDK/javascript-sdk.md index 57e1c9b61..caae8d8a8 100644 --- a/docs/source/SDK/javascript-sdk.md +++ b/docs/source/SDK/javascript-sdk.md @@ -82,6 +82,13 @@ You can grab `reachy-mini-sdk.js` from the [reference example](https://huggingfa import { ReachyMini } from "https://cdn.jsdelivr.net/npm/@pollen-robotics/reachy-mini-sdk/+esm"; ``` +> The same npm package also ships the optional **host shell** (OAuth + robot picker + iframe lifecycle) for apps deployed as Hugging Face Spaces, under the `./host*` subpath exports: +> ```js +> import { mountHost } from "@pollen-robotics/reachy-mini-sdk/host/auto"; +> import { connectToHost } from "@pollen-robotics/reachy-mini-sdk/host/embed"; +> ``` +> See the [host README](https://github.com/pollen-robotics/reachy_mini/tree/main/js/host) for the full integration recipe. + ### 3. Connect to your robot ```js diff --git a/js/CHANGELOG.md b/js/CHANGELOG.md new file mode 100644 index 000000000..7bf204661 --- /dev/null +++ b/js/CHANGELOG.md @@ -0,0 +1,188 @@ +# Changelog + +All notable changes to `@pollen-robotics/reachy-mini-sdk` are +documented here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions +are kept in lock-step with the Reachy Mini Python daemon and are +driven from `pyproject.toml` at the repo root (the npm publish CI +overrides the `version` placeholder in `package.json`). + +The host shell shares this package's version (single source of +truth); the wire protocol is versioned separately in +`PROTOCOL_VERSION` (see [host/SPEC.md §11](./host/SPEC.md#11-backlog)). + +## Unreleased — succeeds 1.7.3 + +> **Breaking change.** Every app currently using +> `@pollen-robotics/reachy-mini-host` has to update its imports +> when it upgrades. Existing `1.7.x` installs keep working against +> the legacy `reachy-mini-host@1.7.x` tarball that stays on npm; +> only consumers who upgrade past `1.7.3` need to migrate. The +> target version is intentionally left unset in this PR — it will +> be picked at release time when `pyproject.toml` is bumped from +> `1.7.3` to the next number (the npm publish CI mirrors that bump +> into the package manifest). + +**Single-package release: `@pollen-robotics/reachy-mini-host` is folded +into `@pollen-robotics/reachy-mini-sdk`.** App authors install, +version, and import from one entry point. The old package stays +available on npm at `1.7.x` for one minor cycle for graceful +migration and will then be `npm deprecate`d with a pointer to the +new subpaths. + +### Migration + +```diff +- "@pollen-robotics/reachy-mini-host": "^1.7.x", +- "@pollen-robotics/reachy-mini-sdk": "^1.7.x" ++ "@pollen-robotics/reachy-mini-sdk": "^" +``` + +```diff +- import { mountHost } from '@pollen-robotics/reachy-mini-host/auto'; +- import { connectToHost } from '@pollen-robotics/reachy-mini-host/embed'; +- import { PROTOCOL_VERSION } from '@pollen-robotics/reachy-mini-host/protocol'; ++ import { mountHost } from '@pollen-robotics/reachy-mini-sdk/host/auto'; ++ import { connectToHost } from '@pollen-robotics/reachy-mini-sdk/host/embed'; ++ import { PROTOCOL_VERSION } from '@pollen-robotics/reachy-mini-sdk/host/protocol'; +``` + +The host CDN bundles also moved: + +```diff +- https://cdn.jsdelivr.net/npm/@pollen-robotics/reachy-mini-host@1/dist/entry/auto.js ++ https://cdn.jsdelivr.net/npm/@pollen-robotics/reachy-mini-sdk@1/host/dist/entry/auto.js +``` + +### Added + +- New subpath exports on `@pollen-robotics/reachy-mini-sdk`: + - `./host` — `mountHost`, `connectToHost`, types + - `./host/auto` — CDN auto bundle for standalone apps + - `./host/embed` — CDN embed bundle for the iframe side + - `./host/protocol` — `PROTOCOL_VERSION`, `decodeCredsFromHash`, etc. +- `reachy-mini-sdk.d.ts` ships next to `reachy-mini-sdk.js` as the + canonical SDK type surface. The host re-exports from there + (`host/src/lib/sdk-types.ts` is now a thin barrel), removing the + earlier duplication TODO. + +### Changed + +- The host bundles (`./host`, `./host/auto`, `./host/embed`) now + import the SDK runtime directly and self-assign + `window.ReachyMini` (when unset) at module-load time, dispatching + `reachymini:ready`. App `index.html` files no longer need a + separate ` + + + ``` -## Publishing +Both bundles auto-install the SDK on `window.ReachyMini` at load time, so an app that uses the host no longer needs a separate ` @@ -61,7 +63,7 @@ npm install --save-dev @pollen-robotics/reachy-mini-sdk The CDN URL pins to the **major** version (`@1`), so patch + minor releases land in every Space automatically; only a deliberate breaking change requires each app to update its tag. The host shares its version with the SDK and the -daemon (single repo, single release). +daemon (single repo, single release, single npm package). ## 90-second tour @@ -96,7 +98,7 @@ if (isEmbed) { **`src/host.ts`** — mounts the shell: ```ts -import { mountHost } from '@pollen-robotics/reachy-mini-host/auto'; +import { mountHost } from '@pollen-robotics/reachy-mini-sdk/host/auto'; mountHost({ appName: 'My App', @@ -108,7 +110,7 @@ mountHost({ **`src/embed.ts`** — connects and runs the app: ```ts -import { connectToHost } from '@pollen-robotics/reachy-mini-host/embed'; +import { connectToHost } from '@pollen-robotics/reachy-mini-sdk/host/embed'; const handle = await connectToHost(); handle.reachy.setHeadRpyDeg(0, 10, 0); @@ -146,7 +148,7 @@ protocol** is tracked separately in `PROTOCOL_VERSION` and bumped only on incompatible postMessage changes (cf. [SPEC §11](./SPEC.md#11-backlog)). App authors should pin to a major (`@1`) in their CDN URL and audit the -[CHANGELOG](./CHANGELOG.md) on each minor bump. +[CHANGELOG](../CHANGELOG.md) on each minor bump. ## License diff --git a/js/host/SPEC.md b/js/host/SPEC.md index e0555b740..1d36c71e4 100644 --- a/js/host/SPEC.md +++ b/js/host/SPEC.md @@ -5,7 +5,7 @@ | 1.0 | Active | 2026-05-16 | Source-of-truth for what a Reachy Mini app must do at boot, what -`@pollen-robotics/reachy-mini-host` does for it, and the contract between the +`@pollen-robotics/reachy-mini-sdk/host` does for it, and the contract between the shell, the embed and the app code. This file describes **observable behaviour and invariants**. The @@ -49,14 +49,14 @@ Three actors, one app repository: | Actor | Lives in | Owns | |------------|-----------------------------------|----------------------------------------------------------------------------| | **App** | `index.html` + `src/dispatch.ts` | UI, app-specific UX, **and full freedom over framework / tooling choices** | -| **Host** | `@pollen-robotics/reachy-mini-host/auto` (CDN) | OAuth, robot discovery, robot picker, connecting overlay, end-session flow | -| **Embed** | `@pollen-robotics/reachy-mini-host/embed` (CDN) | SDK lifecycle inside the iframe (`startSession`, `ensureAwake`, teardown) | +| **Host** | `@pollen-robotics/reachy-mini-sdk/host/auto` (CDN) | OAuth, robot discovery, robot picker, connecting overlay, end-session flow | +| **Embed** | `@pollen-robotics/reachy-mini-sdk/host/embed` (CDN) | SDK lifecycle inside the iframe (`startSession`, `ensureAwake`, teardown) | The **App** consumes: - the Reachy Mini SDK (loaded as a CDN module by `index.html`, exposed as `window.ReachyMini`), -- the `@pollen-robotics/reachy-mini-host` package (npm dep for types, CDN entries +- the `@pollen-robotics/reachy-mini-sdk/host` package (npm dep for types, CDN entries at runtime). The App contains zero auth code, zero picker code, zero @@ -111,7 +111,7 @@ files in §5. ### Why React + MUI for the host (and only the host) -The host shell (`@pollen-robotics/reachy-mini-host/auto`) is built with **React + +The host shell (`@pollen-robotics/reachy-mini-sdk/host/auto`) is built with **React + MUI + Emotion**. This is an explicit, assumed choice: - The shell needs a real component library: forms (sign-in), @@ -146,7 +146,7 @@ The same `index.html` is served for both modes. The dispatcher if (URL.searchParams.has("embedded") && URL.hash.startsWith("#creds=")): boot embed -> import("./embed") else: - boot host -> import("@pollen-robotics/reachy-mini-host/auto").mountHost({...}) + boot host -> import("@pollen-robotics/reachy-mini-sdk/host/auto").mountHost({...}) ``` `?embedded=1` without creds is an invalid mode - render `ErrorView`. @@ -474,8 +474,8 @@ can be rolled out by the SDK team, not the app teams. on deploy. - Reachy Mini SDK in `index.html`: pinned to a **git tag or commit SHA**, never to a branch. jsdelivr caches branches for ~12 h. -- `@pollen-robotics/reachy-mini-host` from CDN: pinned to a **major version** - (`@pollen-robotics/reachy-mini-host@1`) on jsdelivr/unpkg. +- `@pollen-robotics/reachy-mini-sdk/host` from CDN: pinned to a **major version** + (`@pollen-robotics/reachy-mini-sdk@1`) on jsdelivr/unpkg. On detected mismatch (e.g. unknown protocol version, structurally invalid `host:init`), the host's ErrorView's primary button calls @@ -588,11 +588,12 @@ narrow but not zero. Items below are tracked but **not** in §8 yet. Promote to §8 only when implemented and the cost of regressing them is justified. -- **CDN-only host**: today every app vendors `dist/` into - `vendor/reachy-mini-host/` to work around HF Spaces' Docker - build refusing `file:../` deps. Once `@pollen-robotics/reachy-mini-host` is - published to npm, the dispatcher should import from the jsdelivr - bundle so a host fix reaches every Space without per-app +- **CDN-only host**: the historical workaround of vendoring + `dist/` into `vendor/reachy-mini-host/` to placate HF Spaces' + Docker build (which refused `file:../` deps) is no longer + needed now that `@pollen-robotics/reachy-mini-sdk/host` is + published to npm. The dispatcher imports from the jsdelivr + bundle, so a host fix reaches every Space without per-app redeploys. - **Heartbeat / liveness**: detect a frozen embed (JS infinite loop in a same-tab iframe). Trigger if we observe a freeze in @@ -608,7 +609,7 @@ when implemented and the cost of regressing them is justified. `resumeSession()` so we re-run the WebRTC handshake without going through the picker again. - **Mobile parity for ConnectingView**: export a tiny - `` from `@pollen-robotics/reachy-mini-host/embed` so + `` from `@pollen-robotics/reachy-mini-sdk/host/embed` so Mode B apps reuse the 3-step visual without pulling React. - **Observability hook**: opt-in callback for app authors / HF ops to collect a redacted trace of every session. diff --git a/js/host/package.json b/js/host/package.json deleted file mode 100644 index 85e1fb0fe..000000000 --- a/js/host/package.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "name": "@pollen-robotics/reachy-mini-host", - "version": "0.0.0-managed-by-ci", - "description": "Host shell rendered around Reachy Mini apps deployed as Hugging Face Spaces: OAuth, robot picker, session lifecycle, and parent/iframe postMessage bridge.", - "type": "module", - "main": "./dist/index.js", - "module": "./dist/index.js", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./auto": { - "types": "./dist/entry/auto.d.ts", - "import": "./dist/entry/auto.js" - }, - "./embed": { - "types": "./dist/entry/embed.d.ts", - "import": "./dist/entry/embed.js" - }, - "./protocol": { - "types": "./dist/lib/protocol.d.ts", - "import": "./dist/lib/protocol.js" - }, - "./package.json": "./package.json" - }, - "files": [ - "dist", - "README.md", - "SPEC.md", - "APP_AUTHOR_GUIDE.md", - "CHANGELOG.md", - "LICENSE" - ], - "sideEffects": false, - "scripts": { - "dev": "vite", - "build": "vite build && tsc -p tsconfig.build.json", - "build:watch": "vite build --watch", - "typecheck": "tsc --noEmit", - "clean": "rm -rf dist" - }, - "dependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^7.3.5", - "@mui/material": "^7.3.5" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "react": { "optional": true }, - "react-dom": { "optional": true } - }, - "devDependencies": { - "@types/node": "^25.7.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react-swc": "^4.0.0", - "react": "^19.2.5", - "react-dom": "^19.2.5", - "typescript": "^5.7.3", - "vite": "^7.2.1" - }, - "keywords": [ - "reachy", - "reachy-mini", - "pollen-robotics", - "huggingface-spaces", - "host", - "iframe", - "shell", - "react" - ], - "author": "Pollen Robotics", - "license": "Apache-2.0", - "homepage": "https://github.com/pollen-robotics/reachy_mini#readme", - "bugs": { - "url": "https://github.com/pollen-robotics/reachy_mini/issues" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/pollen-robotics/reachy_mini.git", - "directory": "js/host" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/js/host/src/components/ReachyHostShell.tsx b/js/host/src/components/ReachyHostShell.tsx index 345686caa..d484c1512 100644 --- a/js/host/src/components/ReachyHostShell.tsx +++ b/js/host/src/components/ReachyHostShell.tsx @@ -324,7 +324,7 @@ function ReachyHostShellNormal({ setHostPhase('error'); } else { console.warn( - '[reachy-mini-host] embed reported non-fatal error:', + '[reachy-mini-sdk/host] embed reported non-fatal error:', message, detail, ); diff --git a/js/host/src/embed/index.ts b/js/host/src/embed/index.ts index 0db9f44a3..2c0aefc0b 100644 --- a/js/host/src/embed/index.ts +++ b/js/host/src/embed/index.ts @@ -3,10 +3,10 @@ * * Vanilla TypeScript helper that lives in the iframe side of the * host / app split. Consumed by `src/embed.{ts,tsx}` in each app - * (or via the CDN entry `@pollen-robotics/reachy-mini-host/embed` - * script tag). + * (or via the CDN entry + * `@pollen-robotics/reachy-mini-sdk/host/embed` script tag). * - * import { connectToHost } from '@pollen-robotics/reachy-mini-host/embed'; + * import { connectToHost } from '@pollen-robotics/reachy-mini-sdk/host/embed'; * * const handle = await connectToHost(); * handle.onLeave(() => { /* clean up before unmount *\/ }); @@ -230,7 +230,7 @@ async function bootOnce( if (!creds) { throw new Error( - '[reachy-mini-host/embed] no creds bundle found in URL hash. ' + + '[reachy-mini-sdk/host/embed] no creds bundle found in URL hash. ' + 'Was the embed mounted directly without ?embedded=1#creds=...?', ); } @@ -239,7 +239,7 @@ async function bootOnce( const sdkReady = await waitForSdkReady(SDK_READY_TIMEOUT_MS); if (!sdkReady) { throw new Error( - '[reachy-mini-host/embed] window.ReachyMini did not become ' + + '[reachy-mini-sdk/host/embed] window.ReachyMini did not become ' + `available within ${SDK_READY_TIMEOUT_MS}ms - check the SDK CDN tag.`, ); } @@ -402,7 +402,7 @@ function createBridge(expectedOrigin: string) { try { void cb(); } catch (err) { - console.warn('[reachy-mini-host/embed] onLeave threw', err); + console.warn('[reachy-mini-sdk/host/embed] onLeave threw', err); } }); } @@ -602,7 +602,7 @@ function postToHost(msg: EmbedToHostMsg): void { try { window.parent.postMessage(msg, parentTargetOrigin); } catch (err) { - console.warn('[reachy-mini-host/embed] postMessage to host failed', err); + console.warn('[reachy-mini-sdk/host/embed] postMessage to host failed', err); } } diff --git a/js/host/src/entry/auto.ts b/js/host/src/entry/auto.ts index 184585594..056432dd2 100644 --- a/js/host/src/entry/auto.ts +++ b/js/host/src/entry/auto.ts @@ -10,7 +10,19 @@ * The auto bundle exposes `mountHost` on `window.ReachyMiniHost` * for legacy / non-module callers. ESM consumers import it the * normal way. + * + * SDK auto-install + * ──────────────── + * The host depends on `window.ReachyMini` for its OAuth helpers + * (`useSdk`) and the embed client. Now that the SDK lives in the + * same npm package, we import it directly and assign it to + * `window.ReachyMini` here as a side effect, then dispatch the + * `reachymini:ready` event the host's wait loops listen for. + * Existing apps that still set `window.ReachyMini` themselves + * (e.g. via the old jsdelivr `