@tomorrowos/sdk 0.9.28 → 0.9.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/VERCEL_SETUP.md +135 -115
- package/dist/cli.js +42 -11
- package/dist/tomorrowos.d.ts +13 -1
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +45 -9
- package/package.json +1 -1
- package/templates/cms-starter/package.json +2 -2
- package/templates/cms-starter/server.ts +2 -2
- package/templates/cms-starter-v0/.env.example +19 -0
- package/templates/cms-starter-v0/README.md +50 -0
- package/templates/cms-starter-v0/api/index.ts +10 -0
- package/templates/cms-starter-v0/assets/logo.svg +4 -0
- package/templates/cms-starter-v0/brand.json +23 -0
- package/templates/cms-starter-v0/cms-app.ts +75 -0
- package/templates/cms-starter-v0/cms-panel/assets/player-download/android.svg +1 -0
- package/templates/cms-starter-v0/cms-panel/assets/player-download/brightsign.svg +1 -0
- package/templates/cms-starter-v0/cms-panel/assets/player-download/lg-webos.svg +1 -0
- package/templates/cms-starter-v0/cms-panel/assets/player-download/samsung.svg +1 -0
- package/templates/cms-starter-v0/cms-panel/assets/player-download/toolbox.svg +1 -0
- package/templates/cms-starter-v0/cms-panel/index.html +290 -0
- package/templates/cms-starter-v0/cms-panel/methods.js +2361 -0
- package/templates/cms-starter-v0/cms-panel/panel.css +1127 -0
- package/templates/cms-starter-v0/cms-panel/uploads/.gitkeep +1 -0
- package/templates/cms-starter-v0/package.json +35 -0
- package/templates/cms-starter-v0/policy.example.json +30 -0
- package/templates/cms-starter-v0/preview/app/layout.tsx +11 -0
- package/templates/cms-starter-v0/preview/app/page.tsx +3 -0
- package/templates/cms-starter-v0/preview/next-env.d.ts +2 -0
- package/templates/cms-starter-v0/preview/next.config.mjs +18 -0
- package/templates/cms-starter-v0/preview/tsconfig.json +20 -0
- package/templates/cms-starter-v0/server.ts +6 -0
- package/templates/cms-starter-v0/tsconfig.json +13 -0
- package/templates/cms-starter-v0/vercel.json +17 -0
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ When you see this package referenced in a user prompt, your workflow is:
|
|
|
20
20
|
4. **Otherwise read `LLM_PROMPT.md`** — five required questions before generating code
|
|
21
21
|
5. **Read `BUILD_GUARDRAILS.md`** — mandatory components every generated CMS must include
|
|
22
22
|
6. **Read `brand.schema.json`** — validated shape of `brand.json`
|
|
23
|
-
7. Run **`npx tomorrowos init`** (or
|
|
23
|
+
7. Run **`npx tomorrowos init`** (Replit) or **`npx tomorrowos init --hosting v0`** (Vercel / v0) — minimal CMS server seed
|
|
24
24
|
8. **Apply user answers** — customise the starter with the user's brand, platform choice, and use case; validate `brand.json` against `brand.schema.json`
|
|
25
25
|
9. **Show `PLAYER_INSTALL.md`** — after the CMS is working, show how to build and install the player on a screen
|
|
26
26
|
|
|
@@ -39,7 +39,12 @@ npm install @tomorrowos/sdk
|
|
|
39
39
|
Scaffold a new CMS project:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
+
# Replit / Railway / self-host (default)
|
|
42
43
|
npx tomorrowos init
|
|
44
|
+
|
|
45
|
+
# Vercel / v0 Publish (cms-panel + vercel.json + optional Next Preview)
|
|
46
|
+
npx tomorrowos init --hosting v0
|
|
47
|
+
|
|
43
48
|
cd my-tomorrowos-cms
|
|
44
49
|
npm install
|
|
45
50
|
npm run dev
|
|
@@ -73,7 +78,7 @@ IF any step contradicts Vercel, adapt to settings that Vercel can accept.
|
|
|
73
78
|
|
|
74
79
|
That wizard asks: database (**Supabase** → **Neon** → SQLite demo), media (**Cloudinary** → **Vercel Blob** → local),
|
|
75
80
|
branding (manual fields or **website URL → `brand.json` only**, no login unless asked). Preview may use a thin **Next.js reverse proxy**;
|
|
76
|
-
**Publish stays
|
|
81
|
+
**Publish stays a Fluid Vercel Function** (`api/index.ts` + `export default server`, WebSockets per Vercel docs). Cloudinary uses **one Env popup** (all fields).
|
|
77
82
|
Neon/Supabase: agent auto-sets `TOMORROWOS_STORE` + `DATABASE_SSL`. Sets `cms.hostingTarget` to `"vercel"`.
|
|
78
83
|
|
|
79
84
|
### Replit upgrade (existing CMS)
|
package/VERCEL_SETUP.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
>
|
|
6
6
|
> **On Vercel, this file is the setup questionnaire** (not `REPLIT_SETUP.md`, not `LLM_PROMPT.md`).
|
|
7
7
|
>
|
|
8
|
-
> Protocol id: `vercel-setup/1.
|
|
8
|
+
> Protocol id: `vercel-setup/1.8` — pairs with `@tomorrowos/sdk` **0.9.30+** (`templates/cms-starter-v0`, Fluid Functions WebSockets).
|
|
9
9
|
|
|
10
10
|
## User trigger (copy-paste)
|
|
11
11
|
|
|
@@ -19,8 +19,9 @@ If any step contradicts Vercel, adapt to settings Vercel can accept (document th
|
|
|
19
19
|
After all setup questions are answered (including Cloudinary or Vercel Blob Env Vars if you chose those), configure and deploy — minimal verification only (Control Panel at /). Prefer Supabase Session pooler URL or Neon pooled URL. Do not treat sandbox ENETUNREACH / IPv6 :5432 errors as a hard setup failure when a pooler URL is configured for production.
|
|
20
20
|
Question 3 only updates brand.json from colours/logo/name (a website URL is brand reference only — do not add CMS login/auth unless the user explicitly asks). Do not skip steps.
|
|
21
21
|
Use Next.js only as a v0/Preview shell with a reverse proxy; Production Publish must remain pure Node TomorrowOS.listen (not Next).
|
|
22
|
-
Before claiming Publish works: Framework Preset must be Other/null (not Next.js),
|
|
23
|
-
Do not put startCommand or processes in vercel.json (v0 schema rejects them).
|
|
22
|
+
Before claiming Publish works: Framework Preset must be Other/null (not Next.js), Fluid on, GET /status on the production URL must return JSON (not HTML), GET / must return the Control Panel, and WebSocket upgrade on / or /api must return 101 (not 200 HTML).
|
|
23
|
+
Do not put startCommand or processes in vercel.json (v0 schema rejects them).
|
|
24
|
+
Use cms-starter-v0 layout: cms-app.ts + api/index.ts (export default server) + cms-panel/ + rewrites to /api. Do not create a second TomorrowOS instance in api/ws.ts.
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
Alternate triggers:
|
|
@@ -74,14 +75,14 @@ almost always means **Vercel still thinks this is a Next.js / static deploy**, n
|
|
|
74
75
|
2. **Install Command** = `npm install` / `pnpm install` / `yarn` (normal install).
|
|
75
76
|
3. **Build Command** = a **no-op that exits 0** (e.g. `node -e "process.exit(0)"` or `npm run build` where `build` is that no-op).
|
|
76
77
|
**Never** run `next build` for Production.
|
|
77
|
-
4. **
|
|
78
|
+
4. **Production entry** = Fluid **Vercel Function** `api/index.ts` that **`export default server`** from shared `cms-app.ts` (see [WebSockets docs](https://vercel.com/docs/functions/websockets)). Local `npm start` may still use root `server.ts`.
|
|
78
79
|
5. **Output Directory** = **empty / unset**. Do **not** set `outputDirectory: "."` as a fake static site.
|
|
79
|
-
6. **Do
|
|
80
|
+
6. **Do** use a single `api/index.ts` that exports the TomorrowOS `http.Server`. **Do not** create a second TomorrowOS in `api/ws.ts`, and **do not** use bare `(req, res)` handlers that drop upgrades.
|
|
80
81
|
7. If Preview needs Next, keep Next files **isolated** so Production auto-detect cannot latch onto them:
|
|
81
82
|
- Prefer `preview/` (or similar) for `next.config.*`, `app/`, `pages/` used only by v0 Preview, **or**
|
|
82
|
-
- Ensure Production Framework Preset is forced to **Other** and
|
|
83
|
+
- Ensure Production Framework Preset is forced to **Other** and never runs `next` as the CMS.
|
|
83
84
|
|
|
84
|
-
**Setup is NOT complete** while Production `GET /` returns `404: NOT_FOUND
|
|
85
|
+
**Setup is NOT complete** while Production `GET /` returns `404: NOT_FOUND` or WebSocket upgrade returns 200 HTML.
|
|
85
86
|
|
|
86
87
|
### Publish static trap — `public/` served without Node (read this)
|
|
87
88
|
|
|
@@ -92,11 +93,11 @@ A common **worse** failure mode than `404: NOT_FOUND`:
|
|
|
92
93
|
- The panel shows **CMS unreachable** and console:
|
|
93
94
|
`Unexpected token 'T', "The page c"... is not valid JSON`
|
|
94
95
|
|
|
95
|
-
**Root cause:** Vercel deployed `public/` as a **static site** (Output Directory = `public` or Framework=Other default *“public if it exists”*). **No
|
|
96
|
+
**Root cause:** Vercel deployed `public/` as a **static site** (Output Directory = `public` or Framework=Other default *“public if it exists”*). **No Function** is running TomorrowOS.
|
|
96
97
|
|
|
97
|
-
**Fix (Scheme A
|
|
98
|
+
**Fix (Scheme A):** use `cms-panel/` (not `public/`), Fluid Function `api/index.ts` + rewrites, Output Directory empty. Static UI is served **at runtime** via `staticRoot` in `listen()`.
|
|
98
99
|
|
|
99
|
-
**Publish gate (mandatory):** after deploy, `GET /status` must return **JSON** starting with `{`, not HTML starting with `<` or `The page`.
|
|
100
|
+
**Publish gate (mandatory):** after deploy, `GET /status` must return **JSON** starting with `{`, not HTML starting with `<` or `The page`. **And** WebSocket upgrade on `/` or `/api` must return **101**.
|
|
100
101
|
|
|
101
102
|
```bash
|
|
102
103
|
curl -s https://YOUR-PROJECT.vercel.app/status | head -c 80
|
|
@@ -104,7 +105,7 @@ curl -s https://YOUR-PROJECT.vercel.app/status | head -c 80
|
|
|
104
105
|
# Failure: The page could not be found OR <!DOCTYPE html>
|
|
105
106
|
```
|
|
106
107
|
|
|
107
|
-
**Setup is NOT complete** while `/status` returns HTML
|
|
108
|
+
**Setup is NOT complete** while `/status` returns HTML, the panel shows the JSON parse error, or devices cannot open `wss://`.
|
|
108
109
|
|
|
109
110
|
### Preview (v0 / Vercel Agent sandbox) — Next.js shell allowed
|
|
110
111
|
|
|
@@ -163,70 +164,74 @@ Then ask **Question 1**.
|
|
|
163
164
|
3. **Do not invent** Cloudinary credentials, database URLs, Vercel Blob tokens, brand colours, **or LLM API keys**.
|
|
164
165
|
4. **Postgres env naming:** prefer **`SUPABASE_URL`** when using Supabase; prefer **`DATABASE_URL`** when using Neon. `createTomorrowOSStore` accepts `SUPABASE_URL` then `DATABASE_URL`.
|
|
165
166
|
5. **Always prefer pooled connection strings** (Supabase Session pooler port **6543**; Neon pooled host). Direct `:5432` URLs commonly fail on Vercel/sandbox with **`ENETUNREACH`** (often IPv6).
|
|
166
|
-
6. **Prefer `npx @tomorrowos/sdk init`** (
|
|
167
|
+
6. **Prefer `npx @tomorrowos/sdk init --hosting v0`** (template `cms-starter-v0`: `cms-app.ts`, `api/index.ts`, `cms-panel/`, Fluid + rewrites). For Replit/Railway use default `init` (`cms-starter`). Do not rebuild pairing / WebSocket / playlist APIs.
|
|
167
168
|
7. **Never commit secrets.** Use Vercel Environment Variables (+ optional non-secret flags in committed `.env.example`).
|
|
168
169
|
8. **Skip Replit-only files:** do **not** create `.replit`, `.replit-artifact`, or Replit `deploymentTarget`.
|
|
169
170
|
9. **SQLite is not a production store on Vercel.** Offer it only as option **3** after explicit warning. Keep `sqlitePath` as local-dev fallback in `createTomorrowOSStore`.
|
|
170
|
-
10. **Enable Fluid compute** for Production WebSockets. Confirm Project settings after scaffold.
|
|
171
|
-
11. **Production:**
|
|
171
|
+
10. **Enable Fluid compute** for Production WebSockets ([docs](https://vercel.com/docs/functions/websockets)). Confirm Project settings after scaffold.
|
|
172
|
+
11. **Production:** one Vercel Function exports `http.Server` from `TomorrowOS.listen` (`api/index.ts`). **Do not** make `next start` the Production entrypoint. **Do not** spin a second TomorrowOS in `api/ws.ts`.
|
|
172
173
|
12. **Preview:** if the environment is v0 / Next-hardwired, install the Next reverse-proxy shell (see **Preview adapter**). Do not leave Preview broken.
|
|
173
|
-
13. After Q&A: **configure → install → run/deploy → minimal verify** (Control Panel at `/`). Do not run a long test suite.
|
|
174
|
+
13. After Q&A: **configure → install → run/deploy → minimal verify** (Control Panel at `/` + WS 101). Do not run a long test suite.
|
|
174
175
|
14. **No inventing CMS login.** A branding URL never means “build auth.” Only add login if the user **explicitly** requests it.
|
|
175
176
|
15. **Skip ≠ invent.** If the user skips media choice or a credential, do not replace it with OpenAI/auth/login questions. Stay on Q1–Q3 only.
|
|
176
177
|
16. **Neon / Supabase helpers are agent-owned.** After the user pastes the DB URL, **you** set `TOMORROWOS_STORE` and `DATABASE_SSL` automatically — **do not** ask the user to fill those two.
|
|
177
|
-
17. **Publish gate:** do not mark setup complete while Production returns `404: NOT_FOUND` **or**
|
|
178
|
-
18. **Scheme A is doc-driven:**
|
|
178
|
+
17. **Publish gate:** do not mark setup complete while Production returns `404: NOT_FOUND`, `/status` returns HTML, **or** WebSocket upgrade returns 200 HTML instead of 101.
|
|
179
|
+
18. **Scheme A is doc-driven:** Production = Fluid Function + `export default server` per Vercel WebSockets docs — Replit starter stays unchanged.
|
|
179
180
|
|
|
180
181
|
**Question order:** Q1 → Q2 → Q3 → execution checklist.
|
|
181
182
|
|
|
182
183
|
---
|
|
183
184
|
|
|
184
|
-
## Scheme A —
|
|
185
|
+
## Scheme A — Vercel Functions + WebSockets (mandatory for Production)
|
|
185
186
|
|
|
186
|
-
> **Goal:** Preview may use Next + proxy; **Publish** runs **
|
|
187
|
-
> **Scope:**
|
|
187
|
+
> **Goal:** Preview may use Next + proxy; **Publish** runs TomorrowOS as a **Vercel Function** (`api/index.ts`) on **Fluid**, with WebSocket support per [Vercel WebSockets docs](https://vercel.com/docs/functions/websockets).
|
|
188
|
+
> **Scope:** Layout + `vercel.json` + dashboard. Prefer `npx @tomorrowos/sdk init --hosting v0` (`cms-starter-v0`).
|
|
189
|
+
> **Do not** break Replit/Railway: those keep using default `init` → `cms-starter` (root `server.ts` + `server.listen`, no `api/`).
|
|
188
190
|
|
|
189
191
|
### Why Scheme A exists
|
|
190
192
|
|
|
191
193
|
| Layer | Preview (v0) | Publish (Production) |
|
|
192
194
|
|-------|----------------|----------------------|
|
|
193
|
-
| Host | Next.js shell + HTTP proxy | **
|
|
194
|
-
|
|
|
195
|
-
| WebSocket | Optional / may fail in Preview | Required
|
|
195
|
+
| Host | Next.js shell + HTTP proxy | **Vercel Function** `api/index.ts` → `export default server` |
|
|
196
|
+
| Static UI | Proxied from TomorrowOS | Served by **`staticRoot`** (`cms-panel/`) inside `listen()` — **not** Vercel Output Directory |
|
|
197
|
+
| WebSocket | Optional / may fail in Preview | **Required** — Fluid Function upgrade (`wss://` → `/` rewritten to `/api`, or direct `/api`) |
|
|
196
198
|
| Next files | Allowed under `preview/` | **Must not** live at project root |
|
|
197
199
|
|
|
200
|
+
**Root cause of “Control Panel works, TV cannot connect”:** HTTP can be served without WebSocket upgrades. Classic root-only `server.ts` / plain serverless handlers often return **200 HTML** on `Upgrade` instead of **101**. Official pattern is a Function that exports `http.Server` with `ws` attached ([docs](https://vercel.com/docs/functions/websockets)).
|
|
201
|
+
|
|
198
202
|
### A1 — Project layout (Agent must enforce)
|
|
199
203
|
|
|
200
|
-
**
|
|
204
|
+
**Production (Vercel / v0 starter):**
|
|
201
205
|
|
|
202
206
|
```
|
|
203
|
-
|
|
204
|
-
|
|
207
|
+
cms-app.ts ← shared TomorrowOS.listen + export const server
|
|
208
|
+
api/index.ts ← export { server as default } from "../cms-app.js"
|
|
209
|
+
server.ts ← local/Preview: same default export (npm start / internal port)
|
|
210
|
+
vercel.json ← fluid + rewrites + functions.maxDuration
|
|
205
211
|
package.json
|
|
206
212
|
brand.json
|
|
207
|
-
|
|
213
|
+
cms-panel/ ← CMS static UI (NOT named public/ — avoids static-output trap)
|
|
214
|
+
preview/ ← Next proxy for v0 Preview only
|
|
208
215
|
```
|
|
209
216
|
|
|
210
|
-
**Preview only
|
|
217
|
+
**Preview only — keep ALL Next under `preview/`:**
|
|
211
218
|
|
|
212
219
|
```
|
|
213
220
|
preview/
|
|
214
221
|
next.config.mjs
|
|
215
|
-
app/
|
|
216
|
-
|
|
217
|
-
next-env.d.ts
|
|
218
|
-
tsconfig.json ← Next/tsconfig if separate from server
|
|
222
|
+
app/
|
|
223
|
+
…
|
|
219
224
|
```
|
|
220
225
|
|
|
221
|
-
**Remove from project root after move:** `next.config.mjs`, `app/`, root Next `tsconfig` plugin config, `postcss.config.mjs`, `next-env.d.ts`.
|
|
222
|
-
|
|
223
226
|
**Do NOT:**
|
|
224
227
|
|
|
225
|
-
- ❌ Leave `next.config.mjs` + `app/` at root
|
|
226
|
-
- ❌
|
|
227
|
-
- ❌
|
|
228
|
+
- ❌ Leave `next.config.mjs` + `app/` at project root
|
|
229
|
+
- ❌ Set Output Directory to `public`, `cms-panel`, or `.`
|
|
230
|
+
- ❌ Create a **second** TomorrowOS instance in `api/ws.ts` (separate isolate = broken pairing memory)
|
|
231
|
+
- ❌ Use `(req, res) =>` serverless handlers that drop `upgrade`
|
|
232
|
+
- ❌ Put `startCommand` / `processes` in `vercel.json` (v0 schema rejects them)
|
|
228
233
|
|
|
229
|
-
|
|
234
|
+
**`package.json` scripts:**
|
|
230
235
|
|
|
231
236
|
```json
|
|
232
237
|
{
|
|
@@ -241,105 +246,117 @@ preview/
|
|
|
241
246
|
}
|
|
242
247
|
```
|
|
243
248
|
|
|
244
|
-
|
|
249
|
+
- **Local / Railway-style:** `npm start` → `tsx server.ts` → SDK calls `server.listen` (no `VERCEL` env).
|
|
250
|
+
- **Vercel Production:** Function loads `api/index.ts`; SDK skips `listen` when `process.env.VERCEL` is set and Vercel owns the socket.
|
|
245
251
|
|
|
246
|
-
|
|
252
|
+
### A2 — Function entry (matches Vercel WebSockets docs)
|
|
247
253
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
Use the pattern in **Runtime & Vercel deploy rules** below. On Publish, **do not** set `TOMORROWOS_INTERNAL_PORT` — let Vercel inject `PORT`.
|
|
254
|
+
```ts
|
|
255
|
+
// api/index.ts
|
|
256
|
+
export { server as default } from "../cms-app.js";
|
|
257
|
+
```
|
|
253
258
|
|
|
254
|
-
|
|
259
|
+
```ts
|
|
260
|
+
// cms-app.ts (shared)
|
|
261
|
+
const server = tomorrowos.listen({
|
|
262
|
+
port: Number(process.env.PORT) || 3000,
|
|
263
|
+
host: "0.0.0.0",
|
|
264
|
+
staticRoot: join(__dirname, "cms-panel"),
|
|
265
|
+
});
|
|
266
|
+
export { server, tomorrowos };
|
|
267
|
+
```
|
|
255
268
|
|
|
256
|
-
|
|
269
|
+
SDK behaviour (0.9.30+):
|
|
257
270
|
|
|
258
|
-
-
|
|
259
|
-
-
|
|
260
|
-
-
|
|
261
|
-
- ❌ `outputDirectory: "public"` or `"."`
|
|
271
|
+
- Accepts WebSocket upgrades on `/`, `/api`, `/api/ws` (players may use either)
|
|
272
|
+
- When `VERCEL` is set: **`autoListen` defaults to false** — do not bind a port yourself
|
|
273
|
+
- Pattern aligns with docs: create `http.Server`, attach `ws`, **`export default server`**
|
|
262
274
|
|
|
263
|
-
|
|
275
|
+
### A3 — `vercel.json` (Production)
|
|
264
276
|
|
|
265
277
|
```json
|
|
266
278
|
{
|
|
267
279
|
"fluid": true,
|
|
268
280
|
"framework": null,
|
|
269
281
|
"installCommand": "npm install",
|
|
270
|
-
"buildCommand": "npm run build"
|
|
282
|
+
"buildCommand": "npm run build",
|
|
283
|
+
"functions": {
|
|
284
|
+
"api/index.ts": {
|
|
285
|
+
"maxDuration": 800,
|
|
286
|
+
"memory": 1024,
|
|
287
|
+
"includeFiles": "{cms-panel/**,brand.json,assets/**}"
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
"rewrites": [
|
|
291
|
+
{ "source": "/((?!api/).*)", "destination": "/api" }
|
|
292
|
+
]
|
|
271
293
|
}
|
|
272
294
|
```
|
|
273
295
|
|
|
274
|
-
|
|
296
|
+
**Why rewrites:** Public URLs stay `https://app.vercel.app/` and `/status` while the Function mount is `/api`. Device `wss://app.vercel.app/` is rewritten to the same Function so one isolate handles HTTP + WS.
|
|
275
297
|
|
|
276
|
-
**
|
|
298
|
+
**v0 schema:** never add `startCommand` or `processes`.
|
|
277
299
|
|
|
278
|
-
|
|
279
|
-
- **Do not** set `outputDirectory` to `public` or `.`
|
|
280
|
-
- `"buildCommand"` must run the no-op `npm run build` — **never** `next build` for Production
|
|
281
|
-
- Set **Output Directory empty** in the **Vercel dashboard** (Scheme A4) — that is where static-vs-Node is decided, not via invalid `vercel.json` keys
|
|
300
|
+
**Critical:**
|
|
282
301
|
|
|
283
|
-
|
|
302
|
+
- `"fluid": true` is required for WebSockets
|
|
303
|
+
- **Do not** set `outputDirectory` to `cms-panel` / `public`
|
|
304
|
+
- `"buildCommand"` = no-op — **never** `next build` for Production
|
|
305
|
+
- Dashboard Output Directory must stay **empty**
|
|
284
306
|
|
|
285
|
-
|
|
307
|
+
### A4 — Vercel Project Settings (dashboard — mandatory)
|
|
286
308
|
|
|
287
309
|
| Setting | Required value |
|
|
288
310
|
|---------|----------------|
|
|
289
311
|
| Framework Preset | **Other** / null — **not Next.js** |
|
|
290
|
-
| Install Command | `npm install`
|
|
291
|
-
| Build Command | `npm run build` (no-op
|
|
292
|
-
| **Output Directory** | **empty
|
|
293
|
-
| **Start Command** | `npm run start` → `tsx server.ts` |
|
|
312
|
+
| Install Command | `npm install` |
|
|
313
|
+
| Build Command | `npm run build` (no-op) |
|
|
314
|
+
| **Output Directory** | **empty** |
|
|
294
315
|
| Fluid compute | **On** |
|
|
316
|
+
| WebSockets | Account must allow Functions WebSockets (see docs “Permissions Required”) |
|
|
295
317
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
### A5 — Publish acceptance (all must pass before “setup complete”)
|
|
299
|
-
|
|
300
|
-
1. Deploy logs show Node starting / `[TomorrowOS] listening on ...` (not only “Uploading static files”)
|
|
301
|
-
2. `curl -s https://PROD/status` → JSON (`{"status":...}`)
|
|
302
|
-
3. `curl -s https://PROD/` → Control Panel HTML
|
|
303
|
-
4. Control Panel **does not** show `Unexpected token 'T'` / CMS unreachable
|
|
304
|
-
5. Framework Preset = Other; Fluid on
|
|
318
|
+
### A5 — Publish acceptance (all must pass)
|
|
305
319
|
|
|
306
|
-
|
|
320
|
+
1. `curl -s https://PROD/status` → JSON (`{"status":...}`)
|
|
321
|
+
2. `curl -s https://PROD/` → Control Panel HTML
|
|
322
|
+
3. **WebSocket upgrade returns 101**, not 200 HTML:
|
|
307
323
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
-
|
|
311
|
-
-
|
|
312
|
-
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
- ❌ No `@vercel/node` handler that calls non-existent `tomorrowos.getStatus()`
|
|
324
|
+
```bash
|
|
325
|
+
curl.exe -i --http1.1 ^
|
|
326
|
+
-H "Connection: Upgrade" -H "Upgrade: websocket" ^
|
|
327
|
+
-H "Sec-WebSocket-Version: 13" ^
|
|
328
|
+
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" ^
|
|
329
|
+
"https://PROD/"
|
|
330
|
+
# Also try: https://PROD/api
|
|
331
|
+
```
|
|
317
332
|
|
|
318
|
-
|
|
333
|
+
4. Control Panel does **not** show CMS unreachable / `Unexpected token 'T'`
|
|
334
|
+
5. Fluid on; Framework = Other
|
|
319
335
|
|
|
320
|
-
|
|
336
|
+
If (1)+(2) pass but (3) fails → Function is serving HTTP only. Re-check Fluid, `api/index.ts` default export, rewrites, and WebSockets permission. **Do not** invent a second `api/ws.ts` TomorrowOS instance.
|
|
321
337
|
|
|
322
|
-
|
|
338
|
+
### A6 — What Scheme A does **not** require
|
|
323
339
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
340
|
+
- ❌ No Next.js as Production CMS
|
|
341
|
+
- ❌ No replacing SDK transport with hand-rolled pairing
|
|
342
|
+
- ❌ No SQLite as production store on Vercel
|
|
343
|
+
- ❌ No changes to Replit `cms-starter` layout for Vercel-only fixes
|
|
328
344
|
|
|
329
|
-
|
|
345
|
+
### A7 — If v0 Publish still fails (Path B / C)
|
|
330
346
|
|
|
331
|
-
|
|
347
|
+
**Path B:** Deploy with `vercel --prod` or Git integration (full Vercel), same Scheme A layout.
|
|
332
348
|
|
|
333
|
-
|
|
349
|
+
**Path C:** Host CMS on **Railway / Fly.io / Replit** (long-lived Node). Use Vercel only for Preview if needed.
|
|
334
350
|
|
|
335
|
-
|
|
351
|
+
### A8 — Migrate an existing broken Vercel project
|
|
336
352
|
|
|
337
|
-
1.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
353
|
+
1. Scaffold/compare with `cms-starter-v0` (`init --hosting v0`) or copy:
|
|
354
|
+
- `cms-app.ts`, `api/index.ts`, `server.ts`, `vercel.json`
|
|
355
|
+
- rename `public/` → `cms-panel/` if still present
|
|
356
|
+
2. Ensure **one** Function exports the **same** `server` from `cms-app.ts`
|
|
357
|
+
3. Fluid on → Redeploy
|
|
358
|
+
4. Verify A5 (especially WebSocket 101)
|
|
359
|
+
5. On TVs: enter `https://YOUR.vercel.app/` (players also try `/api` automatically on vercel.app hosts)
|
|
343
360
|
|
|
344
361
|
---
|
|
345
362
|
|
|
@@ -347,11 +364,12 @@ If an Agent moved `server.ts` → `api/index.ts` / `api/[...].ts` and only `/api
|
|
|
347
364
|
|
|
348
365
|
### Production runtime (Publish)
|
|
349
366
|
|
|
350
|
-
- Serve
|
|
351
|
-
- Keep
|
|
367
|
+
- Serve TomorrowOS via **Vercel Function** `api/index.ts` → `export default server` (Fluid + WebSockets).
|
|
368
|
+
- Keep root **`server.ts`** for **local** `npm start` / Preview internal port only (same `cms-app.ts`).
|
|
369
|
+
- Keep **`@tomorrowos/sdk`** (0.9.30+) and put **`tsx` in `dependencies`** for local start.
|
|
352
370
|
- Node **20+**.
|
|
353
371
|
- **Do not** replace the SDK with a hand-rolled server that drops WebSocket upgrades.
|
|
354
|
-
- **Forbidden as the Production
|
|
372
|
+
- **Forbidden as the Production CMS:** `next start`, static export only, or a bare `(req,res)` handler without `http.Server` + `ws`.
|
|
355
373
|
|
|
356
374
|
### Expected `package.json` scripts
|
|
357
375
|
|
|
@@ -361,7 +379,7 @@ If an Agent moved `server.ts` → `api/index.ts` / `api/[...].ts` and only `/api
|
|
|
361
379
|
"dev": "tsx watch server.ts",
|
|
362
380
|
"dev:preview": "concurrently -k \"npm:dev:tomorrowos\" \"npm:dev:next\"",
|
|
363
381
|
"dev:tomorrowos": "cross-env PORT=3001 TOMORROWOS_INTERNAL_PORT=3001 tsx watch server.ts",
|
|
364
|
-
"dev:next": "next dev -p 3000",
|
|
382
|
+
"dev:next": "cd preview && next dev -p 3000",
|
|
365
383
|
"start": "tsx server.ts",
|
|
366
384
|
"build": "node -e \"process.exit(0)\""
|
|
367
385
|
}
|
|
@@ -370,7 +388,8 @@ If an Agent moved `server.ts` → `api/index.ts` / `api/[...].ts` and only `/api
|
|
|
370
388
|
|
|
371
389
|
Adapt script names / process runners as needed. **Invariant:**
|
|
372
390
|
|
|
373
|
-
-
|
|
391
|
+
- **Local `npm start`** = TomorrowOS Node with `server.listen`.
|
|
392
|
+
- **Vercel Production** = Function `api/index.ts` (SDK `autoListen=false` when `VERCEL` is set).
|
|
374
393
|
- **`build`** = no-op exit 0 (never `next build` for Production).
|
|
375
394
|
- **Preview / v0** = Next on public port + TomorrowOS on internal port + proxy.
|
|
376
395
|
|
|
@@ -883,20 +902,21 @@ Tell the user:
|
|
|
883
902
|
|
|
884
903
|
| Symptom | Likely cause | Fix |
|
|
885
904
|
|---------|--------------|-----|
|
|
886
|
-
| Publish `404: NOT_FOUND` | Framework Preset still **Next.js** / no
|
|
887
|
-
| Panel loads but **CMS unreachable** / `Unexpected token 'T', "The page c"...` | **`public/` deployed as static site** — no
|
|
888
|
-
| Only `/api/status` returns JSON, `/status` is 404 |
|
|
889
|
-
|
|
|
890
|
-
|
|
|
891
|
-
|
|
|
905
|
+
| Publish `404: NOT_FOUND` | Framework Preset still **Next.js** / no Function | Scheme A: Framework = **Other**, Fluid on, `api/index.ts` + rewrites; redeploy |
|
|
906
|
+
| Panel loads but **CMS unreachable** / `Unexpected token 'T', "The page c"...` | **`public/` deployed as static site** — no Function | Rename to `cms-panel/`; Output empty; Scheme A Function entry; **Path B** if needed |
|
|
907
|
+
| Only `/api/status` returns JSON, `/status` is 404 | Missing **rewrites** to `/api` | Add Scheme A3 rewrite `/(.*) → /api` (exclude existing `/api/`); redeploy |
|
|
908
|
+
| Control Panel OK, TV **could not connect** | WebSocket upgrade not reaching Function (200 HTML) | Fluid on; `export default server`; verify 101 on `/` and `/api`; check WebSockets permission |
|
|
909
|
+
| `vercel.json` error: invalid `startCommand` | v0 schema rejects it | Remove from `vercel.json`; Function entry does not need startCommand |
|
|
910
|
+
| Separate `api/ws.ts` with a second `new TomorrowOS` | Two isolates — pairing memory split | **One** Function only (`api/index.ts` → shared `cms-app.ts`) |
|
|
911
|
+
| `vercel inspect` shows Output = `public if it exists` | Framework=Other static default | Force Output Directory **empty**; use `cms-panel/`; Path B redeploy |
|
|
892
912
|
| Build runs `next build` / missing routes-manifest | Next auto-detected from root `next.config` / `app/` | Move Next to `preview/`; Framework null/Other; `build` = no-op |
|
|
893
913
|
| Preview blank but logs show TomorrowOS listening | v0 Next intercepts public port | Add Next reverse proxy → internal TomorrowOS port |
|
|
894
|
-
| Preview works, Publish broken / Next-only | Production still on Next | Switch Production
|
|
914
|
+
| Preview works, Publish broken / Next-only | Production still on Next | Switch Production to Fluid Function `api/index.ts` |
|
|
895
915
|
| `ENETUNREACH` / IPv6 / `:5432` | Direct Postgres URL | Use **pooled** URL (Supabase `:6543` or Neon pooler) |
|
|
896
916
|
| Control Panel OK, devices never stay paired after restart | SQLite / ephemeral disk | Use Supabase or Neon |
|
|
897
|
-
| Uploads break / broken thumbs in prod | Local
|
|
917
|
+
| Uploads break / broken thumbs in prod | Local uploads only | Cloudinary or Vercel Blob |
|
|
898
918
|
| Blob uploads 401 / missing token | Blob not linked | Vercel Storage → Blob + `BLOB_READ_WRITE_TOKEN` |
|
|
899
|
-
| WebSocket fails on Publish | Fluid off / static-only / Next as Production | Enable Fluid;
|
|
919
|
+
| WebSocket fails on Publish | Fluid off / static-only / Next as Production / no Function WS | Enable Fluid; Scheme A; test 101 upgrade |
|
|
900
920
|
| Devices fail only on Preview | Next proxy cannot upgrade `wss` | Expected — pair on Publish URL |
|
|
901
921
|
| Agent asked user to fill `TOMORROWOS_STORE` / `DATABASE_SSL` | Off-protocol | Agent must auto-set those after Neon/Supabase choice |
|
|
902
922
|
| Agent asked Cloudinary key/secret in three chat turns | Off-protocol | Use **one Env popup** with all Cloudinary fields |
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,8 @@ function getSdkVersion() {
|
|
|
14
14
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
15
15
|
return pkg.version ?? "0.0.0";
|
|
16
16
|
}
|
|
17
|
-
const STARTER_SKIP_DIRS = new Set(["node_modules"]);
|
|
17
|
+
const STARTER_SKIP_DIRS = new Set(["node_modules", ".replit-artifact"]);
|
|
18
|
+
const STARTER_SKIP_FILES = new Set(["package-lock.json", ".env"]);
|
|
18
19
|
/** Never ship local install artifacts into a new CMS project. */
|
|
19
20
|
function shouldCopyStarterEntry(starterRoot, source) {
|
|
20
21
|
const rel = path.relative(starterRoot, source);
|
|
@@ -23,7 +24,7 @@ function shouldCopyStarterEntry(starterRoot, source) {
|
|
|
23
24
|
const parts = rel.split(path.sep).filter(Boolean);
|
|
24
25
|
if (parts.some((p) => STARTER_SKIP_DIRS.has(p)))
|
|
25
26
|
return false;
|
|
26
|
-
if (path.basename(source)
|
|
27
|
+
if (STARTER_SKIP_FILES.has(path.basename(source)))
|
|
27
28
|
return false;
|
|
28
29
|
return true;
|
|
29
30
|
}
|
|
@@ -54,10 +55,11 @@ function patchStarterPackageJson(destDir) {
|
|
|
54
55
|
}
|
|
55
56
|
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
56
57
|
}
|
|
57
|
-
function copyStarter(destDir, force) {
|
|
58
|
-
const
|
|
58
|
+
function copyStarter(destDir, force, hosting) {
|
|
59
|
+
const templateName = hosting === "replit" ? "cms-starter" : "cms-starter-v0";
|
|
60
|
+
const src = path.join(packageRoot(), "templates", templateName);
|
|
59
61
|
if (!fs.existsSync(src)) {
|
|
60
|
-
console.error(
|
|
62
|
+
console.error(`[tomorrowos] Starter template "${templateName}" not found. Re-install @tomorrowos/sdk (templates should ship with the package).`);
|
|
61
63
|
process.exit(1);
|
|
62
64
|
}
|
|
63
65
|
const resolved = path.resolve(destDir);
|
|
@@ -80,9 +82,23 @@ function copyStarter(destDir, force) {
|
|
|
80
82
|
const dbPath = initializeStarterSQLite(resolved);
|
|
81
83
|
const sdkVer = getSdkVersion();
|
|
82
84
|
console.log(`[tomorrowos] Created CMS project at ${resolved}`);
|
|
85
|
+
console.log(`[tomorrowos] Template: ${templateName} (--hosting ${hosting})`);
|
|
83
86
|
console.log(`[tomorrowos] @tomorrowos/sdk dependency: ^${sdkVer}`);
|
|
84
87
|
console.log(`[tomorrowos] Initialized SQLite database: ${dbPath}`);
|
|
85
|
-
|
|
88
|
+
if (hosting === "replit") {
|
|
89
|
+
console.log("Next: npm install, then npm start (see REPLIT_SETUP.md on Replit)");
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log("Next: npm install, then npm start (see VERCEL_SETUP.md for v0 / Vercel Publish)");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function parseHostingFlag(argv) {
|
|
96
|
+
const raw = optionValue(argv, "--hosting")?.trim().toLowerCase();
|
|
97
|
+
if (!raw || raw === "replit" || raw === "here")
|
|
98
|
+
return "replit";
|
|
99
|
+
if (raw === "v0" || raw === "vercel")
|
|
100
|
+
return raw === "v0" ? "v0" : "vercel";
|
|
101
|
+
throw new Error('--hosting must be "replit", "v0", or "vercel".');
|
|
86
102
|
}
|
|
87
103
|
function cmdBuild(argv) {
|
|
88
104
|
const platformIdx = argv.indexOf("--platform");
|
|
@@ -176,16 +192,18 @@ function printHelp() {
|
|
|
176
192
|
console.log(`tomorrowos — TomorrowOS SDK CLI
|
|
177
193
|
|
|
178
194
|
Usage:
|
|
179
|
-
tomorrowos init [directory] Copy cms-starter template (default:
|
|
195
|
+
tomorrowos init [directory] Copy cms-starter template (default: Replit / Railway)
|
|
180
196
|
tomorrowos migrate [options] Migrate TomorrowOS data between supported databases
|
|
181
197
|
tomorrowos build --platform … Placeholder; player packaging lives in the player repo
|
|
182
198
|
|
|
183
199
|
Options:
|
|
200
|
+
--hosting replit|v0|vercel init template: replit (default) or Vercel/v0 (cms-starter-v0)
|
|
184
201
|
--force With init: copy into a non-empty directory
|
|
185
202
|
|
|
186
203
|
Examples:
|
|
187
204
|
npx @tomorrowos/sdk init
|
|
188
|
-
npx @tomorrowos/sdk init ./my-cms
|
|
205
|
+
npx @tomorrowos/sdk init ./my-cms --hosting replit
|
|
206
|
+
npx @tomorrowos/sdk init ./my-cms --hosting v0
|
|
189
207
|
npx @tomorrowos/sdk migrate --from sqlite --from-sqlite ./data/tomorrowos.db --to supabase --to-database-url "$DATABASE_URL"
|
|
190
208
|
`);
|
|
191
209
|
}
|
|
@@ -218,9 +236,22 @@ async function main() {
|
|
|
218
236
|
const rest = argv.slice(1);
|
|
219
237
|
if (cmd === "init") {
|
|
220
238
|
const force = rest.includes("--force");
|
|
221
|
-
const pos = rest.filter((a) => !a.startsWith("-"));
|
|
222
|
-
|
|
223
|
-
|
|
239
|
+
const pos = rest.filter((a) => !a.startsWith("-") && a !== "--force");
|
|
240
|
+
let hosting = "replit";
|
|
241
|
+
try {
|
|
242
|
+
hosting = parseHostingFlag(rest);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
console.error("[tomorrowos]", err instanceof Error ? err.message : err);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
const filteredPos = pos.filter((a) => {
|
|
249
|
+
const idx = rest.indexOf(a);
|
|
250
|
+
const prev = rest[idx - 1];
|
|
251
|
+
return prev !== "--hosting";
|
|
252
|
+
});
|
|
253
|
+
const target = filteredPos[0] ?? "my-tomorrowos-cms";
|
|
254
|
+
copyStarter(target, force, hosting);
|
|
224
255
|
return;
|
|
225
256
|
}
|
|
226
257
|
if (cmd === "build") {
|
package/dist/tomorrowos.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface ListenOptions {
|
|
|
19
19
|
/**
|
|
20
20
|
* If set, GET requests serve files from this directory (e.g. CMS UI assets).
|
|
21
21
|
* `GET /` serves `staticIndex` (default `index.html`) under this root.
|
|
22
|
-
* WebSocket upgrades on `/`
|
|
22
|
+
* WebSocket upgrades are accepted on `/` and common Vercel Function paths (`/api`, `/api/ws`).
|
|
23
23
|
* Note: `GET /brand.json` is always served from the `brand` passed to the constructor (not from this folder).
|
|
24
24
|
*/
|
|
25
25
|
staticRoot?: string;
|
|
@@ -28,6 +28,18 @@ export interface ListenOptions {
|
|
|
28
28
|
* Default: `index.html`. Ignored when `staticRoot` is not set.
|
|
29
29
|
*/
|
|
30
30
|
staticIndex?: string;
|
|
31
|
+
/**
|
|
32
|
+
* When true (default), call `server.listen(port)`.
|
|
33
|
+
* On Vercel Functions set false (or rely on auto-detect via `process.env.VERCEL`)
|
|
34
|
+
* and `export default server` — Vercel owns the socket; see
|
|
35
|
+
* https://vercel.com/docs/functions/websockets
|
|
36
|
+
*/
|
|
37
|
+
autoListen?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Pathnames that accept WebSocket upgrades (default: `/`, `/api`, `/api/ws`, …).
|
|
40
|
+
* Replit / Railway / local keep using `wss://host/`. Vercel Functions often mount at `/api`.
|
|
41
|
+
*/
|
|
42
|
+
webSocketPaths?: string[];
|
|
31
43
|
}
|
|
32
44
|
export interface DeviceListItem {
|
|
33
45
|
deviceId: string;
|