@tomorrowos/sdk 0.9.30 → 0.9.31

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
@@ -74,12 +74,13 @@ In Vercel Agent (or any AI setup prompt), paste:
74
74
  ```text
75
75
  Follow @tomorrowos/sdk VERCEL_SETUP.md and set up my TomorrowOS CMS.
76
76
  IF any step contradicts Vercel, adapt to settings that Vercel can accept.
77
+ After each question, configure that step immediately before asking the next.
77
78
  ```
78
79
 
79
- That wizard asks: database (**Supabase** → **Neon** → SQLite demo), media (**Cloudinary** → **Vercel Blob** → local),
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 a Fluid Vercel Function** (`api/index.ts` + `export default server`, WebSockets per Vercel docs). Cloudinary uses **one Env popup** (all fields).
82
- Neon/Supabase: agent auto-sets `TOMORROWOS_STORE` + `DATABASE_SSL`. Sets `cms.hostingTarget` to `"vercel"`.
80
+ That wizard asks: database (**Supabase** → **Neon** → SQLite demo), media (**Cloudinary recommended** → **Vercel Blob** → local),
81
+ branding (manual name/colours/background/logo **or** **website URL → full `brand.json`**, no login unless asked).
82
+ **Configure immediately after each answer** (e.g. Cloudinary Env popup right after choosing Cloudinary).
83
+ **Publish stays a Fluid Vercel Function** (`api/index.ts` + `export default server`, WebSockets per Vercel docs).
83
84
 
84
85
  ### Replit upgrade (existing CMS)
85
86
 
package/VERCEL_SETUP.md CHANGED
@@ -5,7 +5,8 @@
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.8` — pairs with `@tomorrowos/sdk` **0.9.30+** (`templates/cms-starter-v0`, Fluid Functions WebSockets).
8
+ > Protocol id: `vercel-setup/1.10` — pairs with `@tomorrowos/sdk` **0.9.31+** (`cms-starter-v0`: Fluid Publish + root Next Preview proxy).
9
+ > **IRON RULE — configure immediately:** After the user answers **each** question, **finish that question’s configuration before asking the next**. Never batch “answer Q1–Q3 first, then configure everything.”
9
10
 
10
11
  ## User trigger (copy-paste)
11
12
 
@@ -16,12 +17,14 @@ Follow @tomorrowos/sdk VERCEL_SETUP.md and set up my TomorrowOS CMS.
16
17
  Ask only the questions listed in VERCEL_SETUP.md, in order. Do not use LLM_PROMPT.md or REPLIT_SETUP.md.
17
18
  Do not ask for OpenAI / Anthropic / any LLM API keys — TomorrowOS CMS setup does not require them.
18
19
  If any step contradicts Vercel, adapt to settings Vercel can accept (document the adaptation).
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
- Question 3 only updates brand.json from colours/logo/name (a website URL is brand reference onlydo not add CMS login/auth unless the user explicitly asks). Do not skip steps.
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), 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.
20
+ CRITICAL UX: After I answer each question, configure that step immediately (Env popup / brand.json write) BEFORE asking the next question. Do not wait until all three questions are answered.
21
+ Question 2 options MUST be exactly: 1 Cloudinary (recommended), 2 Vercel Blob, 3 local diskNEVER invent Supabase Storage or “disable media uploads”.
22
+ Question 3 must offer Option A (website URL brand.json) OR Option B (name + colours + background + logo) — NEVER ask only for display name.
23
+ 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.
24
+ Do not add CMS login/auth unless I explicitly ask. Do not skip steps.
25
+ Use Next.js only as a v0/Preview shell with a reverse proxy; Production Publish must remain Fluid Function TomorrowOS (api/index.ts).
26
+ Before claiming Publish works: Framework Other, Fluid on, GET /status JSON, GET / Control Panel, WebSocket upgrade 101 on / or /api.
27
+ Do not put startCommand or processes in vercel.json. Use cms-starter-v0 layout. Do not create a second TomorrowOS in api/ws.ts.
25
28
  ```
26
29
 
27
30
  Alternate triggers:
@@ -128,15 +131,25 @@ v0 Preview is often a **Next.js runtime** hardwired to `next dev`. A raw `tsx se
128
131
 
129
132
  ## Questionnaire scope (STRICT)
130
133
 
131
- Ask **only** these questions, in **this exact order**:
134
+ Ask **only** these questions, in **this exact order**. After each answer, **configure that step immediately**, then proceed:
132
135
 
133
- | Step | Section | When |
134
- |------|---------|------|
135
- | 1 | **Question 1** — Database (Supabase → Neon → SQLite) | Always |
136
- | 2 | **Question 2** — Media storage (Cloudinary → Vercel Blob → local) | Always (after Q1) |
137
- | 3 | **Question 3** — Brand / TomorrowOS look (`brand.json` only) | Always last |
136
+ | Step | Section | When done, before next question |
137
+ |------|---------|----------------------------------|
138
+ | 1 | **Question 1** — Database (Supabase → Neon → SQLite) | Collect DB URL / confirm SQLite → set Env Vars (`SUPABASE_URL` or `DATABASE_URL` + auto `TOMORROWOS_STORE` / `DATABASE_SSL`) |
139
+ | 2 | **Question 2** — Media (**Cloudinary recommended** → Vercel Blob → local) | If Cloudinary/Blob: **open Env popup immediately** and save tokens before Q3 |
140
+ | 3 | **Question 3** — Brand (`brand.json`: name, colours, background, logo, or website URL) | Write `brand.json` immediately, then run final deploy checklist |
138
141
 
139
- **That is the complete list.** Do **not** ask how many screens / devices.
142
+ **Forbidden invented Question 2 options (never offer these):**
143
+
144
+ - ❌ “Supabase Storage” / S3 bucket / `SUPABASE_SERVICE_ROLE_KEY` for media
145
+ - ❌ “No media storage” / “disable uploads” / “remote URLs only” as a numbered choice
146
+ - ❌ Replacing Cloudinary with Blob as the **recommended** option
147
+
148
+ **Forbidden Question 3 shortcuts:**
149
+
150
+ - ❌ Asking **only** for brand / product display name
151
+ - ❌ Skipping background colour, primary colour, and logo when the user chose manual fields
152
+ - ❌ Skipping Option A (website URL → infer full `brand.json`)
140
153
 
141
154
  **Forbidden extra questions (never ask, even if the user skips a step):**
142
155
 
@@ -160,25 +173,25 @@ Then ask **Question 1**.
160
173
  ## Hard rules
161
174
 
162
175
  1. **Ask only VERCEL_SETUP.md questions 1–3.** Never use `LLM_PROMPT.md` or `REPLIT_SETUP.md` as the questionnaire. **Never** ask for OpenAI or other LLM API keys.
163
- 2. **Cloudinary:** open **one** Vercel Env configuration popup / form with **all** Cloudinary fields together (cloud name + API key + API secret). Do **not** quiz the user one credential at a time in chat.
164
- 3. **Do not invent** Cloudinary credentials, database URLs, Vercel Blob tokens, brand colours, **or LLM API keys**.
165
- 4. **Postgres env naming:** prefer **`SUPABASE_URL`** when using Supabase; prefer **`DATABASE_URL`** when using Neon. `createTomorrowOSStore` accepts `SUPABASE_URL` then `DATABASE_URL`.
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).
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.
168
- 7. **Never commit secrets.** Use Vercel Environment Variables (+ optional non-secret flags in committed `.env.example`).
169
- 8. **Skip Replit-only files:** do **not** create `.replit`, `.replit-artifact`, or Replit `deploymentTarget`.
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`.
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`.
173
- 12. **Preview:** if the environment is v0 / Next-hardwired, install the Next reverse-proxy shell (see **Preview adapter**). Do not leave Preview broken.
174
- 13. After Q&A: **configure → install → run/deploy → minimal verify** (Control Panel at `/` + WS 101). Do not run a long test suite.
176
+ 2. **Configure immediately after each answer.** Flow is `ask user answers configure that step ask next`. Do **not** collect all answers then configure in a batch at the end.
177
+ 3. **Cloudinary:** as soon as the user picks Cloudinary, open **one** Vercel Env configuration popup / form with **all** Cloudinary fields together (cloud name + API key + API secret). Do **not** quiz one credential at a time in chat. Do **not** skip Cloudinary in favor of inventing Supabase Storage.
178
+ 4. **Do not invent** Cloudinary credentials, database URLs, Vercel Blob tokens, brand colours, **or LLM API keys**.
179
+ 5. **Postgres env naming:** prefer **`SUPABASE_URL`** when using Supabase; prefer **`DATABASE_URL`** when using Neon. `createTomorrowOSStore` accepts `SUPABASE_URL` then `DATABASE_URL`.
180
+ 6. **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).
181
+ 7. **Prefer `npx @tomorrowos/sdk init --hosting v0`** (template `cms-starter-v0`). For Replit/Railway use default `init` (`cms-starter`). Do not rebuild pairing / WebSocket / playlist APIs.
182
+ 8. **Never commit secrets.** Use Vercel Environment Variables (+ optional non-secret flags in committed `.env.example`).
183
+ 9. **Skip Replit-only files:** do **not** create `.replit`, `.replit-artifact`, or Replit `deploymentTarget`.
184
+ 10. **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`.
185
+ 11. **Enable Fluid compute** for Production WebSockets ([docs](https://vercel.com/docs/functions/websockets)). Confirm Project settings after scaffold.
186
+ 12. **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`.
187
+ 13. After Q1–Q3 each configured: **install → deploy → minimal verify** (Control Panel + WS 101). Do not run a long test suite.
175
188
  14. **No inventing CMS login.** A branding URL never means “build auth.” Only add login if the user **explicitly** requests it.
176
189
  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.
177
190
  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.
178
191
  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
192
  18. **Scheme A is doc-driven:** Production = Fluid Function + `export default server` per Vercel WebSockets docs — Replit starter stays unchanged.
180
193
 
181
- **Question order:** Q1 → Q2 → Q3 → execution checklist.
194
+ **Question order:** Q1 → **configure Q1** → Q2 → **configure Q2** → Q3 → **write brand.json** → deploy checklist.
182
195
 
183
196
  ---
184
197
 
@@ -192,10 +205,10 @@ Then ask **Question 1**.
192
205
 
193
206
  | Layer | Preview (v0) | Publish (Production) |
194
207
  |-------|----------------|----------------------|
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`) |
198
- | Next files | Allowed under `preview/` | **Must not** live at project root |
208
+ | Host | **Root Next** `app/[[...slug]]/route.ts` proxies TomorrowOS `:3001` | **Vercel Function** `api/index.ts` → `export default server` |
209
+ | Static UI | Proxied from TomorrowOS `cms-panel/` | Served by **`staticRoot`** inside `listen()` |
210
+ | WebSocket | Optional / may fail in Preview | **Required** — Fluid Function upgrade |
211
+ | Next files | **Root** `app/` + `next.config.mjs` (required for v0) | Ignored when Framework = Other + no-op `build` + Function entry |
199
212
 
200
213
  **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
214
 
@@ -204,48 +217,41 @@ Then ask **Question 1**.
204
217
  **Production (Vercel / v0 starter):**
205
218
 
206
219
  ```
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
211
- package.json
220
+ cms-app.ts
221
+ api/index.ts ← Publish Function
222
+ server.ts ← local listen entry
223
+ app/ Preview ONLY (Next proxy catch-all)
224
+ layout.tsx
225
+ [[...slug]]/route.ts
226
+ next.config.mjs ← Preview only
227
+ vercel.json
228
+ cms-panel/
212
229
  brand.json
213
- cms-panel/ CMS static UI (NOT named public/ — avoids static-output trap)
214
- preview/ ← Next proxy for v0 Preview only
215
- ```
216
-
217
- **Preview only — keep ALL Next under `preview/`:**
218
-
219
- ```
220
- preview/
221
- next.config.mjs
222
- app/
223
-
230
+ package.json "dev": "npm run dev:preview"
224
231
  ```
225
232
 
226
233
  **Do NOT:**
227
234
 
228
- - ❌ Leave `next.config.mjs` + `app/` at project root
235
+ - ❌ Add `app/page.tsx` that returns null (v0 shows “Your v0 generation will show here”)
229
236
  - ❌ 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)
237
+ - ❌ Create a **second** TomorrowOS instance in `api/ws.ts`
238
+ - ❌ Use `next build` / `next start` for Production
239
+ - ❌ Put `startCommand` / `processes` in `vercel.json`
233
240
 
234
241
  **`package.json` scripts:**
235
242
 
236
243
  ```json
237
244
  {
238
245
  "scripts": {
239
- "dev": "tsx watch server.ts",
246
+ "dev": "npm run dev:preview",
240
247
  "dev:preview": "concurrently -k \"npm:dev:tomorrowos\" \"npm:dev:next\"",
241
248
  "dev:tomorrowos": "cross-env TOMORROWOS_INTERNAL_PORT=3001 tsx watch server.ts",
242
- "dev:next": "cd preview && next dev -p 3000",
249
+ "dev:next": "next dev -p 3000",
243
250
  "start": "tsx server.ts",
244
251
  "build": "node -e \"process.exit(0)\""
245
252
  }
246
253
  }
247
254
  ```
248
-
249
255
  - **Local / Railway-style:** `npm start` → `tsx server.ts` → SDK calls `server.listen` (no `VERCEL` env).
250
256
  - **Vercel Production:** Function loads `api/index.ts`; SDK skips `listen` when `process.env.VERCEL` is set and Vercel owns the socket.
251
257
 
@@ -282,7 +288,7 @@ SDK behaviour (0.9.30+):
282
288
  "buildCommand": "npm run build",
283
289
  "functions": {
284
290
  "api/index.ts": {
285
- "maxDuration": 800,
291
+ "maxDuration": 300,
286
292
  "memory": 1024,
287
293
  "includeFiles": "{cms-panel/**,brand.json,assets/**}"
288
294
  }
@@ -293,6 +299,8 @@ SDK behaviour (0.9.30+):
293
299
  }
294
300
  ```
295
301
 
302
+ **`maxDuration` plan limits:** Hobby / many default plans allow **1–300** seconds. **800** requires a higher Vercel plan. Prefer **`300`** unless the account already allows more — otherwise Publish fails with *“maxDuration must be between 1 and 300… upgrade your plan”*.
303
+
296
304
  **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.
297
305
 
298
306
  **v0 schema:** never add `startCommand` or `processes`.
@@ -376,10 +384,10 @@ If (1)+(2) pass but (3) fails → Function is serving HTTP only. Re-check Fluid,
376
384
  ```json
377
385
  {
378
386
  "scripts": {
379
- "dev": "tsx watch server.ts",
387
+ "dev": "npm run dev:preview",
380
388
  "dev:preview": "concurrently -k \"npm:dev:tomorrowos\" \"npm:dev:next\"",
381
389
  "dev:tomorrowos": "cross-env PORT=3001 TOMORROWOS_INTERNAL_PORT=3001 tsx watch server.ts",
382
- "dev:next": "cd preview && next dev -p 3000",
390
+ "dev:next": "next dev -p 3000",
383
391
  "start": "tsx server.ts",
384
392
  "build": "node -e \"process.exit(0)\""
385
393
  }
@@ -388,10 +396,10 @@ If (1)+(2) pass but (3) fails → Function is serving HTTP only. Re-check Fluid,
388
396
 
389
397
  Adapt script names / process runners as needed. **Invariant:**
390
398
 
391
- - **Local `npm start`** = TomorrowOS Node with `server.listen`.
399
+ - **Local / v0 Preview `npm run dev`** = TomorrowOS `:3001` + Next `:3000` proxy.
392
400
  - **Vercel Production** = Function `api/index.ts` (SDK `autoListen=false` when `VERCEL` is set).
393
401
  - **`build`** = no-op exit 0 (never `next build` for Production).
394
- - **Preview / v0** = Next on public port + TomorrowOS on internal port + proxy.
402
+ - **Do not** ship an empty `app/page.tsx` (v0 placeholder).
395
403
 
396
404
  ### Minimal dependencies (Production)
397
405
 
@@ -469,32 +477,37 @@ export default server;
469
477
 
470
478
  Load env with `dotenv` for local/`vercel dev`. On production Deploy, Vercel injects Environment Variables natively.
471
479
 
472
- ### Preview adapter (v0 / Next reverse proxy) — required when Preview is Next-hardwired
480
+ ### Preview adapter (v0 / Next reverse proxy) — required for Preview UI
473
481
 
474
- Detect Preview / v0 (Next already present, or Preview blank while TomorrowOS logs “listening”). Then:
482
+ v0 Preview is hardwired to **root** Next (`app/`, `next.config.mjs`, `next dev`). An empty `app/page.tsx` (or returning `null`) produces **“Your v0 generation will show here”** and never shows the Control Panel.
475
483
 
476
- 1. Keep a **minimal** Next app **isolated when possible** (e.g. under `preview/`) so Production Framework detection does not latch onto root `next.config.*`.
477
- 2. TomorrowOS listens on **`127.0.0.1:TOMORROWOS_INTERNAL_PORT`** (default **3001**).
478
- 3. Add a **catch-all** Next route (or middleware) that proxies HTTP to TomorrowOS:
484
+ **Required layout (cms-starter-v0):**
479
485
 
480
- ```ts
481
- // Example: preview app proxy handler
482
- // Forward method, headers, and body to http://127.0.0.1:3001/<path>
483
- // Return the upstream status, headers, and body to the browser.
486
+ ```
487
+ app/
488
+ layout.tsx ← minimal shell
489
+ [[...slug]]/route.ts ← proxy ALL methods to http://127.0.0.1:3001
490
+ next.config.mjs ← Preview only (no empty page.tsx)
491
+ cms-app.ts / server.ts ← TomorrowOS on TOMORROWOS_INTERNAL_PORT=3001
492
+ api/index.ts ← Production Function only (Publish unchanged)
484
493
  ```
485
494
 
486
- Preferred UX: proxy **`/`** itself (rewrites) so the Preview URL shows the Control Panel at `/`, not only under `/api/...`.
495
+ **Agent must:**
487
496
 
488
- 4. Start **both** processes for Preview (`dev:preview` pattern above).
489
- 5. Document in the final summary: **Preview = Next shell + proxy; Publish = Node only.**
497
+ 1. Put the Next proxy at **project root** `app/` (v0 does not reliably use `preview/` alone).
498
+ 2. **Do not** add `app/page.tsx` that returns null / placeholder copy.
499
+ 3. Implement `app/[[...slug]]/route.ts` that forwards method, headers, and body to `TOMORROWOS_INTERNAL_URL` (default `http://127.0.0.1:3001`) and returns upstream status/body. On connection failure, return a short HTML 502 explaining `npm run dev`.
500
+ 4. Set `"dev": "npm run dev:preview"` so the default v0/dev command starts **both** TomorrowOS (:3001) and Next (:3000).
501
+ 5. Keep Production on Fluid Function (`api/index.ts`, `framework: null`, no-op `build`). **Do not** make `next start` / `next build` the Publish path.
502
+ 6. Document: **Preview = Next proxy + TomorrowOS :3001; Publish = api/index.ts Fluid (unchanged).**
490
503
 
491
504
  **Forbidden:**
492
505
 
493
- - Making Next the Production CMS or the only server after Publish.
494
- - Deleting TomorrowOS `server.ts` because Preview needed Next.
495
- - Claiming setup complete if Production entrypoint is still `next start`.
496
- - Claiming setup complete if Production `GET /` is `404: NOT_FOUND`.
497
- - Creating root `api/index.*` + `routes` catch-all that exports a raw `http.Server` “to fix 404”.
506
+ - Empty v0 placeholder page as the Preview UX
507
+ - Making Next the Production CMS
508
+ - Deleting `api/index.ts` / `cms-app.ts` to “fix” Preview
509
+ - Claiming Preview complete while the iframe still says “Your v0 generation will show here”
510
+ - Claiming setup complete if Production entrypoint is still `next start` or `GET /` is `404: NOT_FOUND`
498
511
 
499
512
  ### `vercel.json` (Production — explicit Node, not Next)
500
513
 
@@ -552,7 +565,8 @@ If Vercel’s UI and this file disagree on bundling, **prefer whatever keeps a s
552
565
 
553
566
  ## Question 1 — Database (always)
554
567
 
555
- > **Do not** ask screen counts. Present all three options; recommend **Supabase** for Vercel fleets.
568
+ > **Do not** ask screen counts. Present all three options; recommend **Supabase** for Vercel fleets.
569
+ > **After the user picks 1 / 2 / 3:** finish Step B/C/D (collect URL or confirm SQLite + set Env Vars) **before** asking Question 2.
556
570
 
557
571
  ### Step A — Ask storage choice
558
572
 
@@ -570,6 +584,8 @@ If Vercel’s UI and this file disagree on bundling, **prefer whatever keeps a s
570
584
 
571
585
  ### Step B — If **1 / Supabase**
572
586
 
587
+ **Immediately** (same turn as acknowledging their choice — do not ask Q2 yet) ask for the connection string:
588
+
573
589
  **Ask exactly (connection string only):**
574
590
 
575
591
  > Paste your Supabase Postgres connection string. I will store it as **`SUPABASE_URL`**.
@@ -582,17 +598,20 @@ If Vercel’s UI and this file disagree on bundling, **prefer whatever keeps a s
582
598
 
583
599
  **You must then (automatic — do NOT ask the user to type these):**
584
600
 
585
- 1. Set Vercel Env Vars:
601
+ 1. Set Vercel Env Vars **now** (open Env UI if available):
586
602
  - `SUPABASE_URL=<user pooler string>`
587
603
  - `TOMORROWOS_STORE=supabase` ← **agent sets this**
588
604
  - `DATABASE_SSL=true` ← **agent sets this** (unless user already said SSL is off)
589
- 2. Wire `server.ts` as in **Runtime & Vercel deploy rules**.
605
+ 2. Wire store in `cms-app.ts` / `server.ts` as in **Runtime & Vercel deploy rules**.
590
606
  3. Optional committed `.env.example` with **placeholders only**.
591
607
  4. **Do not** commit the real connection string.
592
608
  5. **Do not** open a second form asking for `TOMORROWOS_STORE` or `DATABASE_SSL`.
609
+ 6. **Only after Env Vars are saved** → ask **Question 2**.
593
610
 
594
611
  ### Step C — If **2 / Neon**
595
612
 
613
+ **Immediately** ask for the connection string (do not ask Q2 yet):
614
+
596
615
  **Ask exactly (connection string only):**
597
616
 
598
617
  > Paste your **Neon pooled** Postgres connection string. I will store it as **`DATABASE_URL`**.
@@ -603,14 +622,15 @@ If Vercel’s UI and this file disagree on bundling, **prefer whatever keeps a s
603
622
 
604
623
  **You must then (automatic — do NOT ask the user to type these):**
605
624
 
606
- 1. Set Vercel Env Vars:
625
+ 1. Set Vercel Env Vars **now**:
607
626
  - `DATABASE_URL=<user pooled string>`
608
627
  - `TOMORROWOS_STORE=postgres` ← **agent sets this**
609
628
  - `DATABASE_SSL=true` ← **agent sets this**
610
- 2. Wire `server.ts` — `createTomorrowOSStore` uses `PostgresStore` for `postgres` / `supabase` drivers.
629
+ 2. Wire `createTomorrowOSStore` for Postgres.
611
630
  3. **Do not** commit the real connection string.
612
- 4. **Do not** ask the user to fill `TOMORROWOS_STORE` or `DATABASE_SSL` in chat or in an Env popup. Those are derived from choosing Neon.
631
+ 4. **Do not** ask the user to fill `TOMORROWOS_STORE` or `DATABASE_SSL`.
613
632
  5. If you see `ENETUNREACH` on `:5432`, switch to Neon’s **pooled** URL.
633
+ 6. **Only after Env Vars are saved** → ask **Question 2**.
614
634
 
615
635
  ### Step D — If **3 / SQLite**
616
636
 
@@ -623,8 +643,9 @@ Only proceed after the user explicitly confirms.
623
643
  **You must then (automatic):**
624
644
 
625
645
  1. Set `TOMORROWOS_STORE=sqlite` yourself (do not ask the user to type it).
626
- 2. Keep `sqlitePath` in `server.ts` for `data/tomorrowos.db`.
646
+ 2. Keep `sqlitePath` in `cms-app.ts` for `data/tomorrowos.db`.
627
647
  3. Warn again in the final summary that they should move to Supabase or Neon for real devices.
648
+ 4. **Then** ask **Question 2**.
628
649
 
629
650
  **Later in Question 3:** set `cms.hostingTarget` to **`"vercel"`** (all Q1 branches).
630
651
 
@@ -633,25 +654,26 @@ Only proceed after the user explicitly confirms.
633
654
  ## Question 2 — Media storage
634
655
 
635
656
  > **This question is only about media files (images/videos).**
636
- > It is **not** about OpenAI, AI pairing, or LLM keys. If you are about to ask for an `sk-` key, **stop** — you are off-protocol.
657
+ > It is **not** about OpenAI, AI pairing, or LLM keys. If you are about to ask for an `sk-` key, **stop** — you are off-protocol.
658
+ > **Copy the three options below verbatim.** Do **not** substitute Supabase Storage, “disable uploads”, or make Vercel Blob the recommended option.
637
659
 
638
660
  ### Step A — Ask storage choice
639
661
 
640
- **Ask exactly:**
662
+ **Ask exactly (wording must match):**
641
663
 
642
664
  > How should playlist media (images/videos) be stored?
643
665
  >
644
- > **1. Cloudinary (recommended)** — durable public HTTPS URLs (`https://res.cloudinary.com/...`). Works out of the box with `@tomorrowos/sdk` auto-detection.
666
+ > **1. Cloudinary (recommended)** — durable public HTTPS URLs (`https://res.cloudinary.com/...`). Works out of the box with `@tomorrowos/sdk` auto-detection. **Prefer this on Vercel.**
645
667
  >
646
- > **2. Vercel Blob** — Vercel-native object storage; durable `https://*.public.blob.vercel-storage.com/...` URLs. Good when you want media on the same Vercel project without a Cloudinary account.
668
+ > **2. Vercel Blob** — Vercel-native object storage; durable `https://*.public.blob.vercel-storage.com/...` URLs. Use when you want media on the same Vercel project without a Cloudinary account.
647
669
  >
648
- > **3. Local disk only** — `public/uploads` (OK for quick local tests; **not** for production Vercel fleets — files are ephemeral).
670
+ > **3. Local disk only** — `cms-panel/uploads` (OK for quick local tests; **not** for production Vercel fleets — files are ephemeral).
649
671
  >
650
672
  > Reply with **1**, **2**, or **3** (or “Cloudinary” / “Vercel Blob” / “local”).
651
673
 
652
- ### Step B — If **1 / Cloudinary** (same Question 2 **one Env popup**)
674
+ ### Step B — If **1 / Cloudinary** (configure **immediately** — one Env popup)
653
675
 
654
- > **Critical UX rule:** Immediately open the **Vercel Environment Variables configuration popup / form** with **all Cloudinary fields in one UI**. Do **not** ask for cloud name, then key, then secret in separate chat messages.
676
+ > **Critical UX rule:** As soon as the user chooses Cloudinary, **immediately** open the **Vercel Environment Variables configuration popup / form** with **all Cloudinary fields in one UI**. Do **not** ask Q3 first. Do **not** ask for cloud name, then key, then secret in separate chat messages.
655
677
 
656
678
  **Preferred:** Vercel Env Vars UI (or Agent secrets form) with these fields together:
657
679
 
@@ -677,10 +699,13 @@ Only proceed after the user explicitly confirms.
677
699
  2. **Do not** invent placeholders.
678
700
  3. **Do not** proceed to Question 3 until all three required vars exist.
679
701
  4. **Do not** run a formal Cloudinary upload test unless the user asks.
702
+ 5. **Only after Cloudinary Env Vars are saved** → ask **Question 3**.
680
703
 
681
704
  The SDK auto-detects these env vars.
682
705
 
683
- ### Step C — If **2 / Vercel Blob** (same Question 2)
706
+ ### Step C — If **2 / Vercel Blob** (configure **immediately**)
707
+
708
+ **Immediately** (do not ask Q3 yet):
684
709
 
685
710
  **Ask exactly:**
686
711
 
@@ -708,46 +733,51 @@ import { put } from "@vercel/blob";
708
733
 
709
734
  4. Warn: until the project’s Blob bridge is wired, Control Panel **Media** status may show local/ephemeral — verify uploads return `https://*.blob.vercel-storage.com/...` before calling setup complete.
710
735
  5. Do **not** proceed to Question 3 until `BLOB_READ_WRITE_TOKEN` exists (or Blob is linked and token is confirmed in the Vercel dashboard).
736
+ 6. **Only then** → ask **Question 3**.
711
737
 
712
738
  ### Step D — If **3 / local uploads**
713
739
 
714
- 1. `mkdir -p public/uploads`
715
- 2. Warn clearly: files may vanish on redeploy; players need stable HTTPS URLs — Cloudinary or Vercel Blob is strongly preferred for Vercel.
740
+ 1. Ensure `cms-panel/uploads` exists (or `public/uploads` on Replit-style starters).
741
+ 2. Warn clearly: files may vanish on redeploy; players need stable HTTPS URLs — **Cloudinary is strongly preferred** on Vercel.
742
+ 3. **Then** ask **Question 3**.
716
743
 
717
744
  ---
718
745
 
719
746
  ## Question 3 — Brand (`brand.json` only)
720
747
 
721
- > Updates **only** `brand.json`. Does **not** change Vercel project settings, Env Vars, or `server.ts` store wiring beyond what Q1–Q2 already required.
748
+ > Updates **only** `brand.json`. Does **not** change Vercel project settings, Env Vars, or store wiring beyond what Q1–Q2 already required.
722
749
  >
723
750
  > **IRON RULE — website URL ≠ build a website / login / auth CMS.**
724
- > If the user pastes a URL (or says “make it look like this site”), that input is **reference material for `brand.json` only** (name, colours, fonts, logo, tagline).
751
+ > If the user pastes a URL (or says “make it look like this site”), that input is **reference material for `brand.json` only** (name, colours, fonts, logo, tagline, **background**).
725
752
  > **Do not** scaffold a login page, signup, OAuth, gated dashboard, or copy the reference site’s IA/pages — **unless the user explicitly asks for CMS login / auth**.
726
- > Default TomorrowOS Control Panel has **no login**. Keep it that way.
753
+ > Default TomorrowOS Control Panel has **no login**. Keep it that way.
754
+ > **Never ask only for a display name.** Always offer Option A (URL) **or** the full Option B field list below.
727
755
 
728
756
  ### Step A — Ask branding input
729
757
 
730
758
  **Ask exactly:**
731
759
 
732
- > Let’s brand your TomorrowOS experience. You can answer in either way:
760
+ > Let’s brand your TomorrowOS experience. Choose **one**:
733
761
  >
734
- > **Option A — Website URL only (brand reference)**
735
- > Paste **one public website URL**. I will use it **only** to infer colours, fonts, name, tagline, and logo for **`brand.json`**.
736
- > I will **not** rebuild that website, add a login page, or change Control Panel features — unless you explicitly ask for login/auth later.
762
+ > **Option A — Website URL (recommended if you have a site)**
763
+ > Paste **one public website URL**. I will use it **only** to fill **`brand.json`**: product name, tagline, primary / secondary / **background** / text colours, fonts, and logo.
764
+ > I will **not** rebuild that website or add login — unless you explicitly ask for login/auth later.
737
765
  >
738
- > **Option B — Manual fields**
739
- > Provide:
740
- > 1. **Product / venue name**
766
+ > **Option B — Manual brand fields**
767
+ > Send as many of these as you can (name + at least one colour + logo is ideal):
768
+ > 1. **Product / venue name** (required)
741
769
  > 2. **Tagline** (optional)
742
770
  > 3. **Primary colour** (hex, e.g. `#FF8A3D`)
743
- > 4. **Background colour** (hex, optional default `#FAFAF9`)
771
+ > 4. **Background colour** (hex — page / panel background)
744
772
  > 5. **Text colour** (hex, optional — default `#0A0908`)
745
773
  > 6. **Secondary / accent colour** (hex, optional)
746
- > 7. **Logo** — upload SVG/PNG into the project, or a URL I can fetch into `./assets/`
774
+ > 7. **Logo** — upload SVG/PNG into the project, or a public image URL I can fetch into `./assets/`
775
+ >
776
+ > Reply with a URL **or** the manual fields. Do not send only a name with no colours/logo unless you want defaults (I will confirm defaults before writing).
747
777
 
748
778
  ### Step B — If the user gives **only a website URL** (Option A)
749
779
 
750
- **You must:**
780
+ **You must (immediately — do not defer brand write to “end of setup”):**
751
781
 
752
782
  1. **Fetch and inspect** the page (HTTP GET the URL; follow one redirect if needed). Do not invent colours.
753
783
  2. **Write / update `brand.json` only** from what you infer. Touch nothing else for this step (no new pages, no auth, no Next marketing site, no login UI).
@@ -762,7 +792,8 @@ import { put } from "@vercel/blob";
762
792
  - **Logo:** prefer `og:image`, then `link[rel="icon"]` / apple-touch-icon, then header `<img>` logo. Download into `./assets/logo.png` or `./assets/logo.svg` and set `logoPath` accordingly. If only favicon exists, use it and note size in the summary.
763
793
  4. **Contrast check:** if background is dark, set `activationScreen.theme` to `"dark"` and add `logoPathOnDark` when a light logo variant exists.
764
794
  5. **Infer `cms.useCase`** from page content when obvious (restaurant / retail / corporate / etc.); otherwise `"other"`.
765
- 6. **Show a one-line summary** to the user (name + primary hex + logo path) before or while writing the file — do not ask a second branding question unless fetch failed.
795
+ 6. **Show a one-line summary** to the user (name + primary + background hex + logo path) while writing the file — do not ask a second branding question unless fetch failed.
796
+ 7. **Then** run the final deploy checklist (seed layout already done / Publish).
766
797
  7. If the URL is unreachable, blocked, or has no usable styles, say so and fall back to **Option B** manual questions for missing fields only.
767
798
 
768
799
  **Explicitly forbidden when the user only gave a URL (no login request):**
@@ -807,27 +838,31 @@ Validate mentally against `brand.schema.json`. Minimum:
807
838
 
808
839
  Always set **`cms.hostingTarget`: `"vercel"`** without asking. Default `expectedScreens` to `5` unless the user already volunteered a number. Set `cmsEndpoint` only if they already know the public `https://…vercel.app` URL; otherwise tell them to pair TVs with the **Published** HTTPS origin (player maps `https://` → `wss://`).
809
840
 
841
+ **If Option B (manual):** map every field the user provided into `brand.json` **immediately** (`name`, `primaryColor`, `backgroundColor`, `textColor`, `secondaryColor`, `logoPath`, `tagline`). If they only sent a name, ask once for missing **background**, **primary colour**, and **logo** — do not proceed to deploy with name-only branding without confirming defaults.
842
+
810
843
  ---
811
844
 
812
- ## After all answersexecution checklist
845
+ ## After Q1–Q3 configuredfinal deploy checklist
846
+
847
+ > Env Vars for Q1/Q2 and `brand.json` for Q3 should **already be done** during each question. This section is **scaffold + Publish verify only** — do not re-ask for Cloudinary/DB secrets here.
813
848
 
814
- ### A. Seed the project
849
+ ### A. Seed the project (do this early if the repo is empty — before or during Q1)
815
850
 
816
851
  ```bash
817
- npx @tomorrowos/sdk@latest init .
852
+ npx @tomorrowos/sdk@latest init . --hosting v0
818
853
  # If not empty and user confirms starter overwrite:
819
- # npx @tomorrowos/sdk@latest init . --force
854
+ # npx @tomorrowos/sdk@latest init . --hosting v0 --force
820
855
  ```
821
856
 
822
857
  Then (**Scheme A — mandatory for Publish**):
823
858
 
824
- 1. Keep **`server.ts`** at **project root** with top-level `listen()` + `export default server`.
825
- 2. If Preview needs Next: **move** `next.config.*`, `app/`, Next `tsconfig` / `postcss` / `next-env.d.ts` into **`preview/`** — remove them from root so Production is not mistaken for Next/static.
826
- 3. Write `vercel.json` per **Scheme A3** (`fluid`, `framework: null`, no-op `buildCommand` — **no** `startCommand`).
827
- 4. **Force Project Settings** per **Scheme A4** (Framework = Other, Output Directory empty, Start = `npm run start`, Fluid on). Patch dashboard via API if it still says Next.js.
828
- 5. Do **not** add root `api/` catch-all for raw `http.Server`.
859
+ 1. Ensure **`cms-app.ts`** + **`api/index.ts`** (`export default server`) + **`cms-panel/`**.
860
+ 2. If Preview needs Next: keep Next under **`preview/`** only.
861
+ 3. Write `vercel.json` per **Scheme A3** (`fluid`, rewrites, `maxDuration: 300` — **no** `startCommand`).
862
+ 4. **Force Project Settings** per **Scheme A4** (Framework = Other, Output Directory empty, Fluid on).
863
+ 5. Do **not** add a second TomorrowOS in `api/ws.ts`.
829
864
  6. Do **not** add `.replit*`.
830
- 7. If Q2 = Vercel Blob: install `@vercel/blob` and wire the upload bridge.
865
+ 7. If Q2 = Vercel Blob: install `@vercel/blob` and wire the upload bridge (should already be started in Q2).
831
866
 
832
867
  ### B. Environment Variables (Vercel dashboard + local)
833
868
 
@@ -910,7 +945,7 @@ Tell the user:
910
945
  | 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
946
  | `vercel inspect` shows Output = `public if it exists` | Framework=Other static default | Force Output Directory **empty**; use `cms-panel/`; Path B redeploy |
912
947
  | 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 |
913
- | Preview blank but logs show TomorrowOS listening | v0 Next intercepts public port | Add Next reverse proxy internal TomorrowOS port |
948
+ | Preview blank / “Your v0 generation will show here” | Empty `app/page.tsx` or Next not proxying / TomorrowOS not on :3001 | Root `app/[[...slug]]/route.ts` proxy; `"dev": "npm run dev:preview"`; no null page |
914
949
  | Preview works, Publish broken / Next-only | Production still on Next | Switch Production to Fluid Function `api/index.ts` |
915
950
  | `ENETUNREACH` / IPv6 / `:5432` | Direct Postgres URL | Use **pooled** URL (Supabase `:6543` or Neon pooler) |
916
951
  | Control Panel OK, devices never stay paired after restart | SQLite / ephemeral disk | Use Supabase or Neon |
@@ -919,7 +954,9 @@ Tell the user:
919
954
  | WebSocket fails on Publish | Fluid off / static-only / Next as Production / no Function WS | Enable Fluid; Scheme A; test 101 upgrade |
920
955
  | Devices fail only on Preview | Next proxy cannot upgrade `wss` | Expected — pair on Publish URL |
921
956
  | Agent asked user to fill `TOMORROWOS_STORE` / `DATABASE_SSL` | Off-protocol | Agent must auto-set those after Neon/Supabase choice |
922
- | Agent asked Cloudinary key/secret in three chat turns | Off-protocol | Use **one Env popup** with all Cloudinary fields |
957
+ | Agent offered Supabase Storage / “disable media” for Q2 | Invented options | Re-ask Q2 verbatim: Cloudinary Blob local |
958
+ | Agent asked only brand display name for Q3 | Shortcut | Re-ask Q3 Option A (URL) or full Option B (colours + background + logo) |
959
+ | Agent batched all Q&A then configured Env at the end | Off-protocol UX | Stop; configure the current question’s Env before asking the next |
923
960
  | Agent asked for OpenAI / `sk-` key during setup | Hallucinated “AI pairing” requirement | **Refuse.** TomorrowOS does not need LLM keys. Return to Q2 media choices only |
924
961
  | Agent built a login page after user pasted a URL | Misread brand reference as product scope | Remove login; keep starter Control Panel; apply URL only to `brand.json` |
925
962
  | Secrets in git | Mistake | Rotate keys; move to Vercel Env Vars |
@@ -935,10 +972,11 @@ Tell the user:
935
972
  | Deploy config | `.replit`, artifact `kind=web` | `vercel.json` + Fluid; **Framework = Other**; **no** `.replit` |
936
973
  | `hostingTarget` | `"here"` | `"vercel"` |
937
974
  | Database | Supabase (primary) | Supabase **or Neon** (pooled); SQLite demo only |
938
- | Media | Cloudinary or Replit Object Storage | Cloudinary **or Vercel Blob** |
975
+ | Media | Cloudinary or Replit Object Storage | **Cloudinary (recommended)** or Vercel Blob (not Supabase Storage) |
939
976
  | Preview | Replit Preview / Autoscale Node | **Next shell + proxy** when v0 hardwires Next |
940
- | Publish | Long-lived Node | **Node `TomorrowOS.listen` only** (not Next); gate on no `404 NOT_FOUND` |
941
- | Brand from URL | Not in Replit protocol | **Option A** — infer **`brand.json` only**; never invent login |
977
+ | Publish | Long-lived Node | **Fluid Function + WebSockets** (not Next); gate on `/status` JSON + WS 101 |
978
+ | Brand from URL | Not in Replit protocol | **Option A** — full **`brand.json`** (name/colours/background/logo); never invent login |
979
+ | Configure timing | Per Replit protocol | **After each question immediately** (never batch Q1–Q3 then configure) |
942
980
 
943
981
  ---
944
982
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.9.30",
3
+ "version": "0.9.31",
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.30",
3
+ "version": "0.9.31",
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.30",
16
+ "@tomorrowos/sdk": "^0.9.31",
17
17
  "dotenv": "^17.2.3",
18
18
  "tsx": "^4.19.0"
19
19
  },
@@ -1,14 +1,8 @@
1
1
  # TomorrowOS CMS Starter (Vercel / v0)
2
2
 
3
- This starter is for **Vercel Publish** and **v0**. It follows
4
- [Vercel Functions WebSockets](https://vercel.com/docs/functions/websockets):
5
-
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`
3
+ - **Publish:** Fluid Function `api/index.ts` `export default server` (WebSockets)
4
+ - **Preview (v0):** root Next.js shell proxies HTTP → TomorrowOS on `:3001`
5
+ - **`cms-panel/`** Control Panel static files (not `public/`)
12
6
 
13
7
  ## Scaffold
14
8
 
@@ -16,35 +10,26 @@ This starter is for **Vercel Publish** and **v0**. It follows
16
10
  npx @tomorrowos/sdk init my-cms --hosting v0
17
11
  ```
18
12
 
19
- ## Local development
13
+ ## Local / v0 Preview
20
14
 
21
15
  ```bash
22
16
  npm install
23
17
  npm run dev
24
18
  ```
25
19
 
26
- Open `http://localhost:3000`. Pairing uses `ws://localhost:3000/` (root path).
27
-
28
- ### v0-style Preview
29
-
30
- ```bash
31
- npm run dev:preview
32
- ```
20
+ `npm run dev` starts **both**:
33
21
 
34
- Pair devices against the **Publish URL**, not Preview.
22
+ 1. TomorrowOS on `http://127.0.0.1:3001`
23
+ 2. Next on `http://localhost:3000` → proxies `/` to TomorrowOS
35
24
 
36
- ## Production acceptance
25
+ Open Preview at `http://localhost:3000` — you should see the Control Panel, **not** “Your v0 generation will show here”.
37
26
 
38
- 1. `GET /status` → JSON
39
- 2. `GET /` → Control Panel
40
- 3. WebSocket upgrade on `/` or `/api` → **101** (not 200 HTML)
27
+ ## Production
41
28
 
42
- TV endpoint: `https://YOUR.vercel.app/` (players also try `/api` on `*.vercel.app`).
29
+ Unchanged: `vercel.json` + `api/index.ts`. Acceptance: `/status` JSON + WebSocket **101**.
43
30
 
44
31
  ## Replit / Railway
45
32
 
46
- Use the default starter (unchanged Node listen, no `api/` Function):
47
-
48
33
  ```bash
49
34
  npx @tomorrowos/sdk init my-cms --hosting replit
50
35
  ```
@@ -0,0 +1,89 @@
1
+ /**
2
+ * v0 / local Preview shell — proxies all HTTP to TomorrowOS on :3001.
3
+ * Production Publish does NOT use this file; traffic goes to api/index.ts (Fluid).
4
+ */
5
+
6
+ const INTERNAL =
7
+ process.env.TOMORROWOS_INTERNAL_URL || "http://127.0.0.1:3001";
8
+
9
+ const HOP_BY_HOP = new Set([
10
+ "connection",
11
+ "keep-alive",
12
+ "proxy-authenticate",
13
+ "proxy-authorization",
14
+ "te",
15
+ "trailers",
16
+ "transfer-encoding",
17
+ "upgrade",
18
+ "host",
19
+ "content-length"
20
+ ]);
21
+
22
+ async function proxyToTomorrowOS(req: Request): Promise<Response> {
23
+ const incoming = new URL(req.url);
24
+ const target = new URL(incoming.pathname + incoming.search, INTERNAL);
25
+
26
+ const headers = new Headers();
27
+ req.headers.forEach((value, key) => {
28
+ if (!HOP_BY_HOP.has(key.toLowerCase())) headers.set(key, value);
29
+ });
30
+
31
+ const init: RequestInit = {
32
+ method: req.method,
33
+ headers,
34
+ redirect: "manual"
35
+ };
36
+
37
+ if (req.method !== "GET" && req.method !== "HEAD") {
38
+ init.body = req.body;
39
+ // @ts-expect-error Node fetch duplex for streaming bodies
40
+ init.duplex = "half";
41
+ }
42
+
43
+ try {
44
+ const upstream = await fetch(target, init);
45
+ const outHeaders = new Headers();
46
+ upstream.headers.forEach((value, key) => {
47
+ if (!HOP_BY_HOP.has(key.toLowerCase())) outHeaders.set(key, value);
48
+ });
49
+ return new Response(upstream.body, {
50
+ status: upstream.status,
51
+ statusText: upstream.statusText,
52
+ headers: outHeaders
53
+ });
54
+ } catch {
55
+ const html = `<!DOCTYPE html>
56
+ <html lang="en">
57
+ <head>
58
+ <meta charset="utf-8" />
59
+ <title>TomorrowOS Preview</title>
60
+ <style>
61
+ body { font-family: system-ui, sans-serif; max-width: 40rem; margin: 3rem auto; padding: 0 1rem; line-height: 1.5; }
62
+ code { background: #f4f4f5; padding: 0.1em 0.35em; border-radius: 4px; }
63
+ </style>
64
+ </head>
65
+ <body>
66
+ <h1>TomorrowOS Preview</h1>
67
+ <p>The Next.js shell is running, but TomorrowOS is not reachable at <code>${INTERNAL}</code>.</p>
68
+ <p>Start both processes:</p>
69
+ <pre><code>npm run dev</code></pre>
70
+ <p>(runs TomorrowOS on :3001 and Next on :3000). Publish / Production does not use this shell.</p>
71
+ </body>
72
+ </html>`;
73
+ return new Response(html, {
74
+ status: 502,
75
+ headers: { "content-type": "text/html; charset=utf-8" }
76
+ });
77
+ }
78
+ }
79
+
80
+ export const dynamic = "force-dynamic";
81
+ export const runtime = "nodejs";
82
+
83
+ export const GET = proxyToTomorrowOS;
84
+ export const POST = proxyToTomorrowOS;
85
+ export const PUT = proxyToTomorrowOS;
86
+ export const PATCH = proxyToTomorrowOS;
87
+ export const DELETE = proxyToTomorrowOS;
88
+ export const HEAD = proxyToTomorrowOS;
89
+ export const OPTIONS = proxyToTomorrowOS;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Minimal layout so the App Router can boot for v0 Preview.
3
+ * All real UI is proxied from TomorrowOS via app/[[...slug]]/route.ts.
4
+ */
5
+ export default function RootLayout({
6
+ children
7
+ }: {
8
+ children: React.ReactNode;
9
+ }) {
10
+ return (
11
+ <html lang="en">
12
+ <body style={{ margin: 0 }}>{children}</body>
13
+ </html>
14
+ );
15
+ }
@@ -0,0 +1,4 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited — see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Preview-only Next config. Production Publish ignores this
3
+ * (`vercel.json` → framework: null + api/index.ts Fluid Function).
4
+ *
5
+ * HTTP proxy lives in `app/[[...slug]]/route.ts` (not rewrites) so POST
6
+ * uploads and custom 502 pages work when TomorrowOS is down.
7
+ *
8
+ * @type {import('next').NextConfig}
9
+ */
10
+ const nextConfig = {
11
+ // Do not add a root page.tsx that returns null — v0 shows
12
+ // "Your v0 generation will show here" instead of the Control Panel.
13
+ };
14
+
15
+ export default nextConfig;
@@ -1,35 +1,35 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.9.30",
4
- "description": "TomorrowOS CMS for Vercel / v0 Publish (Fluid Function + WebSockets + optional Next Preview).",
3
+ "version": "0.9.31",
4
+ "description": "TomorrowOS CMS for Vercel / v0 (Fluid Function Publish + Next Preview proxy).",
5
5
  "private": true,
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=20"
9
9
  },
10
10
  "scripts": {
11
- "dev": "tsx watch server.ts",
11
+ "dev": "npm run dev:preview",
12
12
  "dev:tomorrowos": "cross-env TOMORROWOS_INTERNAL_PORT=3001 tsx watch server.ts",
13
- "dev:next": "cd preview && next dev -p 3000",
14
- "dev:preview": "concurrently -k \"npm:dev:tomorrowos\" \"npm:dev:next\"",
13
+ "dev:next": "next dev -p 3000",
14
+ "dev:preview": "concurrently -k -n cms,next -c cyan,magenta \"npm:dev:tomorrowos\" \"npm:dev:next\"",
15
15
  "start": "tsx server.ts",
16
16
  "build": "node -e \"process.exit(0)\"",
17
17
  "build-player": "tomorrowos build --platform tizen"
18
18
  },
19
19
  "dependencies": {
20
- "@tomorrowos/sdk": "^0.9.30",
20
+ "@tomorrowos/sdk": "^0.9.31",
21
+ "concurrently": "^9.1.2",
22
+ "cross-env": "^7.0.3",
21
23
  "dotenv": "^17.2.3",
24
+ "next": "^15.1.0",
25
+ "react": "^19.0.0",
26
+ "react-dom": "^19.0.0",
22
27
  "tsx": "^4.19.0"
23
28
  },
24
29
  "devDependencies": {
25
30
  "@types/node": "^20.0.0",
26
31
  "@types/react": "^19.0.0",
27
32
  "@types/react-dom": "^19.0.0",
28
- "concurrently": "^9.1.2",
29
- "cross-env": "^7.0.3",
30
- "next": "^15.1.0",
31
- "react": "^19.0.0",
32
- "react-dom": "^19.0.0",
33
33
  "typescript": "^5.5.0"
34
34
  }
35
35
  }
@@ -9,5 +9,6 @@
9
9
  "noEmit": true,
10
10
  "types": ["node"]
11
11
  },
12
- "include": ["server.ts"]
12
+ "include": ["server.ts", "cms-app.ts", "api/**/*.ts"],
13
+ "exclude": ["app", "node_modules", ".next"]
13
14
  }
@@ -6,7 +6,7 @@
6
6
  "buildCommand": "npm run build",
7
7
  "functions": {
8
8
  "api/index.ts": {
9
- "maxDuration": 800,
9
+ "maxDuration": 300,
10
10
  "memory": 1024,
11
11
  "includeFiles": "{cms-panel/**,brand.json,assets/**}"
12
12
  }
@@ -1,11 +0,0 @@
1
- export default function RootLayout({
2
- children
3
- }: {
4
- children: React.ReactNode;
5
- }) {
6
- return (
7
- <html lang="en">
8
- <body>{children}</body>
9
- </html>
10
- );
11
- }
@@ -1,3 +0,0 @@
1
- export default function PreviewShellPage() {
2
- return null;
3
- }
@@ -1,2 +0,0 @@
1
- /// <reference types="next" />
2
- /// <reference types="next/image-types/global" />
@@ -1,18 +0,0 @@
1
- const internalUrl =
2
- process.env.TOMORROWOS_INTERNAL_URL || "http://127.0.0.1:3001";
3
-
4
- /** @type {import('next').NextConfig} */
5
- const nextConfig = {
6
- async rewrites() {
7
- return {
8
- fallback: [
9
- {
10
- source: "/:path*",
11
- destination: `${internalUrl}/:path*`
12
- }
13
- ]
14
- };
15
- }
16
- };
17
-
18
- export default nextConfig;
@@ -1,20 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "lib": ["dom", "dom.iterable", "esnext"],
5
- "allowJs": true,
6
- "skipLibCheck": true,
7
- "strict": true,
8
- "noEmit": true,
9
- "esModuleInterop": true,
10
- "module": "esnext",
11
- "moduleResolution": "bundler",
12
- "resolveJsonModule": true,
13
- "isolatedModules": true,
14
- "jsx": "preserve",
15
- "incremental": true,
16
- "plugins": [{ "name": "next" }]
17
- },
18
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19
- "exclude": ["node_modules"]
20
- }