From 34211985375b1874819f1e5f23038dd9c6caf82e Mon Sep 17 00:00:00 2001 From: Jonathan Hinkel Date: Mon, 7 Sep 2026 12:44:48 +0200 Subject: [PATCH] Convert to an npm-workspaces monorepo (packages/core, packages/humanoid, apps/template) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the SvelteKit app into three workspaces per docs/MONOREPO_MIGRATION_BRIEF.md: - packages/core (@webavatar/webavatar-core): session management, auth tokens, provider adapters, and the WS relay, with curated entry points (., ./client, ./server, ./vite) and a browser-stub guard on the server-only ones. - packages/humanoid (@webavatar/webavatar-humanoid): avatar-state derivation and animation-channel composition, depending on core only via type-only imports (no runtime reference to core in its built output). - apps/template: the SvelteKit application template, now importing both packages by name, routed through a server-only wrapper and a build-time guard (server-only-guard.ts) for the server/vite entries. Also collapses the duplicated server.js/vite-plugin composition wiring into a single createRealtimeRuntime factory (exported from packages/core/server), adds ESLint boundary-enforcement rules and Gitea Actions workflows (verify.yml, publish.yml), externalizes both packages in the production SSR build for artifact parity with a real client install, and rewrites the project documentation to match. No runtime behavior or wire protocol changes. Registry publication stays blocked on Gitea host/org placeholders not yet resolved — see the brief's status banner for exactly what remains. Co-Authored-By: Claude Sonnet 5 --- .claude/launch.json | 2 +- .gitea/workflows/publish.yml | 30 ++ .gitea/workflows/verify.yml | 29 ++ .gitignore | 9 +- .npmrc | 1 + .prettierignore | 2 +- AGENTS.md | 22 +- README.md | 49 +- .env.example => apps/template/.env.example | 0 apps/template/README.md | 49 ++ apps/template/package.json | 49 ++ .../template/playwright.config.ts | 0 apps/template/server-only-guard.ts | 27 ++ apps/template/server.js | 23 + {src => apps/template/src}/app.d.ts | 0 {src => apps/template/src}/app.html | 0 .../template/src}/lib/assets/favicon.svg | 0 .../src/lib/client}/DebugPanel.svelte | 17 +- .../src/lib/client}/DebugPanel.svelte.spec.ts | 2 +- .../lib/client/project/tools/flash-page.ts | 2 +- .../src}/lib/client/project/tools/index.ts | 2 +- {src => apps/template/src}/lib/index.ts | 0 .../template/src}/lib/project/persona.ts | 14 +- .../template/src}/lib/project/tools.ts | 2 +- .../server/project/tools/get-current-time.ts | 2 +- .../src}/lib/server/project/tools/index.ts | 2 +- .../template/src/lib/server/webavatar-core.ts | 1 + .../template/src}/routes/+layout.svelte | 0 .../template/src}/routes/+page.svelte | 0 .../routes/api/realtime/session/+server.ts | 3 +- .../src}/routes/demo/+layout.server.ts | 0 .../src}/routes/demo/humanoid/+page.svelte | 0 .../demo/humanoid/AvatarStateDemo.svelte | 16 +- .../routes/demo/humanoid/EyeSystemDemo.svelte | 19 +- .../humanoid/eye-demo/GazeDebugPanel.svelte | 5 +- .../eye-demo/GazePlaneVisualizer.svelte | 0 .../eye-demo/ThreeAvatarViewer.svelte | 2 +- .../src}/routes/demo/realtime/+page.svelte | 14 +- .../template/static}/models/avatar.glb | 0 {static => apps/template/static}/robots.txt | 0 tsconfig.json => apps/template/tsconfig.json | 18 +- .../template/tsconfig.server.json | 7 +- .../template/vite.config.ts | 34 +- docs/ANALYTICS.md | 28 +- docs/ARCHITECTURE.md | 38 +- docs/CONFIGURATION.md | 14 +- docs/HUMANOID.md | 30 +- docs/LOGGING.md | 18 +- docs/MONOREPO_MIGRATION_BRIEF.md | 100 ++-- docs/REALTIME_SESSION.md | 44 +- docs/TOOLS.md | 26 +- eslint.config.js | 80 +++ package-lock.json | 458 ++++++++++++++++-- package.json | 52 +- packages/core/README.md | 40 ++ packages/core/package.json | 60 +++ .../core/src/client}/client.ts | 2 +- packages/core/src/client/index.ts | 22 + .../core/src/client}/logger.spec.ts | 0 .../core/src/client}/logger.ts | 2 +- .../core/src/client}/resilient-client.spec.ts | 2 +- .../core/src/client}/resilient-client.ts | 2 +- .../src/client}/server-log-forwarder.spec.ts | 2 +- .../core/src/client}/server-log-forwarder.ts | 2 +- .../core/src/client}/tool-dispatcher.spec.ts | 2 +- .../core/src/client}/tool-dispatcher.ts | 4 +- packages/core/src/index.ts | 16 + {src/lib/core => packages/core/src}/logger.ts | 0 packages/core/src/persona.ts | 11 + .../core => packages/core/src}/protocol.ts | 0 .../core/src/server}/analytics.spec.ts | 0 .../core/src/server}/analytics.ts | 0 packages/core/src/server/browser-stub.ts | 3 + .../core/src/server}/config.spec.ts | 0 .../core/src/server}/config.ts | 0 .../core/src/server}/defaults.spec.ts | 0 .../core/src/server}/defaults.ts | 2 +- packages/core/src/server/index.ts | 96 ++++ .../core/src/server}/logger.spec.ts | 0 .../core/src/server}/logger.ts | 2 +- .../core/src/server}/provider.ts | 4 +- .../src/server}/providers/fixtures/default.ts | 0 .../core/src/server}/providers/index.ts | 0 .../core/src/server}/providers/mock.spec.ts | 2 +- .../core/src/server}/providers/mock.ts | 2 +- .../core/src/server}/providers/openai.spec.ts | 2 +- .../core/src/server}/providers/openai.ts | 4 +- .../core/src/server}/session.spec.ts | 4 +- .../core/src/server}/session.ts | 6 +- .../core/src/server}/tokens.spec.ts | 0 .../core/src/server}/tokens.ts | 0 .../core/src/server}/tools.spec.ts | 2 +- .../core/src/server}/tools.ts | 2 +- .../core/src/server}/ws-handler.ts | 2 +- {src/lib/core => packages/core/src}/tools.ts | 0 packages/core/src/vite/index.ts | 57 +++ packages/core/tsconfig.build.json | 12 + packages/core/tsconfig.json | 11 + packages/core/vitest.config.ts | 9 + packages/humanoid/README.md | 28 ++ packages/humanoid/package.json | 47 ++ .../humanoid/src}/animation.spec.ts | 0 .../humanoid/src}/animation.ts | 0 .../humanoid/src/client}/avatar-state.spec.ts | 2 +- .../humanoid/src/client}/avatar-state.ts | 6 +- packages/humanoid/src/client/index.ts | 1 + .../humanoid/src}/config.spec.ts | 0 .../humanoid/src}/config.ts | 0 .../humanoid/src}/eye/DESIGN_NOTES.md | 0 .../humanoid/src}/eye/blend-shapes.spec.ts | 0 .../humanoid/src}/eye/blend-shapes.ts | 0 .../humanoid/src/eye/client.spec.ts | 4 +- .../humanoid/src/eye/client.ts | 10 +- .../humanoid/src}/eye/config.spec.ts | 0 .../humanoid/src}/eye/config.ts | 0 .../humanoid/src}/eye/curves.spec.ts | 0 .../humanoid/src}/eye/curves.ts | 0 .../humanoid/src}/eye/gaze.spec.ts | 0 .../humanoid/src}/eye/gaze.ts | 0 .../humanoid/src}/eye/index.ts | 0 packages/humanoid/src/index.ts | 3 + .../humanoid/src}/state.spec.ts | 0 .../humanoid/src}/state.ts | 0 packages/humanoid/tsconfig.build.json | 12 + packages/humanoid/tsconfig.json | 10 + packages/humanoid/vitest.config.ts | 9 + server.js | 55 --- src/lib/server/core/dev-plugin.ts | 81 ---- tsconfig.base.json | 10 + 129 files changed, 1506 insertions(+), 503 deletions(-) create mode 100644 .gitea/workflows/publish.yml create mode 100644 .gitea/workflows/verify.yml rename .env.example => apps/template/.env.example (100%) create mode 100644 apps/template/README.md create mode 100644 apps/template/package.json rename playwright.config.ts => apps/template/playwright.config.ts (100%) create mode 100644 apps/template/server-only-guard.ts create mode 100644 apps/template/server.js rename {src => apps/template/src}/app.d.ts (100%) rename {src => apps/template/src}/app.html (100%) rename {src => apps/template/src}/lib/assets/favicon.svg (100%) rename {src/lib/client/core => apps/template/src/lib/client}/DebugPanel.svelte (93%) rename {src/lib/client/core => apps/template/src/lib/client}/DebugPanel.svelte.spec.ts (96%) rename {src => apps/template/src}/lib/client/project/tools/flash-page.ts (86%) rename {src => apps/template/src}/lib/client/project/tools/index.ts (74%) rename {src => apps/template/src}/lib/index.ts (100%) rename {src => apps/template/src}/lib/project/persona.ts (70%) rename {src => apps/template/src}/lib/project/tools.ts (94%) rename {src => apps/template/src}/lib/server/project/tools/get-current-time.ts (63%) rename {src => apps/template/src}/lib/server/project/tools/index.ts (89%) create mode 100644 apps/template/src/lib/server/webavatar-core.ts rename {src => apps/template/src}/routes/+layout.svelte (100%) rename {src => apps/template/src}/routes/+page.svelte (100%) rename {src => apps/template/src}/routes/api/realtime/session/+server.ts (67%) rename {src => apps/template/src}/routes/demo/+layout.server.ts (100%) rename {src => apps/template/src}/routes/demo/humanoid/+page.svelte (100%) rename {src => apps/template/src}/routes/demo/humanoid/AvatarStateDemo.svelte (85%) rename {src => apps/template/src}/routes/demo/humanoid/EyeSystemDemo.svelte (83%) rename {src => apps/template/src}/routes/demo/humanoid/eye-demo/GazeDebugPanel.svelte (96%) rename {src => apps/template/src}/routes/demo/humanoid/eye-demo/GazePlaneVisualizer.svelte (100%) rename {src => apps/template/src}/routes/demo/humanoid/eye-demo/ThreeAvatarViewer.svelte (98%) rename {src => apps/template/src}/routes/demo/realtime/+page.svelte (90%) rename {static => apps/template/static}/models/avatar.glb (100%) rename {static => apps/template/static}/robots.txt (100%) rename tsconfig.json => apps/template/tsconfig.json (53%) rename tsconfig.server.json => apps/template/tsconfig.server.json (71%) rename vite.config.ts => apps/template/vite.config.ts (62%) create mode 100644 packages/core/README.md create mode 100644 packages/core/package.json rename {src/lib/client/core => packages/core/src/client}/client.ts (98%) create mode 100644 packages/core/src/client/index.ts rename {src/lib/client/core => packages/core/src/client}/logger.spec.ts (100%) rename {src/lib/client/core => packages/core/src/client}/logger.ts (97%) rename {src/lib/client/core => packages/core/src/client}/resilient-client.spec.ts (98%) rename {src/lib/client/core => packages/core/src/client}/resilient-client.ts (98%) rename {src/lib/client/core => packages/core/src/client}/server-log-forwarder.spec.ts (98%) rename {src/lib/client/core => packages/core/src/client}/server-log-forwarder.ts (97%) rename {src/lib/client/core => packages/core/src/client}/tool-dispatcher.spec.ts (97%) rename {src/lib/client/core => packages/core/src/client}/tool-dispatcher.ts (93%) create mode 100644 packages/core/src/index.ts rename {src/lib/core => packages/core/src}/logger.ts (100%) create mode 100644 packages/core/src/persona.ts rename {src/lib/core => packages/core/src}/protocol.ts (100%) rename {src/lib/server/core => packages/core/src/server}/analytics.spec.ts (100%) rename {src/lib/server/core => packages/core/src/server}/analytics.ts (100%) create mode 100644 packages/core/src/server/browser-stub.ts rename {src/lib/server/core => packages/core/src/server}/config.spec.ts (100%) rename {src/lib/server/core => packages/core/src/server}/config.ts (100%) rename {src/lib/server/core => packages/core/src/server}/defaults.spec.ts (100%) rename {src/lib/server/core => packages/core/src/server}/defaults.ts (93%) create mode 100644 packages/core/src/server/index.ts rename {src/lib/server/core => packages/core/src/server}/logger.spec.ts (100%) rename {src/lib/server/core => packages/core/src/server}/logger.ts (98%) rename {src/lib/server/core => packages/core/src/server}/provider.ts (90%) rename {src/lib/server/core => packages/core/src/server}/providers/fixtures/default.ts (100%) rename {src/lib/server/core => packages/core/src/server}/providers/index.ts (100%) rename {src/lib/server/core => packages/core/src/server}/providers/mock.spec.ts (96%) rename {src/lib/server/core => packages/core/src/server}/providers/mock.ts (96%) rename {src/lib/server/core => packages/core/src/server}/providers/openai.spec.ts (98%) rename {src/lib/server/core => packages/core/src/server}/providers/openai.ts (98%) rename {src/lib/server/core => packages/core/src/server}/session.spec.ts (99%) rename {src/lib/server/core => packages/core/src/server}/session.ts (99%) rename {src/lib/server/core => packages/core/src/server}/tokens.spec.ts (100%) rename {src/lib/server/core => packages/core/src/server}/tokens.ts (100%) rename {src/lib/server/core => packages/core/src/server}/tools.spec.ts (96%) rename {src/lib/server/core => packages/core/src/server}/tools.ts (96%) rename {src/lib/server/core => packages/core/src/server}/ws-handler.ts (97%) rename {src/lib/core => packages/core/src}/tools.ts (100%) create mode 100644 packages/core/src/vite/index.ts create mode 100644 packages/core/tsconfig.build.json create mode 100644 packages/core/tsconfig.json create mode 100644 packages/core/vitest.config.ts create mode 100644 packages/humanoid/README.md create mode 100644 packages/humanoid/package.json rename {src/lib/humanoid => packages/humanoid/src}/animation.spec.ts (100%) rename {src/lib/humanoid => packages/humanoid/src}/animation.ts (100%) rename {src/lib/client/humanoid => packages/humanoid/src/client}/avatar-state.spec.ts (98%) rename {src/lib/client/humanoid => packages/humanoid/src/client}/avatar-state.ts (97%) create mode 100644 packages/humanoid/src/client/index.ts rename {src/lib/humanoid => packages/humanoid/src}/config.spec.ts (100%) rename {src/lib/humanoid => packages/humanoid/src}/config.ts (100%) rename {src/lib/humanoid => packages/humanoid/src}/eye/DESIGN_NOTES.md (100%) rename {src/lib/humanoid => packages/humanoid/src}/eye/blend-shapes.spec.ts (100%) rename {src/lib/humanoid => packages/humanoid/src}/eye/blend-shapes.ts (100%) rename src/lib/client/humanoid/eye/gaze-controller.spec.ts => packages/humanoid/src/eye/client.spec.ts (98%) rename src/lib/client/humanoid/eye/gaze-controller.ts => packages/humanoid/src/eye/client.ts (95%) rename {src/lib/humanoid => packages/humanoid/src}/eye/config.spec.ts (100%) rename {src/lib/humanoid => packages/humanoid/src}/eye/config.ts (100%) rename {src/lib/humanoid => packages/humanoid/src}/eye/curves.spec.ts (100%) rename {src/lib/humanoid => packages/humanoid/src}/eye/curves.ts (100%) rename {src/lib/humanoid => packages/humanoid/src}/eye/gaze.spec.ts (100%) rename {src/lib/humanoid => packages/humanoid/src}/eye/gaze.ts (100%) rename {src/lib/humanoid => packages/humanoid/src}/eye/index.ts (100%) create mode 100644 packages/humanoid/src/index.ts rename {src/lib/humanoid => packages/humanoid/src}/state.spec.ts (100%) rename {src/lib/humanoid => packages/humanoid/src}/state.ts (100%) create mode 100644 packages/humanoid/tsconfig.build.json create mode 100644 packages/humanoid/tsconfig.json create mode 100644 packages/humanoid/vitest.config.ts delete mode 100644 server.js delete mode 100644 src/lib/server/core/dev-plugin.ts create mode 100644 tsconfig.base.json diff --git a/.claude/launch.json b/.claude/launch.json index 8acb8d3..ef5988d 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -4,7 +4,7 @@ { "name": "webavatar-dev", "runtimeExecutable": "npm", - "runtimeArgs": ["run", "dev"], + "runtimeArgs": ["run", "dev", "--workspace", "apps/template"], "port": 5173 } ] diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..4633080 --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,30 @@ +name: publish +on: + push: + tags: ['core-v*', 'humanoid-v*'] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https:///api/packages//npm/ + scope: '@webavatar' + - run: npm ci + - run: npm run build:packages + - name: Resolve the workspace from the tag and check its version + run: | + case "$GITHUB_REF_NAME" in + core-v*) WS=packages/core; VERSION="${GITHUB_REF_NAME#core-v}" ;; + humanoid-v*) WS=packages/humanoid; VERSION="${GITHUB_REF_NAME#humanoid-v}" ;; + *) echo "unexpected tag $GITHUB_REF_NAME"; exit 1 ;; + esac + test "$(node -p "require('./$WS/package.json').version")" = "$VERSION" + echo "WS=$WS" >> "$GITHUB_ENV" + - run: npm test -w "$WS" + - run: npm publish -w "$WS" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml new file mode 100644 index 0000000..114ed45 --- /dev/null +++ b/.gitea/workflows/verify.yml @@ -0,0 +1,29 @@ +name: verify +on: + push: + pull_request: + +jobs: + verify: + # A runner (or container image) with Node 22+, git-lfs, and Chromium's system + # dependencies for the browser-mode Vitest project. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci # no registry token on purpose — see "Versions, ranges, and the 0.x window" + - run: npx playwright install --with-deps chromium + - run: npm run lint + - run: npm run check + - run: npm test + - run: npm run build + - run: npm publish --dry-run -w packages/core -w packages/humanoid + - name: Humanoid ships no runtime reference to core + run: if grep -rl "webavatar-core" packages/humanoid/dist --include="*.js"; then exit 1; fi + - name: The SvelteKit server build imports core by name + run: grep -rl "@webavatar/webavatar-core/server" apps/template/build/server diff --git a/.gitignore b/.gitignore index 2c8d843..191838f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,12 +5,13 @@ node_modules .vercel .netlify .wrangler -/.svelte-kit -/build -/build-server +apps/*/.svelte-kit +apps/*/build +apps/*/build-server +packages/*/dist # Analytics (local SQLite db — see docs/ANALYTICS.md) -/data +apps/*/data # OS .DS_Store diff --git a/.npmrc b/.npmrc index b6f27f1..4664db0 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ engine-strict=true +@webavatar:registry=https:///api/packages//npm/ diff --git a/.prettierignore b/.prettierignore index 7d74fe2..7a6f110 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,4 +6,4 @@ bun.lock bun.lockb # Miscellaneous -/static/ +apps/*/static/ diff --git a/AGENTS.md b/AGENTS.md index 39c3e06..389c6db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,24 +1,24 @@ # webavatar-core -Base SvelteKit project meant to be branched off for future webavatar client projects. It provides reusable core infrastructure — session management, auth tokens, realtime model connections — so client projects don't reimplement the basics from scratch. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full picture; `docs/` holds per-subsystem deep-dives beyond that. +An npm-workspaces monorepo providing reusable core infrastructure — session management, auth tokens, realtime model connections — for future webavatar client projects, plus a SvelteKit template to branch those projects from. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full picture; `docs/` holds per-subsystem deep-dives beyond that. ## Project structure -- `src/lib/core/` — shared client+server contracts (types, protocols). No SvelteKit-specific imports (`$env`, `$app`, etc.) — must stay portable. -- `src/lib/server/core/` — server-only implementation. SvelteKit enforces this boundary at build time; browser code cannot import from here, even transitively. -- `src/lib/client/core/` — thin browser-side wrappers around the shared contracts. Transport only, no business logic. -- `src/lib/project/`, `src/lib/server/project/`, `src/lib/client/project/` — mirrors the three directories above, but for code specific to whichever client project branches from this template (tool implementations, custom instructions, etc.), instead of reusable template infrastructure. **Core never imports from project, only the reverse** — see [docs/TOOLS.md](docs/TOOLS.md) for the concrete example this convention was introduced for. -- `src/lib/humanoid/`, `src/lib/client/humanoid/` — an **optional package** (not `core`, not `project`; see "Module layering" in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)) providing avatar-state derivation and animation-channel composition for 3D humanoid avatars. Nothing in `core`/`project` imports from it — a project only pulls it in if it's building a humanoid avatar. See [docs/HUMANOID.md](docs/HUMANOID.md). -- `src/routes/` — thin route glue; delegates into the above rather than holding logic itself. -- `src/routes/demo/` — core-owned verification scaffolding (the live smoke-test surface referenced under "Verifying changes" below), not a client project's UI. Gated to dev-only (404s in production) so a branched project never needs to delete it to keep it out of a shipped build. -- `docs/` — architecture and subsystem documentation (see "Documentation" below). +- `packages/core/` — `@webavatar/webavatar-core`, published-package infrastructure every client project needs: session management, auth tokens, the realtime provider adapters, and the Vite dev-server integration. Curated entry points (`.`, `./client`, `./server`, `./vite`) — see the package's own README for what each exports. No SvelteKit-specific imports (`$env`, `$app`, etc.) and no dependency on `apps/template` — it's a portable package, not app code. +- `packages/humanoid/` — `@webavatar/webavatar-humanoid`, an **optional package** (not every client project needs it) providing avatar-state derivation and animation-channel composition for 3D humanoid avatars. Depends on core's public contracts only as types (`import type`), never at runtime — its built JavaScript contains no reference to core at all. See [docs/HUMANOID.md](docs/HUMANOID.md) and the package's own README. +- `apps/template/` — the SvelteKit application template client projects branch from: routes, project-owned persona/tools, deployment configuration (`server.js`, `vite.config.ts`), and the demo pages. This is what a client project copies out and installs the packages into — see its README's "Starting a client project" procedure. Within it: + - `src/lib/project/`, `src/lib/server/project/`, `src/lib/client/project/` — code specific to whichever client project branches from the template (tool implementations, custom instructions, etc.). **Neither package imports from project code, only the reverse** — see [docs/TOOLS.md](docs/TOOLS.md) for the concrete example this convention was introduced for. + - `src/lib/server/webavatar-core.ts` — the server-only wrapper (`export * from '@webavatar/webavatar-core/server'`) that SvelteKit routes and server modules import through, so SvelteKit's own `$lib/server` build-time guard covers them; `server.js` and `vite.config.ts` are the only two files allowed to import the package's `/server` and `/vite` entries directly. + - `src/routes/` — thin route glue; delegates into the packages and project code rather than holding logic itself. + - `src/routes/demo/` — verification scaffolding (the live smoke-test surface referenced under "Verifying changes" below), not a client project's own UI. Gated to dev-only (404s in production) so a branched project never needs to delete it to keep it out of a shipped build. +- `docs/` — architecture and subsystem documentation (see "Documentation" below), at the repo root and shared across both packages and the template. ## Development -- `npm run dev` / `npm run build` / `npm run preview` — standard SvelteKit commands. `build` also compiles `src/lib/core/**` + `src/lib/server/core/**` standalone (`tsc -p tsconfig.server.json`) for production's custom `server.js` to import — see [docs/REALTIME_SESSION.md](docs/REALTIME_SESSION.md) for why that's needed. +- `npm run dev` / `npm run build` / `npm run preview` — standard SvelteKit commands, run from `apps/template` (or via `--workspace apps/template` from the root). `build` also compiles `src/lib/project/**` + `src/lib/server/project/**` standalone (`tsc -p tsconfig.server.json`) for production's custom `server.js` to import. - `npm run check` — type-check (`svelte-check`). `npm run lint` / `npm run format` — prettier + eslint. `npm run test:unit` — vitest. - Copy `.env.example` to `.env` and fill in `OPENAI_API_KEY` / `REALTIME_TOKEN_SECRET` before running anything that touches the realtime session layer. -- **Gotcha**: `src/lib/server/core/**` is only loaded via `vite.config.ts`'s plugin list, not through the app's normal module graph — so it's **not** part of Vite's HMR. Restart `npm run dev` after editing anything there; a hot-reload will silently keep running the old code, which reads as a mysterious bug if you don't know this. +- **Gotcha (verified live)**: `apps/template`'s own `npm run dev` alone does **not** hot-reload `@webavatar/webavatar-core`/`@webavatar/webavatar-humanoid` source changes — those packages resolve through their built `dist/`, and nothing recompiles it unless the package's own `tsc --watch` is also running. Use the root `npm run dev` (builds once, then watches both packages alongside `apps/template`'s dev server) for working hot reload. Separately, the realtime WS relay's dev-mode plugin wiring (`@webavatar/webavatar-core/vite`) loads once at dev-server startup and is never hot-reloaded regardless — a full restart is required after editing it. See [docs/REALTIME_SESSION.md](docs/REALTIME_SESSION.md)'s "Dev-mode reload behavior" for the full picture. ## Documentation diff --git a/README.md b/README.md index 91e3657..1e38cca 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,47 @@ -# webavatar-core +# webavatar -A base SvelteKit project meant to be branched off for future webavatar client projects. It provides the reusable core infrastructure those projects would otherwise reimplement from scratch — session management, auth tokens, and the realtime model connection — as a foundation to branch from, not a finished product. +An npm-workspaces monorepo providing reusable core infrastructure for webavatar client projects — session management, auth tokens, and the realtime model connection — as independently-versioned packages, plus a SvelteKit template to branch client projects from. -See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full architecture map, and [AGENTS.md](AGENTS.md) for project conventions and structure. `docs/` holds a deep-dive per subsystem. +See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full architecture map, [AGENTS.md](AGENTS.md) for project conventions, and `docs/` for a deep-dive per subsystem. + +## Workspaces + +| Workspace | What it is | +| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`packages/core`](packages/core) | `@webavatar/webavatar-core` — session management, auth tokens, provider adapters, the WS relay. Every client project needs this. | +| [`packages/humanoid`](packages/humanoid) | `@webavatar/webavatar-humanoid` — avatar-state derivation and animation-channel composition for 3D humanoid avatars. Optional — only projects building a humanoid avatar need it. | +| [`apps/template`](apps/template) | The SvelteKit application template client projects branch from. See its own README for the client-project bootstrap procedure. | + +Both packages are privately distributed (not published to the public npm registry) and ESM-only. ## Getting started ```sh npm install -cp .env.example .env -# fill in OPENAI_API_KEY and REALTIME_TOKEN_SECRET in .env +cp apps/template/.env.example apps/template/.env +# fill in OPENAI_API_KEY and REALTIME_TOKEN_SECRET in apps/template/.env npm run dev ``` -Open [`/demo/realtime`](http://localhost:5173/demo/realtime) for the realtime session reference wiring, or [`/demo/humanoid`](http://localhost:5173/demo/humanoid) for the humanoid avatar-state demo (works offline via `REALTIME_PROVIDER=mock`, no API key needed). +The root `npm run dev` builds both packages once, then watches them alongside the template app's own dev server — this is what makes package-source hot reload work (see [docs/REALTIME_SESSION.md](docs/REALTIME_SESSION.md)'s "Dev-mode reload behavior"). Open [`/demo/realtime`](http://localhost:5173/demo/realtime) or [`/demo/humanoid`](http://localhost:5173/demo/humanoid) (works offline via `REALTIME_PROVIDER=mock`, no API key needed). ## Scripts -- `npm run dev` / `npm run build` / `npm run preview` — standard SvelteKit commands. -- `npm run check` — type-check. `npm run lint` / `npm run format` — prettier + eslint. -- `npm run test:unit` — vitest. `npm run test:e2e` — Playwright. -- `npm start` — run the production build via `server.js` (the persistent Node process the realtime WebSocket relay requires — see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)). +Run from the repo root, these orchestrate all three workspaces: + +- `npm run dev` / `npm run build` / `npm run check` / `npm test` — build the packages first, then run the equivalent command across every workspace. +- `npm run lint` / `npm run format` — prettier + eslint across the whole repo. + +Run a single workspace's own scripts with `-w`, e.g. `npm run dev -w apps/template`, or `npm run test -w packages/core`. ## Documentation -| Subsystem | Doc | -| -------------------------------------------------------- | ---------------------------------------------------- | -| Architecture overview and module layering | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | -| Realtime session management | [docs/REALTIME_SESSION.md](docs/REALTIME_SESSION.md) | -| Tool calling (server- and client-executed) | [docs/TOOLS.md](docs/TOOLS.md) | -| Analytics recording | [docs/ANALYTICS.md](docs/ANALYTICS.md) | -| Configuration (env vars, persona) | [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | -| `humanoid` package (avatar-state, animation composition) | [docs/HUMANOID.md](docs/HUMANOID.md) | +| Subsystem | Doc | +| --------------------------------------------------------------------- | -------------------------------------------------------------------- | +| Architecture overview and module layering | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | +| Realtime session management | [docs/REALTIME_SESSION.md](docs/REALTIME_SESSION.md) | +| Tool calling (server- and client-executed) | [docs/TOOLS.md](docs/TOOLS.md) | +| Analytics recording | [docs/ANALYTICS.md](docs/ANALYTICS.md) | +| Configuration (env vars, persona) | [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | +| `@webavatar/webavatar-humanoid` (avatar-state, animation composition) | [docs/HUMANOID.md](docs/HUMANOID.md) | +| The monorepo migration itself — rationale, decisions, verification | [docs/MONOREPO_MIGRATION_BRIEF.md](docs/MONOREPO_MIGRATION_BRIEF.md) | diff --git a/.env.example b/apps/template/.env.example similarity index 100% rename from .env.example rename to apps/template/.env.example diff --git a/apps/template/README.md b/apps/template/README.md new file mode 100644 index 0000000..650d9a7 --- /dev/null +++ b/apps/template/README.md @@ -0,0 +1,49 @@ +# webavatar-template + +The SvelteKit application template webavatar client projects branch from. It composes `@webavatar/webavatar-core` and `@webavatar/webavatar-humanoid` with project-owned persona, tools, and deployment configuration — see the monorepo root's [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md) for the full architecture map, and [AGENTS.md](../../AGENTS.md) for project conventions. `docs/` holds a deep-dive per subsystem. + +## Getting started (inside this monorepo) + +```sh +npm install # from the monorepo root +cp apps/template/.env.example apps/template/.env +# fill in OPENAI_API_KEY and REALTIME_TOKEN_SECRET in apps/template/.env +npm run dev # from the monorepo root — builds the packages, then watches them alongside this app +``` + +Open [`/demo/realtime`](http://localhost:5173/demo/realtime) for the realtime session reference wiring, or [`/demo/humanoid`](http://localhost:5173/demo/humanoid) for the humanoid avatar-state demo (works offline via `REALTIME_PROVIDER=mock`, no API key needed). + +Running `npm run dev` from _this_ directory alone starts the app but does **not** hot-reload package source changes — see [docs/REALTIME_SESSION.md](../../docs/REALTIME_SESSION.md)'s "Dev-mode reload behavior" for why, and use the root `npm run dev` instead when iterating on `packages/core` or `packages/humanoid`. + +## Scripts + +- `npm run dev` / `npm run build` / `npm run preview` — standard SvelteKit commands. +- `npm run check` — type-check. `npm run lint` / `npm run format` — prettier + eslint. +- `npm run test:unit` — vitest. `npm run test:e2e` — Playwright. +- `npm start` — run the production build via `server.js` (the persistent Node process the realtime WebSocket relay requires — see [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md)). + +## Documentation + +| Subsystem | Doc | +| --------------------------------------------------------------------- | ---------------------------------------------------------- | +| Architecture overview and module layering | [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md) | +| Realtime session management | [docs/REALTIME_SESSION.md](../../docs/REALTIME_SESSION.md) | +| Tool calling (server- and client-executed) | [docs/TOOLS.md](../../docs/TOOLS.md) | +| Analytics recording | [docs/ANALYTICS.md](../../docs/ANALYTICS.md) | +| Configuration (env vars, persona) | [docs/CONFIGURATION.md](../../docs/CONFIGURATION.md) | +| `@webavatar/webavatar-humanoid` (avatar-state, animation composition) | [docs/HUMANOID.md](../../docs/HUMANOID.md) | + +## Starting a client project from this template + +The packages (`@webavatar/webavatar-core`, `@webavatar/webavatar-humanoid`) are what a client project upgrades; this template is what it starts from. A project installs versions of the packages — it does not merge package source from this repository. + +1. Copy this directory (`apps/template`) into a new repository. Do not copy the monorepo root, `packages/`, or the root lockfile. +2. The copied `package.json` becomes the project's own root manifest. It stays `"private": true`. Its `@webavatar/*` ranges no longer resolve to workspace links, so pin them to published versions. +3. Add the committed `.npmrc` scope mapping (`@webavatar:registry=...`), configure registry authentication, and run `npm install`. +4. Copy `.env.example` to `.env`, fill in the secrets, and run `npm run dev`. The demo routes stay dev-only gated and can be kept or deleted. +5. Project code goes under `src/lib/**/project`, exactly as in this template. +6. To upgrade, bump the package versions; a peer-range conflict between core and humanoid surfaces as an `npm install` error — that's the intended signal, not a bug. + +Changes to this template shell itself (`server.js`, `vite.config.ts`, routes, deployment files) don't arrive through package upgrades; a project applies them by hand. The composition-root factory (`createRealtimeRuntime`, from `@webavatar/webavatar-core/server`) is what keeps the shell thin enough that this stays rare — a change in how the relay is assembled ships inside a core release instead. + +**Current status:** steps 3 and 6 (installing from the registry, and the peer-range upgrade signal) require the private Gitea package registry, which isn't provisioned yet — see `docs/MONOREPO_MIGRATION_BRIEF.md`'s "Decisions required before handoff". Steps 1, 2, 4, and 5 work today; the packages themselves have been verified installable from packed tarballs in place of the registry (see the migration brief's verification requirements). diff --git a/apps/template/package.json b/apps/template/package.json new file mode 100644 index 0000000..bb21898 --- /dev/null +++ b/apps/template/package.json @@ -0,0 +1,49 @@ +{ + "name": "webavatar-template", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build && tsc -p tsconfig.server.json", + "preview": "vite preview", + "start": "node server.js", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --check . && eslint .", + "format": "prettier --write .", + "test:unit": "vitest", + "test": "npm run test:unit -- --run && npm run test:e2e", + "test:e2e": "playwright install && playwright test" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.60.0", + "@sveltejs/adapter-node": "^5.5.7", + "@sveltejs/kit": "^2.63.0", + "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@types/node": "^24", + "@types/three": "^0.185.4", + "@vitest/browser-playwright": "^4.1.8", + "eslint": "^10.4.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.19.0", + "globals": "^17.6.0", + "playwright": "^1.60.0", + "prettier": "^3.8.3", + "prettier-plugin-svelte": "^4.1.0", + "svelte": "^5.56.1", + "svelte-check": "^4.6.0", + "three": "^0.185.1", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16", + "vitest": "^4.1.8", + "vitest-browser-svelte": "^2.1.1" + }, + "dependencies": { + "@webavatar/webavatar-core": "^0.1.0", + "@webavatar/webavatar-humanoid": "^0.1.0" + } +} diff --git a/playwright.config.ts b/apps/template/playwright.config.ts similarity index 100% rename from playwright.config.ts rename to apps/template/playwright.config.ts diff --git a/apps/template/server-only-guard.ts b/apps/template/server-only-guard.ts new file mode 100644 index 0000000..9038a42 --- /dev/null +++ b/apps/template/server-only-guard.ts @@ -0,0 +1,27 @@ +import type { Plugin } from 'vite'; + +const SERVER_ONLY = /^@webavatar\/webavatar-core\/(server|vite)(\/|$)/; + +/** + * Build-time backstop for the server-only boundary: SvelteKit's own + * `$lib/server` guard is path-based and doesn't reach into `node_modules`, so + * once core's server code is a package this is what makes a client-side + * import of `@webavatar/webavatar-core/server` (or `/vite`) fail the build + * rather than only failing at runtime via the package's `browser` stub. See + * docs/ARCHITECTURE.md's "Server-only safety" section. + */ +export function serverOnlyGuard(): Plugin { + return { + name: 'webavatar-server-only-guard', + enforce: 'pre', + resolveId(id, importer, options) { + const isClient = this.environment ? this.environment.name === 'client' : !options?.ssr; + if (isClient && SERVER_ONLY.test(id)) { + throw new Error( + `${id} is server-only; import it through $lib/server instead (importer: ${importer}).` + ); + } + return null; + } + }; +} diff --git a/apps/template/server.js b/apps/template/server.js new file mode 100644 index 0000000..46dcaf6 --- /dev/null +++ b/apps/template/server.js @@ -0,0 +1,23 @@ +// Custom production server: wraps adapter-node's generated request handler in +// a plain http.Server so we can also intercept WebSocket upgrade requests, +// which SvelteKit itself has no hook for (see @webavatar/webavatar-core's +// server/ws-handler.ts). +// +// `.env` is not auto-loaded here (SvelteKit's own adapter-node doesn't in +// production either) — run with `node --env-file=.env server.js` locally, or +// provide OPENAI_API_KEY / REALTIME_TOKEN_SECRET via your deployment's real +// secret mechanism. +import { createServer } from 'node:http'; +import { createRealtimeRuntime } from '@webavatar/webavatar-core/server'; +import { handler } from './build/handler.js'; +import { SESSION_PERSONA } from './build-server/project/persona.js'; +import { toolRegistry } from './build-server/server/project/tools/index.js'; + +const runtime = createRealtimeRuntime({ persona: SESSION_PERSONA, toolRegistry, isDev: false }); +const server = createServer(handler); + +server.on('upgrade', runtime.handleUpgrade); + +server.listen(runtime.config.port, () => { + console.log(`listening on port ${runtime.config.port}`); +}); diff --git a/src/app.d.ts b/apps/template/src/app.d.ts similarity index 100% rename from src/app.d.ts rename to apps/template/src/app.d.ts diff --git a/src/app.html b/apps/template/src/app.html similarity index 100% rename from src/app.html rename to apps/template/src/app.html diff --git a/src/lib/assets/favicon.svg b/apps/template/src/lib/assets/favicon.svg similarity index 100% rename from src/lib/assets/favicon.svg rename to apps/template/src/lib/assets/favicon.svg diff --git a/src/lib/client/core/DebugPanel.svelte b/apps/template/src/lib/client/DebugPanel.svelte similarity index 93% rename from src/lib/client/core/DebugPanel.svelte rename to apps/template/src/lib/client/DebugPanel.svelte index cff6b78..d1b6370 100644 --- a/src/lib/client/core/DebugPanel.svelte +++ b/apps/template/src/lib/client/DebugPanel.svelte @@ -1,12 +1,17 @@