@tomorrowos/sdk 0.9.29 → 0.9.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -78,7 +78,7 @@ IF any step contradicts Vercel, adapt to settings that Vercel can accept.
78
78
 
79
79
  That wizard asks: database (**Supabase** → **Neon** → SQLite demo), media (**Cloudinary** → **Vercel Blob** → local),
80
80
  branding (manual fields or **website URL → `brand.json` only**, no login unless asked). Preview may use a thin **Next.js reverse proxy**;
81
- **Publish stays pure Node** `TomorrowOS.listen` (Scheme A: root `server.ts`, Next in `preview/`, `GET /status` must be JSON). Cloudinary uses **one Env popup** (all fields).
81
+ **Publish stays a Fluid Vercel Function** (`api/index.ts` + `export default server`, WebSockets per Vercel docs). Cloudinary uses **one Env popup** (all fields).
82
82
  Neon/Supabase: agent auto-sets `TOMORROWOS_STORE` + `DATABASE_SSL`. Sets `cms.hostingTarget` to `"vercel"`.
83
83
 
84
84
  ### Replit upgrade (existing CMS)
package/VERCEL_SETUP.md CHANGED
@@ -5,7 +5,7 @@
5
5
  >
6
6
  > **On Vercel, this file is the setup questionnaire** (not `REPLIT_SETUP.md`, not `LLM_PROMPT.md`).
7
7
  >
8
- > Protocol id: `vercel-setup/1.7` — pairs with `@tomorrowos/sdk` packages that ship `templates/cms-starter` and this file.
8
+ > Protocol id: `vercel-setup/1.8` — pairs with `@tomorrowos/sdk` **0.9.30+** (`templates/cms-starter-v0`, Fluid Functions WebSockets).
9
9
 
10
10
  ## User trigger (copy-paste)
11
11
 
@@ -19,8 +19,9 @@ If any step contradicts Vercel, adapt to settings Vercel can accept (document th
19
19
  After all setup questions are answered (including Cloudinary or Vercel Blob Env Vars if you chose those), configure and deploy — minimal verification only (Control Panel at /). Prefer Supabase Session pooler URL or Neon pooled URL. Do not treat sandbox ENETUNREACH / IPv6 :5432 errors as a hard setup failure when a pooler URL is configured for production.
20
20
  Question 3 only updates brand.json from colours/logo/name (a website URL is brand reference only — do not add CMS login/auth unless the user explicitly asks). Do not skip steps.
21
21
  Use Next.js only as a v0/Preview shell with a reverse proxy; Production Publish must remain pure Node TomorrowOS.listen (not Next).
22
- Before claiming Publish works: Framework Preset must be Other/null (not Next.js), package.json `start` must run TomorrowOS Node, GET /status on the production URL must return JSON (not HTML), and GET / must return the Control Panel (not 404 NOT_FOUND).
23
- Do not put startCommand or processes in vercel.json (v0 schema rejects them). Do not move server.ts into api/.
22
+ Before claiming Publish works: Framework Preset must be Other/null (not Next.js), Fluid on, GET /status on the production URL must return JSON (not HTML), GET / must return the Control Panel, and WebSocket upgrade on / or /api must return 101 (not 200 HTML).
23
+ Do not put startCommand or processes in vercel.json (v0 schema rejects them).
24
+ Use cms-starter-v0 layout: cms-app.ts + api/index.ts (export default server) + cms-panel/ + rewrites to /api. Do not create a second TomorrowOS instance in api/ws.ts.
24
25
  ```
25
26
 
26
27
  Alternate triggers:
@@ -74,14 +75,14 @@ almost always means **Vercel still thinks this is a Next.js / static deploy**, n
74
75
  2. **Install Command** = `npm install` / `pnpm install` / `yarn` (normal install).
75
76
  3. **Build Command** = a **no-op that exits 0** (e.g. `node -e "process.exit(0)"` or `npm run build` where `build` is that no-op).
76
77
  **Never** run `next build` for Production.
77
- 4. **Start / Dev Command for Production** = `npm run start` / `pnpm start` **`tsx server.ts`** (or `.mts`) that calls `TomorrowOS.listen` and **`export default server`**.
78
+ 4. **Production entry** = Fluid **Vercel Function** `api/index.ts` that **`export default server`** from shared `cms-app.ts` (see [WebSockets docs](https://vercel.com/docs/functions/websockets)). Local `npm start` may still use root `server.ts`.
78
79
  5. **Output Directory** = **empty / unset**. Do **not** set `outputDirectory: "."` as a fake static site.
79
- 6. **Do not** create a root `api/index.ts` that exports a raw `http.Server` and catch-all `routes` to “fix” 404 Vercel Fluid does **not** treat that as a supported handler for this CMS. That pattern causes more 404s.
80
+ 6. **Do** use a single `api/index.ts` that exports the TomorrowOS `http.Server`. **Do not** create a second TomorrowOS in `api/ws.ts`, and **do not** use bare `(req, res)` handlers that drop upgrades.
80
81
  7. If Preview needs Next, keep Next files **isolated** so Production auto-detect cannot latch onto them:
81
82
  - Prefer `preview/` (or similar) for `next.config.*`, `app/`, `pages/` used only by v0 Preview, **or**
82
- - Ensure Production Framework Preset is forced to **Other** and Start Command never runs `next`.
83
+ - Ensure Production Framework Preset is forced to **Other** and never runs `next` as the CMS.
83
84
 
84
- **Setup is NOT complete** while Production `GET /` returns `404: NOT_FOUND`. Fix Framework Preset + Start Command and redeploy.
85
+ **Setup is NOT complete** while Production `GET /` returns `404: NOT_FOUND` or WebSocket upgrade returns 200 HTML.
85
86
 
86
87
  ### Publish static trap — `public/` served without Node (read this)
87
88
 
@@ -92,11 +93,11 @@ A common **worse** failure mode than `404: NOT_FOUND`:
92
93
  - The panel shows **CMS unreachable** and console:
93
94
  `Unexpected token 'T', "The page c"... is not valid JSON`
94
95
 
95
- **Root cause:** Vercel deployed `public/` as a **static site** (Output Directory = `public` or Framework=Other default *“public if it exists”*). **No Node process** is running `tsx server.ts`, so API routes never hit `TomorrowOS`.
96
+ **Root cause:** Vercel deployed `public/` as a **static site** (Output Directory = `public` or Framework=Other default *“public if it exists”*). **No Function** is running TomorrowOS.
96
97
 
97
- **Fix (Scheme A — see dedicated section below):** force Vercel to run the **root `server.ts` Node entry** with Fluid. `public/` must be served **at runtime** via `staticRoot` in `listen()`, **not** as the build output directory.
98
+ **Fix (Scheme A):** use `cms-panel/` (not `public/`), Fluid Function `api/index.ts` + rewrites, Output Directory empty. Static UI is served **at runtime** via `staticRoot` in `listen()`.
98
99
 
99
- **Publish gate (mandatory):** after deploy, `GET /status` must return **JSON** starting with `{`, not HTML starting with `<` or `The page`.
100
+ **Publish gate (mandatory):** after deploy, `GET /status` must return **JSON** starting with `{`, not HTML starting with `<` or `The page`. **And** WebSocket upgrade on `/` or `/api` must return **101**.
100
101
 
101
102
  ```bash
102
103
  curl -s https://YOUR-PROJECT.vercel.app/status | head -c 80
@@ -104,7 +105,7 @@ curl -s https://YOUR-PROJECT.vercel.app/status | head -c 80
104
105
  # Failure: The page could not be found OR <!DOCTYPE html>
105
106
  ```
106
107
 
107
- **Setup is NOT complete** while `/status` returns HTML or the panel shows the JSON parse error.
108
+ **Setup is NOT complete** while `/status` returns HTML, the panel shows the JSON parse error, or devices cannot open `wss://`.
108
109
 
109
110
  ### Preview (v0 / Vercel Agent sandbox) — Next.js shell allowed
110
111
 
@@ -163,70 +164,74 @@ Then ask **Question 1**.
163
164
  3. **Do not invent** Cloudinary credentials, database URLs, Vercel Blob tokens, brand colours, **or LLM API keys**.
164
165
  4. **Postgres env naming:** prefer **`SUPABASE_URL`** when using Supabase; prefer **`DATABASE_URL`** when using Neon. `createTomorrowOSStore` accepts `SUPABASE_URL` then `DATABASE_URL`.
165
166
  5. **Always prefer pooled connection strings** (Supabase Session pooler port **6543**; Neon pooled host). Direct `:5432` URLs commonly fail on Vercel/sandbox with **`ENETUNREACH`** (often IPv6).
166
- 6. **Prefer `npx @tomorrowos/sdk init --hosting v0`** (template `cms-starter-v0`: root `server.ts`, `cms-panel/`, `vercel.json`). For Replit use default `init` (`cms-starter`). Do not rebuild pairing / WebSocket / playlist APIs.
167
+ 6. **Prefer `npx @tomorrowos/sdk init --hosting v0`** (template `cms-starter-v0`: `cms-app.ts`, `api/index.ts`, `cms-panel/`, Fluid + rewrites). For Replit/Railway use default `init` (`cms-starter`). Do not rebuild pairing / WebSocket / playlist APIs.
167
168
  7. **Never commit secrets.** Use Vercel Environment Variables (+ optional non-secret flags in committed `.env.example`).
168
169
  8. **Skip Replit-only files:** do **not** create `.replit`, `.replit-artifact`, or Replit `deploymentTarget`.
169
170
  9. **SQLite is not a production store on Vercel.** Offer it only as option **3** after explicit warning. Keep `sqlitePath` as local-dev fallback in `createTomorrowOSStore`.
170
- 10. **Enable Fluid compute** for Production WebSockets. Confirm Project settings after scaffold.
171
- 11. **Production:** export the HTTP server Vercel can capture: `const server = tomorrowos.listen(...); export default server;` (or equivalent). **Do not** make `next start` the Production entrypoint.
171
+ 10. **Enable Fluid compute** for Production WebSockets ([docs](https://vercel.com/docs/functions/websockets)). Confirm Project settings after scaffold.
172
+ 11. **Production:** one Vercel Function exports `http.Server` from `TomorrowOS.listen` (`api/index.ts`). **Do not** make `next start` the Production entrypoint. **Do not** spin a second TomorrowOS in `api/ws.ts`.
172
173
  12. **Preview:** if the environment is v0 / Next-hardwired, install the Next reverse-proxy shell (see **Preview adapter**). Do not leave Preview broken.
173
- 13. After Q&A: **configure → install → run/deploy → minimal verify** (Control Panel at `/`). Do not run a long test suite.
174
+ 13. After Q&A: **configure → install → run/deploy → minimal verify** (Control Panel at `/` + WS 101). Do not run a long test suite.
174
175
  14. **No inventing CMS login.** A branding URL never means “build auth.” Only add login if the user **explicitly** requests it.
175
176
  15. **Skip ≠ invent.** If the user skips media choice or a credential, do not replace it with OpenAI/auth/login questions. Stay on Q1–Q3 only.
176
177
  16. **Neon / Supabase helpers are agent-owned.** After the user pastes the DB URL, **you** set `TOMORROWOS_STORE` and `DATABASE_SSL` automatically — **do not** ask the user to fill those two.
177
- 17. **Publish gate:** do not mark setup complete while Production returns `404: NOT_FOUND` **or** `/status` returns HTML (static `public/` trap).
178
- 18. **Scheme A is doc-driven:** achieve Production Node Publish by **project layout + `vercel.json` + dashboard settings** during setup **no SDK code changes** required (see **Scheme A** section).
178
+ 17. **Publish gate:** do not mark setup complete while Production returns `404: NOT_FOUND`, `/status` returns HTML, **or** WebSocket upgrade returns 200 HTML instead of 101.
179
+ 18. **Scheme A is doc-driven:** Production = Fluid Function + `export default server` per Vercel WebSockets docsReplit starter stays unchanged.
179
180
 
180
181
  **Question order:** Q1 → Q2 → Q3 → execution checklist.
181
182
 
182
183
  ---
183
184
 
184
- ## Scheme A — Standard Node Publish (mandatory for Production)
185
+ ## Scheme A — Vercel Functions + WebSockets (mandatory for Production)
185
186
 
186
- > **Goal:** Preview may use Next + proxy; **Publish** runs **only** root `server.ts` + `TomorrowOS.listen` on Vercel Fluid.
187
- > **Scope:** This section is implemented entirely during setup (move files, write `vercel.json`, patch dashboard). **Do not** change `@tomorrowos/sdk` source.
187
+ > **Goal:** Preview may use Next + proxy; **Publish** runs TomorrowOS as a **Vercel Function** (`api/index.ts`) on **Fluid**, with WebSocket support per [Vercel WebSockets docs](https://vercel.com/docs/functions/websockets).
188
+ > **Scope:** Layout + `vercel.json` + dashboard. Prefer `npx @tomorrowos/sdk init --hosting v0` (`cms-starter-v0`).
189
+ > **Do not** break Replit/Railway: those keep using default `init` → `cms-starter` (root `server.ts` + `server.listen`, no `api/`).
188
190
 
189
191
  ### Why Scheme A exists
190
192
 
191
193
  | Layer | Preview (v0) | Publish (Production) |
192
194
  |-------|----------------|----------------------|
193
- | Host | Next.js shell + HTTP proxy | **Node `server.ts`** only |
194
- | `public/` | Proxied from TomorrowOS | Served by **`staticRoot`** inside `listen()` — **not** as Vercel Output Directory |
195
- | WebSocket | Optional / may fail in Preview | Required on same origin (`wss://`) |
195
+ | Host | Next.js shell + HTTP proxy | **Vercel Function** `api/index.ts` `export default server` |
196
+ | Static UI | Proxied from TomorrowOS | Served by **`staticRoot`** (`cms-panel/`) inside `listen()` — **not** Vercel Output Directory |
197
+ | WebSocket | Optional / may fail in Preview | **Required** Fluid Function upgrade (`wss://` → `/` rewritten to `/api`, or direct `/api`) |
196
198
  | Next files | Allowed under `preview/` | **Must not** live at project root |
197
199
 
200
+ **Root cause of “Control Panel works, TV cannot connect”:** HTTP can be served without WebSocket upgrades. Classic root-only `server.ts` / plain serverless handlers often return **200 HTML** on `Upgrade` instead of **101**. Official pattern is a Function that exports `http.Server` with `ws` attached ([docs](https://vercel.com/docs/functions/websockets)).
201
+
198
202
  ### A1 — Project layout (Agent must enforce)
199
203
 
200
- **At project root (Production):**
204
+ **Production (Vercel / v0 starter):**
201
205
 
202
206
  ```
203
- server.ts REQUIRED name at root (or server.mts)
204
- vercel.json
207
+ cms-app.ts shared TomorrowOS.listen + export const server
208
+ api/index.ts ← export { server as default } from "../cms-app.js"
209
+ server.ts ← local/Preview: same default export (npm start / internal port)
210
+ vercel.json ← fluid + rewrites + functions.maxDuration
205
211
  package.json
206
212
  brand.json
207
- public/ ← CMS static UI; NOT the Vercel output directory
213
+ cms-panel/ ← CMS static UI (NOT named public/ — avoids static-output trap)
214
+ preview/ ← Next proxy for v0 Preview only
208
215
  ```
209
216
 
210
- **Preview only (when v0 needs Next) move ALL of these under `preview/`:**
217
+ **Preview only — keep ALL Next under `preview/`:**
211
218
 
212
219
  ```
213
220
  preview/
214
221
  next.config.mjs
215
- app/ ← Next proxy routes
216
- postcss.config.mjs
217
- next-env.d.ts
218
- tsconfig.json ← Next/tsconfig if separate from server
222
+ app/
223
+
219
224
  ```
220
225
 
221
- **Remove from project root after move:** `next.config.mjs`, `app/`, root Next `tsconfig` plugin config, `postcss.config.mjs`, `next-env.d.ts`.
222
-
223
226
  **Do NOT:**
224
227
 
225
- - ❌ Leave `next.config.mjs` + `app/` at root while claiming Production is Node-only
226
- - ❌ Create root `api/index.ts` exporting `http.Server` + `routes` catch-all
227
- - ❌ Set Output Directory to `public` or `.`
228
+ - ❌ Leave `next.config.mjs` + `app/` at project root
229
+ - ❌ Set Output Directory to `public`, `cms-panel`, or `.`
230
+ - ❌ Create a **second** TomorrowOS instance in `api/ws.ts` (separate isolate = broken pairing memory)
231
+ - ❌ Use `(req, res) =>` serverless handlers that drop `upgrade`
232
+ - ❌ Put `startCommand` / `processes` in `vercel.json` (v0 schema rejects them)
228
233
 
229
- **Update `package.json` scripts when Preview uses `preview/`:**
234
+ **`package.json` scripts:**
230
235
 
231
236
  ```json
232
237
  {
@@ -241,105 +246,117 @@ preview/
241
246
  }
242
247
  ```
243
248
 
244
- ### A2 `server.ts` entry (Vercel zero-config Node detection)
249
+ - **Local / Railway-style:** `npm start` → `tsx server.ts` SDK calls `server.listen` (no `VERCEL` env).
250
+ - **Vercel Production:** Function loads `api/index.ts`; SDK skips `listen` when `process.env.VERCEL` is set and Vercel owns the socket.
245
251
 
246
- Vercel detects a **root** `server.ts` / `server.mts` and routes traffic to it when:
252
+ ### A2 Function entry (matches Vercel WebSockets docs)
247
253
 
248
- 1. The file lives at **project root** (not only under `api/`)
249
- 2. **`listen()` runs at module load** (top-level, not inside an unexported function)
250
- 3. The file **`export default server`** (the `http.Server` from `tomorrowos.listen`)
251
-
252
- Use the pattern in **Runtime & Vercel deploy rules** below. On Publish, **do not** set `TOMORROWOS_INTERNAL_PORT` — let Vercel inject `PORT`.
254
+ ```ts
255
+ // api/index.ts
256
+ export { server as default } from "../cms-app.js";
257
+ ```
253
258
 
254
- ### A3 — `vercel.json` (Production)
259
+ ```ts
260
+ // cms-app.ts (shared)
261
+ const server = tomorrowos.listen({
262
+ port: Number(process.env.PORT) || 3000,
263
+ host: "0.0.0.0",
264
+ staticRoot: join(__dirname, "cms-panel"),
265
+ });
266
+ export { server, tomorrowos };
267
+ ```
255
268
 
256
- **v0 / `.v0/vercel.deploy.json` schema:** only a **minimal** object is valid. These keys are **rejected** — never add them:
269
+ SDK behaviour (0.9.30+):
257
270
 
258
- - `startCommand`
259
- - `processes`
260
- - `routes` / `rewrites` to fake API routing
261
- - ❌ `outputDirectory: "public"` or `"."`
271
+ - Accepts WebSocket upgrades on `/`, `/api`, `/api/ws` (players may use either)
272
+ - When `VERCEL` is set: **`autoListen` defaults to false** — do not bind a port yourself
273
+ - Pattern aligns with docs: create `http.Server`, attach `ws`, **`export default server`**
262
274
 
263
- Vercel discovers the start command from **`package.json` → `"start": "tsx server.ts"`**, not from `vercel.json`.
275
+ ### A3 `vercel.json` (Production)
264
276
 
265
277
  ```json
266
278
  {
267
279
  "fluid": true,
268
280
  "framework": null,
269
281
  "installCommand": "npm install",
270
- "buildCommand": "npm run build"
282
+ "buildCommand": "npm run build",
283
+ "functions": {
284
+ "api/index.ts": {
285
+ "maxDuration": 800,
286
+ "memory": 1024,
287
+ "includeFiles": "{cms-panel/**,brand.json,assets/**}"
288
+ }
289
+ },
290
+ "rewrites": [
291
+ { "source": "/((?!api/).*)", "destination": "/api" }
292
+ ]
271
293
  }
272
294
  ```
273
295
 
274
- Use `pnpm` / `yarn` equivalents if the project uses them.
296
+ **Why rewrites:** Public URLs stay `https://app.vercel.app/` and `/status` while the Function mount is `/api`. Device `wss://app.vercel.app/` is rewritten to the same Function so one isolate handles HTTP + WS.
275
297
 
276
- **Critical:**
298
+ **v0 schema:** never add `startCommand` or `processes`.
277
299
 
278
- - **Do not** add `startCommand` to `vercel.json` (invalid in v0; redundant on full Vercel — use `package.json` `start`)
279
- - **Do not** set `outputDirectory` to `public` or `.`
280
- - `"buildCommand"` must run the no-op `npm run build` — **never** `next build` for Production
281
- - Set **Output Directory empty** in the **Vercel dashboard** (Scheme A4) — that is where static-vs-Node is decided, not via invalid `vercel.json` keys
300
+ **Critical:**
282
301
 
283
- ### A4 Vercel Project Settings (dashboard or API — mandatory)
302
+ - `"fluid": true` is required for WebSockets
303
+ - **Do not** set `outputDirectory` to `cms-panel` / `public`
304
+ - `"buildCommand"` = no-op — **never** `next build` for Production
305
+ - Dashboard Output Directory must stay **empty**
284
306
 
285
- `vercel.json` alone is **not enough** if the dashboard still shows Framework = **Next.js** or Output = `public`.
307
+ ### A4 Vercel Project Settings (dashboard mandatory)
286
308
 
287
309
  | Setting | Required value |
288
310
  |---------|----------------|
289
311
  | Framework Preset | **Other** / null — **not Next.js** |
290
- | Install Command | `npm install` (or pnpm/yarn) |
291
- | Build Command | `npm run build` (no-op exit 0) |
292
- | **Output Directory** | **empty / null** — **NOT `public`** |
293
- | **Start Command** | `npm run start` → `tsx server.ts` |
312
+ | Install Command | `npm install` |
313
+ | Build Command | `npm run build` (no-op) |
314
+ | **Output Directory** | **empty** |
294
315
  | Fluid compute | **On** |
316
+ | WebSockets | Account must allow Functions WebSockets (see docs “Permissions Required”) |
295
317
 
296
- If Framework Preset still shows **Next.js** after `vercel.json`: **patch via Vercel API / CLI / dashboard**, then redeploy.
297
-
298
- ### A5 — Publish acceptance (all must pass before “setup complete”)
299
-
300
- 1. Deploy logs show Node starting / `[TomorrowOS] listening on ...` (not only “Uploading static files”)
301
- 2. `curl -s https://PROD/status` → JSON (`{"status":...}`)
302
- 3. `curl -s https://PROD/` → Control Panel HTML
303
- 4. Control Panel **does not** show `Unexpected token 'T'` / CMS unreachable
304
- 5. Framework Preset = Other; Fluid on
318
+ ### A5 Publish acceptance (all must pass)
305
319
 
306
- If (2) fails but (3) passes → **static `public/` trap**. Re-apply A3 + A4; confirm dashboard Start Command + empty Output Directory; redeploy. **Do not** add `api/` catch-all. **Do not** move `server.ts` into `api/`.
320
+ 1. `curl -s https://PROD/status` JSON (`{"status":...}`)
321
+ 2. `curl -s https://PROD/` → Control Panel HTML
322
+ 3. **WebSocket upgrade returns 101**, not 200 HTML:
307
323
 
308
- ### A6 — What Scheme A does **not** require
309
-
310
- - No changes to `@tomorrowos/sdk` package code
311
- - No `(req, res)` serverless handler refactor
312
- - No `next build` / `next start` on Production
313
- - ❌ No root `api/` directory (`api/index.ts`, `api/[...].ts`, `api/[[...slug]].ts`)
314
- - No `rewrites` so only `/api/status` works while `/status` stays 404
315
- - ❌ No renaming `start` → `dev` with `tsx watch` for Production
316
- - ❌ No `@vercel/node` handler that calls non-existent `tomorrowos.getStatus()`
324
+ ```bash
325
+ curl.exe -i --http1.1 ^
326
+ -H "Connection: Upgrade" -H "Upgrade: websocket" ^
327
+ -H "Sec-WebSocket-Version: 13" ^
328
+ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" ^
329
+ "https://PROD/"
330
+ # Also try: https://PROD/api
331
+ ```
317
332
 
318
- ### A7 If v0 Publish still serves static `public/` (Path B)
333
+ 4. Control Panel does **not** show CMS unreachable / `Unexpected token 'T'`
334
+ 5. Fluid on; Framework = Other
319
335
 
320
- When **v0 Publish** keeps returning the static trap (`/` HTML OK, `/status` HTML, `Unexpected token 'T'`) **after** Scheme A is correct, the v0 deploy pipeline may not be starting the Fluid Node server.
336
+ If (1)+(2) pass but (3) fails Function is serving HTTP only. Re-check Fluid, `api/index.ts` default export, rewrites, and WebSockets permission. **Do not** invent a second `api/ws.ts` TomorrowOS instance.
321
337
 
322
- **Path Bdeploy outside v0 Publish (recommended):**
338
+ ### A6What Scheme A does **not** require
323
339
 
324
- 1. Keep Scheme A layout (root `server.ts`, Next only under `preview/`, minimal `vercel.json`).
325
- 2. Connect the repo to **Vercel Git** or run **`vercel --prod`** from the project root (full Vercel CLI / dashboard deploy — not the v0 “Publish” button).
326
- 3. In **Vercel Dashboard Project Settings → General**: Framework = **Other**, Output Directory = **empty**, Build = `npm run build`, **override** Install/Start if needed so Production runs `npm run start`.
327
- 4. Redeploy; verify `curl https://PROD/status` JSON.
340
+ - No Next.js as Production CMS
341
+ - No replacing SDK transport with hand-rolled pairing
342
+ - No SQLite as production store on Vercel
343
+ - No changes to Replit `cms-starter` layout for Vercel-only fixes
328
344
 
329
- **Path Cif Vercel cannot run TomorrowOS Node + WebSocket reliably:**
345
+ ### A7If v0 Publish still fails (Path B / C)
330
346
 
331
- Host the CMS on **Railway**, **Fly.io**, or **Replit Publish** (long-lived Node). Use v0/Vercel only for Preview/demo if needed.
347
+ **Path B:** Deploy with `vercel --prod` or Git integration (full Vercel), same Scheme A layout.
332
348
 
333
- ### A8 Undo a broken Agent “api/ migration”
349
+ **Path C:** Host CMS on **Railway / Fly.io / Replit** (long-lived Node). Use Vercel only for Preview if needed.
334
350
 
335
- If an Agent moved `server.ts` `api/index.ts` / `api/[...].ts` and only `/api/status` returns JSON:
351
+ ### A8 Migrate an existing broken Vercel project
336
352
 
337
- 1. **Restore** `server.ts` at **project root** (from git or `templates/cms-starter/server.ts`).
338
- 2. **Delete** root `api/index.ts`, `api/[...].ts`, `api/[[...slug]].ts` (Preview proxy may stay under `preview/app/` only).
339
- 3. Revert `package.json` `start` to `tsx server.ts` (not `tsx watch` for Production).
340
- 4. Apply minimal `vercel.json` (A3) no `startCommand`, no `rewrites`.
341
- 5. Dashboard: Framework Other, Output empty, Fluid on.
342
- 6. Redeploy via Path B if v0 Publish still fails.
353
+ 1. Scaffold/compare with `cms-starter-v0` (`init --hosting v0`) or copy:
354
+ - `cms-app.ts`, `api/index.ts`, `server.ts`, `vercel.json`
355
+ - rename `public/` `cms-panel/` if still present
356
+ 2. Ensure **one** Function exports the **same** `server` from `cms-app.ts`
357
+ 3. Fluid on Redeploy
358
+ 4. Verify A5 (especially WebSocket 101)
359
+ 5. On TVs: enter `https://YOUR.vercel.app/` (players also try `/api` automatically on vercel.app hosts)
343
360
 
344
361
  ---
345
362
 
@@ -347,11 +364,12 @@ If an Agent moved `server.ts` → `api/index.ts` / `api/[...].ts` and only `/api
347
364
 
348
365
  ### Production runtime (Publish)
349
366
 
350
- - Serve with **`tsx server.ts`** (or `server.mts` / compiled Node that still calls `TomorrowOS.listen` with `staticRoot`).
351
- - Keep **`@tomorrowos/sdk`** and put **`tsx` in `dependencies`** (not only `devDependencies`) if production runs via `tsx`.
367
+ - Serve TomorrowOS via **Vercel Function** `api/index.ts` `export default server` (Fluid + WebSockets).
368
+ - Keep root **`server.ts`** for **local** `npm start` / Preview internal port only (same `cms-app.ts`).
369
+ - Keep **`@tomorrowos/sdk`** (0.9.30+) and put **`tsx` in `dependencies`** for local start.
352
370
  - Node **20+**.
353
371
  - **Do not** replace the SDK with a hand-rolled server that drops WebSocket upgrades.
354
- - **Forbidden as the Production Deploy entrypoint:** `next start`, static export only, root `api/` fake catch-all for raw `http.Server`, or any command that does not start `TomorrowOS.listen(...)`.
372
+ - **Forbidden as the Production CMS:** `next start`, static export only, or a bare `(req,res)` handler without `http.Server` + `ws`.
355
373
 
356
374
  ### Expected `package.json` scripts
357
375
 
@@ -361,7 +379,7 @@ If an Agent moved `server.ts` → `api/index.ts` / `api/[...].ts` and only `/api
361
379
  "dev": "tsx watch server.ts",
362
380
  "dev:preview": "concurrently -k \"npm:dev:tomorrowos\" \"npm:dev:next\"",
363
381
  "dev:tomorrowos": "cross-env PORT=3001 TOMORROWOS_INTERNAL_PORT=3001 tsx watch server.ts",
364
- "dev:next": "next dev -p 3000",
382
+ "dev:next": "cd preview && next dev -p 3000",
365
383
  "start": "tsx server.ts",
366
384
  "build": "node -e \"process.exit(0)\""
367
385
  }
@@ -370,7 +388,8 @@ If an Agent moved `server.ts` → `api/index.ts` / `api/[...].ts` and only `/api
370
388
 
371
389
  Adapt script names / process runners as needed. **Invariant:**
372
390
 
373
- - **`npm start` / Production** = TomorrowOS Node only.
391
+ - **Local `npm start`** = TomorrowOS Node with `server.listen`.
392
+ - **Vercel Production** = Function `api/index.ts` (SDK `autoListen=false` when `VERCEL` is set).
374
393
  - **`build`** = no-op exit 0 (never `next build` for Production).
375
394
  - **Preview / v0** = Next on public port + TomorrowOS on internal port + proxy.
376
395
 
@@ -883,20 +902,21 @@ Tell the user:
883
902
 
884
903
  | Symptom | Likely cause | Fix |
885
904
  |---------|--------------|-----|
886
- | Publish `404: NOT_FOUND` | Framework Preset still **Next.js** / no Node start | Scheme A: Framework = **Other**, Start = `npm run start`, Output empty, Fluid on; redeploy |
887
- | Panel loads but **CMS unreachable** / `Unexpected token 'T', "The page c"...` | **`public/` deployed as static site** — no Node running | Scheme A: Output empty in dashboard, `start` in package.json; undo any `api/` migration; try **Path B** deploy |
888
- | Only `/api/status` returns JSON, `/status` is 404 | Wrong **api/ serverless** migration + broken rewrites | **A8 undo:** restore root `server.ts`, delete root `api/*`; never accept this as “fixed” |
889
- | `vercel.json` error: invalid `startCommand` | v0 schema rejects it | Remove from `vercel.json`; use `package.json` `start` + dashboard Start Command |
890
- | Agent moved server to `api/`, added `@vercel/node` handler | Off-protocol serverless refactor | **A8 undo** TomorrowOS needs one root `listen()` server, not per-request handlers |
891
- | `vercel inspect` shows Output = `public if it exists` | Framework=Other static default | Force Output Directory **empty** in dashboard; root `server.ts`; Path B redeploy |
905
+ | Publish `404: NOT_FOUND` | Framework Preset still **Next.js** / no Function | Scheme A: Framework = **Other**, Fluid on, `api/index.ts` + rewrites; redeploy |
906
+ | Panel loads but **CMS unreachable** / `Unexpected token 'T', "The page c"...` | **`public/` deployed as static site** — no Function | Rename to `cms-panel/`; Output empty; Scheme A Function entry; **Path B** if needed |
907
+ | Only `/api/status` returns JSON, `/status` is 404 | Missing **rewrites** to `/api` | Add Scheme A3 rewrite `/(.*) /api` (exclude existing `/api/`); redeploy |
908
+ | Control Panel OK, TV **could not connect** | WebSocket upgrade not reaching Function (200 HTML) | Fluid on; `export default server`; verify 101 on `/` and `/api`; check WebSockets permission |
909
+ | `vercel.json` error: invalid `startCommand` | v0 schema rejects it | Remove from `vercel.json`; Function entry does not need startCommand |
910
+ | Separate `api/ws.ts` with a second `new TomorrowOS` | Two isolates pairing memory split | **One** Function only (`api/index.ts` shared `cms-app.ts`) |
911
+ | `vercel inspect` shows Output = `public if it exists` | Framework=Other static default | Force Output Directory **empty**; use `cms-panel/`; Path B redeploy |
892
912
  | Build runs `next build` / missing routes-manifest | Next auto-detected from root `next.config` / `app/` | Move Next to `preview/`; Framework null/Other; `build` = no-op |
893
913
  | Preview blank but logs show TomorrowOS listening | v0 Next intercepts public port | Add Next reverse proxy → internal TomorrowOS port |
894
- | Preview works, Publish broken / Next-only | Production still on Next | Switch Production entry to `tsx server.ts` / `export default server` |
914
+ | Preview works, Publish broken / Next-only | Production still on Next | Switch Production to Fluid Function `api/index.ts` |
895
915
  | `ENETUNREACH` / IPv6 / `:5432` | Direct Postgres URL | Use **pooled** URL (Supabase `:6543` or Neon pooler) |
896
916
  | Control Panel OK, devices never stay paired after restart | SQLite / ephemeral disk | Use Supabase or Neon |
897
- | Uploads break / broken thumbs in prod | Local `public/uploads` | Cloudinary or Vercel Blob |
917
+ | Uploads break / broken thumbs in prod | Local uploads only | Cloudinary or Vercel Blob |
898
918
  | Blob uploads 401 / missing token | Blob not linked | Vercel Storage → Blob + `BLOB_READ_WRITE_TOKEN` |
899
- | WebSocket fails on Publish | Fluid off / static-only / Next as Production | Enable Fluid; sole Node `listen()` for Publish |
919
+ | WebSocket fails on Publish | Fluid off / static-only / Next as Production / no Function WS | Enable Fluid; Scheme A; test 101 upgrade |
900
920
  | Devices fail only on Preview | Next proxy cannot upgrade `wss` | Expected — pair on Publish URL |
901
921
  | Agent asked user to fill `TOMORROWOS_STORE` / `DATABASE_SSL` | Off-protocol | Agent must auto-set those after Neon/Supabase choice |
902
922
  | Agent asked Cloudinary key/secret in three chat turns | Off-protocol | Use **one Env popup** with all Cloudinary fields |
@@ -19,7 +19,7 @@ export interface ListenOptions {
19
19
  /**
20
20
  * If set, GET requests serve files from this directory (e.g. CMS UI assets).
21
21
  * `GET /` serves `staticIndex` (default `index.html`) under this root.
22
- * WebSocket upgrades on `/` are unchanged.
22
+ * WebSocket upgrades are accepted on `/` and common Vercel Function paths (`/api`, `/api/ws`).
23
23
  * Note: `GET /brand.json` is always served from the `brand` passed to the constructor (not from this folder).
24
24
  */
25
25
  staticRoot?: string;
@@ -28,6 +28,18 @@ export interface ListenOptions {
28
28
  * Default: `index.html`. Ignored when `staticRoot` is not set.
29
29
  */
30
30
  staticIndex?: string;
31
+ /**
32
+ * When true (default), call `server.listen(port)`.
33
+ * On Vercel Functions set false (or rely on auto-detect via `process.env.VERCEL`)
34
+ * and `export default server` — Vercel owns the socket; see
35
+ * https://vercel.com/docs/functions/websockets
36
+ */
37
+ autoListen?: boolean;
38
+ /**
39
+ * Pathnames that accept WebSocket upgrades (default: `/`, `/api`, `/api/ws`, …).
40
+ * Replit / Railway / local keep using `wss://host/`. Vercel Functions often mount at `/api`.
41
+ */
42
+ webSocketPaths?: string[];
31
43
  }
32
44
  export interface DeviceListItem {
33
45
  deviceId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
@@ -140,6 +140,31 @@ function resolveHelloDeviceId(msg) {
140
140
  : null;
141
141
  return deviceId;
142
142
  }
143
+ /** Default paths that accept device WebSocket upgrades (local + Vercel Functions). */
144
+ const DEFAULT_WEBSOCKET_PATHS = ["/", "/api", "/api/", "/api/ws", "/api/index"];
145
+ function normalizeWebSocketPaths(paths) {
146
+ const list = paths && paths.length > 0 ? paths : DEFAULT_WEBSOCKET_PATHS;
147
+ const out = new Set();
148
+ for (const raw of list) {
149
+ let p = String(raw || "").trim() || "/";
150
+ if (!p.startsWith("/"))
151
+ p = `/${p}`;
152
+ out.add(p);
153
+ if (p.length > 1 && p.endsWith("/"))
154
+ out.add(p.replace(/\/+$/, ""));
155
+ else if (p !== "/")
156
+ out.add(`${p}/`);
157
+ }
158
+ out.add("/");
159
+ return out;
160
+ }
161
+ function isAllowedWebSocketPath(pathname, allowed) {
162
+ const p = !pathname || pathname === "" ? "/" : pathname;
163
+ if (allowed.has(p))
164
+ return true;
165
+ const trimmed = p.replace(/\/+$/, "") || "/";
166
+ return allowed.has(trimmed) || allowed.has(`${trimmed}/`);
167
+ }
143
168
  const MAX_MEDIA_UPLOAD_BYTES = 100 * 1024 * 1024;
144
169
  async function readJsonBody(req) {
145
170
  const buf = await readRawBody(req, MAX_MEDIA_UPLOAD_BYTES);
@@ -773,6 +798,10 @@ export class TomorrowOS extends EventEmitter {
773
798
  const server = http.createServer((req, res) => {
774
799
  void this.handleHttp(req, res);
775
800
  });
801
+ // Match Vercel Functions WebSocket pattern: attach ws to the http.Server
802
+ // (https://vercel.com/docs/functions/websockets). Path filter keeps non-CMS
803
+ // upgrades from being accepted when the same process hosts other routes.
804
+ const allowedWsPaths = normalizeWebSocketPaths(options.webSocketPaths);
776
805
  const wss = new WebSocketServer({ noServer: true });
777
806
  server.on("upgrade", (request, socket, head) => {
778
807
  if (!request.url) {
@@ -780,23 +809,30 @@ export class TomorrowOS extends EventEmitter {
780
809
  return;
781
810
  }
782
811
  const { pathname } = new URL(request.url, `http://${request.headers.host}`);
783
- if (pathname === "/" || pathname === "") {
784
- wss.handleUpgrade(request, socket, head, (ws) => {
785
- wss.emit("connection", ws, request);
786
- });
787
- }
788
- else {
812
+ if (!isAllowedWebSocketPath(pathname, allowedWsPaths)) {
789
813
  socket.destroy();
814
+ return;
790
815
  }
816
+ wss.handleUpgrade(request, socket, head, (ws) => {
817
+ wss.emit("connection", ws, request);
818
+ });
791
819
  });
792
820
  wss.on("connection", (ws) => {
793
821
  this.handleConnection(ws);
794
822
  });
795
823
  this.serverStartedAt = new Date().toISOString();
796
- server.listen(port, host, () => {
824
+ const onVercel = Boolean(process.env.VERCEL);
825
+ const shouldListen = options.autoListen ?? !onVercel;
826
+ if (shouldListen) {
827
+ server.listen(port, host, () => {
828
+ // eslint-disable-next-line no-console
829
+ console.log(`[TomorrowOS] listening on http://${host}:${port}`);
830
+ });
831
+ }
832
+ else {
797
833
  // eslint-disable-next-line no-console
798
- console.log(`[TomorrowOS] listening on http://${host}:${port}`);
799
- });
834
+ console.log(`[TomorrowOS] http.Server ready (autoListen=false${onVercel ? ", Vercel" : ""}; export default server)`);
835
+ }
800
836
  this.httpServer = server;
801
837
  this.wss = wss;
802
838
  return server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.9.29",
3
+ "version": "0.9.30",
4
4
  "description": "TomorrowOS CMS server SDK - WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.9.29",
3
+ "version": "0.9.30",
4
4
  "description": "CMS server on @tomorrowos/sdk. Add your UI (React, static files, etc.) alongside this server.",
5
5
  "private": true,
6
6
  "type": "module",
@@ -13,7 +13,7 @@
13
13
  "build-player": "tomorrowos build --platform tizen"
14
14
  },
15
15
  "dependencies": {
16
- "@tomorrowos/sdk": "^0.9.29",
16
+ "@tomorrowos/sdk": "^0.9.30",
17
17
  "dotenv": "^17.2.3",
18
18
  "tsx": "^4.19.0"
19
19
  },
@@ -1,17 +1,19 @@
1
1
  # TomorrowOS CMS Starter (Vercel / v0)
2
2
 
3
- This starter is for **Vercel Publish** and **v0** no-code setup. It uses:
3
+ This starter is for **Vercel Publish** and **v0**. It follows
4
+ [Vercel Functions WebSockets](https://vercel.com/docs/functions/websockets):
4
5
 
5
- - Root **`server.ts`** with `export default server` (Vercel Fluid Node)
6
- - Control Panel static files in **`cms-panel/`** (not `public/` — avoids Vercel static-output trap)
7
- - **`preview/`**thin Next.js shell for v0 Preview only (`npm run dev:preview`)
8
- - **`vercel.json`**minimal Fluid config (no `startCommand`; use `package.json` `start`)
6
+ - **`cms-app.ts`** shared `TomorrowOS.listen` + `export const server`
7
+ - **`api/index.ts`** Production Function: `export default server` (Fluid)
8
+ - **`server.ts`**local / Preview: same server (`npm start`)
9
+ - **`cms-panel/`**Control Panel static files (not `public/`)
10
+ - **`preview/`** — Next.js shell for v0 Preview only
11
+ - **`vercel.json`** — `fluid`, rewrites → `/api`, `maxDuration`
9
12
 
10
13
  ## Scaffold
11
14
 
12
15
  ```bash
13
16
  npx @tomorrowos/sdk init my-cms --hosting v0
14
- # or: --hosting vercel
15
17
  ```
16
18
 
17
19
  ## Local development
@@ -21,39 +23,27 @@ npm install
21
23
  npm run dev
22
24
  ```
23
25
 
24
- Open `http://localhost:3000` TomorrowOS Control Panel.
26
+ Open `http://localhost:3000`. Pairing uses `ws://localhost:3000/` (root path).
25
27
 
26
- ### v0-style Preview (Next proxy + TomorrowOS on :3001)
28
+ ### v0-style Preview
27
29
 
28
30
  ```bash
29
31
  npm run dev:preview
30
32
  ```
31
33
 
32
- Open `http://localhost:3000` — Next proxies to TomorrowOS. Pair devices against **Publish URL**, not Preview.
34
+ Pair devices against the **Publish URL**, not Preview.
33
35
 
34
- ## Production (Vercel / v0 Publish)
36
+ ## Production acceptance
35
37
 
36
- - **Start:** `npm run start` → `tsx server.ts`
37
- - **Build:** no-op (`npm run build`)
38
- - **Acceptance:** `GET /status` on your production URL must return **JSON**
38
+ 1. `GET /status` → JSON
39
+ 2. `GET /` Control Panel
40
+ 3. WebSocket upgrade on `/` or `/api` **101** (not 200 HTML)
39
41
 
40
- Follow **`VERCEL_SETUP.md`** in `@tomorrowos/sdk` for database, media, and branding Q&A.
41
-
42
- ## Database
43
-
44
- Use Vercel Environment Variables (Neon or Supabase pooled URL recommended):
45
-
46
- ```env
47
- TOMORROWOS_STORE=postgres
48
- DATABASE_URL=postgresql://...pooler...
49
- DATABASE_SSL=true
50
- ```
51
-
52
- SQLite under `data/` is for local demos only — not for production on Vercel.
42
+ TV endpoint: `https://YOUR.vercel.app/` (players also try `/api` on `*.vercel.app`).
53
43
 
54
44
  ## Replit / Railway
55
45
 
56
- Use the default starter instead:
46
+ Use the default starter (unchanged Node listen, no `api/` Function):
57
47
 
58
48
  ```bash
59
49
  npx @tomorrowos/sdk init my-cms --hosting replit
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Vercel Function entry (Fluid + WebSockets).
3
+ * Mounted at `/api`. vercel.json rewrites `/` and `/status` etc. here so the
4
+ * Control Panel and device `wss://` upgrades hit the same Node http.Server.
5
+ *
6
+ * Pattern: https://vercel.com/docs/functions/websockets
7
+ * const server = …; export default server;
8
+ */
9
+
10
+ export { server as default } from "../cms-app.js";
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Shared TomorrowOS app for local Node and Vercel Functions.
3
+ * - Local / Railway-style: `server.ts` imports this (autoListen binds PORT).
4
+ * - Vercel Production: `api/index.ts` re-exports `server` (VERCEL → no listen;
5
+ * Fluid Function owns the socket — see https://vercel.com/docs/functions/websockets).
6
+ */
7
+
8
+ import "dotenv/config";
9
+ import { readFileSync } from "fs";
10
+ import { fileURLToPath } from "url";
11
+ import { dirname, join } from "path";
12
+ import { createTomorrowOSStore, TomorrowOS } from "@tomorrowos/sdk";
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
16
+ const store = createTomorrowOSStore({
17
+ databaseUrl:
18
+ process.env.SUPABASE_URL ||
19
+ process.env.NEON_DATABASE_URL ||
20
+ process.env.DATABASE_URL,
21
+ sqlitePath: join(__dirname, "data", "tomorrowos.db")
22
+ });
23
+
24
+ export const tomorrowos = new TomorrowOS({ brand, store });
25
+
26
+ const port = Number(
27
+ process.env.TOMORROWOS_INTERNAL_PORT || process.env.PORT || 3000
28
+ );
29
+
30
+ export const server = tomorrowos.listen({
31
+ port,
32
+ host: "0.0.0.0",
33
+ staticRoot: join(__dirname, "cms-panel")
34
+ // autoListen defaults to false when process.env.VERCEL is set
35
+ });
36
+
37
+ tomorrowos.on("device.paired", (event) => {
38
+ console.log(`[TomorrowOS] device paired: ${event.deviceId}`);
39
+ void tomorrowos.pushLatestPolicyToDevice(event.deviceId).then((r) => {
40
+ if (r.pushed) {
41
+ console.log(`[TomorrowOS] pushed latest policy to ${event.deviceId} (paired)`);
42
+ }
43
+ }).catch((err) => {
44
+ console.error(`[TomorrowOS] policy push on paired failed for ${event.deviceId}:`, err);
45
+ });
46
+ });
47
+
48
+ tomorrowos.on("device.online", (event) => {
49
+ console.log(`[TomorrowOS] device online: ${event.deviceId}`);
50
+ void tomorrowos.pushLatestPolicyToDevice(event.deviceId).then((r) => {
51
+ if (r.pushed) {
52
+ console.log(`[TomorrowOS] pushed latest policy to ${event.deviceId}`);
53
+ }
54
+ }).catch((err) => {
55
+ console.error(`[TomorrowOS] latest policy push failed for ${event.deviceId}:`, err);
56
+ });
57
+ });
58
+
59
+ tomorrowos.on("device.offline", (event) => {
60
+ console.log(
61
+ `[TomorrowOS] device offline: ${event.deviceId} (lastSeen: ${event.lastSeen})`
62
+ );
63
+ });
64
+
65
+ tomorrowos.on("command.verified", (event) => {
66
+ console.log(
67
+ `[TomorrowOS] command verified: ${event.commandId} (${event.method})`
68
+ );
69
+ });
70
+
71
+ tomorrowos.on("command.failed", (event) => {
72
+ console.error(
73
+ `[TomorrowOS] command failed: ${event.commandId} (${event.method}) — ${event.error.message}`
74
+ );
75
+ });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.9.29",
4
- "description": "TomorrowOS CMS for Vercel / v0 Publish (Node Fluid + optional Next Preview shell).",
3
+ "version": "0.9.30",
4
+ "description": "TomorrowOS CMS for Vercel / v0 Publish (Fluid Function + WebSockets + optional Next Preview).",
5
5
  "private": true,
6
6
  "type": "module",
7
7
  "engines": {
@@ -17,7 +17,7 @@
17
17
  "build-player": "tomorrowos build --platform tizen"
18
18
  },
19
19
  "dependencies": {
20
- "@tomorrowos/sdk": "^0.9.29",
20
+ "@tomorrowos/sdk": "^0.9.30",
21
21
  "dotenv": "^17.2.3",
22
22
  "tsx": "^4.19.0"
23
23
  },
@@ -1,75 +1,6 @@
1
1
  /**
2
- * TomorrowOS CMS server Vercel / v0 starter.
3
- * Production: root server.ts + Fluid (see vercel.json).
4
- * Preview: npm run dev:preview (Next in preview/ proxies to this server on :3001).
2
+ * Local / non-Vercel entry (`npm start`, Preview internal port).
3
+ * On Vercel Production the Function entry is `api/index.ts` (see vercel.json).
5
4
  */
6
5
 
7
- import "dotenv/config";
8
- import { readFileSync } from "fs";
9
- import { fileURLToPath } from "url";
10
- import { dirname, join } from "path";
11
- import { createTomorrowOSStore, TomorrowOS } from "@tomorrowos/sdk";
12
-
13
- const __dirname = dirname(fileURLToPath(import.meta.url));
14
- const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
15
- const store = createTomorrowOSStore({
16
- databaseUrl:
17
- process.env.SUPABASE_URL ||
18
- process.env.NEON_DATABASE_URL ||
19
- process.env.DATABASE_URL,
20
- sqlitePath: join(__dirname, "data", "tomorrowos.db")
21
- });
22
-
23
- const tomorrowos = new TomorrowOS({ brand, store });
24
-
25
- const port = Number(
26
- process.env.TOMORROWOS_INTERNAL_PORT || process.env.PORT || 3000
27
- );
28
-
29
- const server = tomorrowos.listen({
30
- port,
31
- host: "0.0.0.0",
32
- staticRoot: join(__dirname, "cms-panel")
33
- });
34
-
35
- tomorrowos.on("device.paired", (event) => {
36
- console.log(`[TomorrowOS] device paired: ${event.deviceId}`);
37
- void tomorrowos.pushLatestPolicyToDevice(event.deviceId).then((r) => {
38
- if (r.pushed) {
39
- console.log(`[TomorrowOS] pushed latest policy to ${event.deviceId} (paired)`);
40
- }
41
- }).catch((err) => {
42
- console.error(`[TomorrowOS] policy push on paired failed for ${event.deviceId}:`, err);
43
- });
44
- });
45
-
46
- tomorrowos.on("device.online", (event) => {
47
- console.log(`[TomorrowOS] device online: ${event.deviceId}`);
48
- void tomorrowos.pushLatestPolicyToDevice(event.deviceId).then((r) => {
49
- if (r.pushed) {
50
- console.log(`[TomorrowOS] pushed latest policy to ${event.deviceId}`);
51
- }
52
- }).catch((err) => {
53
- console.error(`[TomorrowOS] latest policy push failed for ${event.deviceId}:`, err);
54
- });
55
- });
56
-
57
- tomorrowos.on("device.offline", (event) => {
58
- console.log(
59
- `[TomorrowOS] device offline: ${event.deviceId} (lastSeen: ${event.lastSeen})`
60
- );
61
- });
62
-
63
- tomorrowos.on("command.verified", (event) => {
64
- console.log(
65
- `[TomorrowOS] command verified: ${event.commandId} (${event.method})`
66
- );
67
- });
68
-
69
- tomorrowos.on("command.failed", (event) => {
70
- console.error(
71
- `[TomorrowOS] command failed: ${event.commandId} (${event.method}) — ${event.error.message}`
72
- );
73
- });
74
-
75
- export default server;
6
+ export { server as default } from "./cms-app.js";
@@ -3,5 +3,15 @@
3
3
  "fluid": true,
4
4
  "framework": null,
5
5
  "installCommand": "npm install",
6
- "buildCommand": "npm run build"
6
+ "buildCommand": "npm run build",
7
+ "functions": {
8
+ "api/index.ts": {
9
+ "maxDuration": 800,
10
+ "memory": 1024,
11
+ "includeFiles": "{cms-panel/**,brand.json,assets/**}"
12
+ }
13
+ },
14
+ "rewrites": [
15
+ { "source": "/((?!api/).*)", "destination": "/api" }
16
+ ]
7
17
  }