@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 +5 -4
- package/VERCEL_SETUP.md +160 -122
- package/package.json +1 -1
- package/templates/cms-starter/package.json +2 -2
- package/templates/cms-starter-v0/README.md +10 -25
- package/templates/cms-starter-v0/app/[[...slug]]/route.ts +89 -0
- package/templates/cms-starter-v0/app/layout.tsx +15 -0
- package/templates/cms-starter-v0/next-env.d.ts +4 -0
- package/templates/cms-starter-v0/next.config.mjs +15 -0
- package/templates/cms-starter-v0/package.json +11 -11
- package/templates/cms-starter-v0/tsconfig.json +2 -1
- package/templates/cms-starter-v0/vercel.json +1 -1
- package/templates/cms-starter-v0/preview/app/layout.tsx +0 -11
- package/templates/cms-starter-v0/preview/app/page.tsx +0 -3
- package/templates/cms-starter-v0/preview/next-env.d.ts +0 -2
- package/templates/cms-starter-v0/preview/next.config.mjs +0 -18
- package/templates/cms-starter-v0/preview/tsconfig.json +0 -20
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
|
|
81
|
-
**
|
|
82
|
-
|
|
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
|
+
> 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
|
-
|
|
20
|
-
Question
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Do not
|
|
24
|
-
Use
|
|
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 disk — NEVER 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) |
|
|
136
|
-
| 2 | **Question 2** — Media
|
|
137
|
-
| 3 | **Question 3** — Brand
|
|
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
|
-
**
|
|
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. **
|
|
164
|
-
3. **
|
|
165
|
-
4. **
|
|
166
|
-
5. **
|
|
167
|
-
6. **
|
|
168
|
-
7. **
|
|
169
|
-
8. **
|
|
170
|
-
9. **
|
|
171
|
-
10. **
|
|
172
|
-
11. **
|
|
173
|
-
12. **
|
|
174
|
-
13. After
|
|
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 →
|
|
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.
|
|
196
|
-
| Static UI | Proxied from TomorrowOS | Served by **`staticRoot`**
|
|
197
|
-
| WebSocket | Optional / may fail in Preview | **Required** — Fluid Function upgrade
|
|
198
|
-
| Next files |
|
|
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
|
|
208
|
-
api/index.ts ←
|
|
209
|
-
server.ts ← local
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
- ❌
|
|
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`
|
|
231
|
-
- ❌ Use `
|
|
232
|
-
- ❌ Put `startCommand` / `processes` in `vercel.json`
|
|
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": "
|
|
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": "
|
|
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":
|
|
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": "
|
|
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": "
|
|
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
|
|
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
|
-
- **
|
|
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
|
|
480
|
+
### Preview adapter (v0 / Next reverse proxy) — required for Preview UI
|
|
473
481
|
|
|
474
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
495
|
+
**Agent must:**
|
|
487
496
|
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
-
|
|
494
|
-
-
|
|
495
|
-
-
|
|
496
|
-
- Claiming
|
|
497
|
-
-
|
|
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 `
|
|
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
|
|
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 `
|
|
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.
|
|
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** — `
|
|
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** (
|
|
674
|
+
### Step B — If **1 / Cloudinary** (configure **immediately** — one Env popup)
|
|
653
675
|
|
|
654
|
-
> **Critical UX rule:**
|
|
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** (
|
|
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. `
|
|
715
|
-
2. Warn clearly: files may vanish on redeploy; players need stable HTTPS URLs — Cloudinary
|
|
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
|
|
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.
|
|
760
|
+
> Let’s brand your TomorrowOS experience. Choose **one**:
|
|
733
761
|
>
|
|
734
|
-
> **Option A — Website URL
|
|
735
|
-
> Paste **one public website URL**. I will use it **only** to
|
|
736
|
-
> I will **not** rebuild that website
|
|
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
|
-
>
|
|
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
|
|
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)
|
|
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
|
|
845
|
+
## After Q1–Q3 configured — final 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.
|
|
825
|
-
2. If Preview needs Next:
|
|
826
|
-
3. Write `vercel.json` per **Scheme A3** (`fluid`, `
|
|
827
|
-
4. **Force Project Settings** per **Scheme A4** (Framework = Other, Output Directory empty,
|
|
828
|
-
5. Do **not** add
|
|
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
|
|
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
|
|
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 | **
|
|
941
|
-
| Brand from URL | Not in Replit protocol | **Option A** —
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
4
|
-
|
|
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
|
|
13
|
+
## Local / v0 Preview
|
|
20
14
|
|
|
21
15
|
```bash
|
|
22
16
|
npm install
|
|
23
17
|
npm run dev
|
|
24
18
|
```
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
### v0-style Preview
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
npm run dev:preview
|
|
32
|
-
```
|
|
20
|
+
`npm run dev` starts **both**:
|
|
33
21
|
|
|
34
|
-
|
|
22
|
+
1. TomorrowOS on `http://127.0.0.1:3001`
|
|
23
|
+
2. Next on `http://localhost:3000` → proxies `/` to TomorrowOS
|
|
35
24
|
|
|
36
|
-
|
|
25
|
+
Open Preview at `http://localhost:3000` — you should see the Control Panel, **not** “Your v0 generation will show here”.
|
|
37
26
|
|
|
38
|
-
|
|
39
|
-
2. `GET /` → Control Panel
|
|
40
|
-
3. WebSocket upgrade on `/` or `/api` → **101** (not 200 HTML)
|
|
27
|
+
## Production
|
|
41
28
|
|
|
42
|
-
|
|
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,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.
|
|
4
|
-
"description": "TomorrowOS CMS for Vercel / v0
|
|
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": "
|
|
11
|
+
"dev": "npm run dev:preview",
|
|
12
12
|
"dev:tomorrowos": "cross-env TOMORROWOS_INTERNAL_PORT=3001 tsx watch server.ts",
|
|
13
|
-
"dev:next": "
|
|
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.
|
|
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
|
}
|
|
@@ -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
|
-
}
|