@tomorrowos/sdk 0.9.29 → 0.9.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/VERCEL_SETUP.md +135 -115
- 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-v0/README.md +17 -27
- package/templates/cms-starter-v0/api/index.ts +10 -0
- package/templates/cms-starter-v0/cms-app.ts +75 -0
- package/templates/cms-starter-v0/package.json +3 -3
- package/templates/cms-starter-v0/server.ts +3 -72
- package/templates/cms-starter-v0/vercel.json +11 -1
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ IF any step contradicts Vercel, adapt to settings that Vercel can accept.
|
|
|
78
78
|
|
|
79
79
|
That wizard asks: database (**Supabase** → **Neon** → SQLite demo), media (**Cloudinary** → **Vercel Blob** → local),
|
|
80
80
|
branding (manual fields or **website URL → `brand.json` only**, no login unless asked). Preview may use a thin **Next.js reverse proxy**;
|
|
81
|
-
**Publish stays
|
|
81
|
+
**Publish stays a Fluid Vercel Function** (`api/index.ts` + `export default server`, WebSockets per Vercel docs). Cloudinary uses **one Env popup** (all fields).
|
|
82
82
|
Neon/Supabase: agent auto-sets `TOMORROWOS_STORE` + `DATABASE_SSL`. Sets `cms.hostingTarget` to `"vercel"`.
|
|
83
83
|
|
|
84
84
|
### Replit upgrade (existing CMS)
|
package/VERCEL_SETUP.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
>
|
|
6
6
|
> **On Vercel, this file is the setup questionnaire** (not `REPLIT_SETUP.md`, not `LLM_PROMPT.md`).
|
|
7
7
|
>
|
|
8
|
-
> Protocol id: `vercel-setup/1.
|
|
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 --hosting v0`** (template `cms-starter-v0`:
|
|
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/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;
|
package/dist/tomorrowos.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,IAAI,MAAM,MAAM,CAAC;AAYxB,OAAO,EAAE,eAAe,EAAE,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,KAAK,EACV,gBAAgB,EAKhB,eAAe,EAEhB,MAAM,kBAAkB,CAAC;AAe1B,YAAY,EACV,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,eAAe,CAAC;IACvB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
|
package/dist/tomorrowos.js
CHANGED
|
@@ -140,6 +140,31 @@ function resolveHelloDeviceId(msg) {
|
|
|
140
140
|
: null;
|
|
141
141
|
return deviceId;
|
|
142
142
|
}
|
|
143
|
+
/** Default paths that accept device WebSocket upgrades (local + Vercel Functions). */
|
|
144
|
+
const DEFAULT_WEBSOCKET_PATHS = ["/", "/api", "/api/", "/api/ws", "/api/index"];
|
|
145
|
+
function normalizeWebSocketPaths(paths) {
|
|
146
|
+
const list = paths && paths.length > 0 ? paths : DEFAULT_WEBSOCKET_PATHS;
|
|
147
|
+
const out = new Set();
|
|
148
|
+
for (const raw of list) {
|
|
149
|
+
let p = String(raw || "").trim() || "/";
|
|
150
|
+
if (!p.startsWith("/"))
|
|
151
|
+
p = `/${p}`;
|
|
152
|
+
out.add(p);
|
|
153
|
+
if (p.length > 1 && p.endsWith("/"))
|
|
154
|
+
out.add(p.replace(/\/+$/, ""));
|
|
155
|
+
else if (p !== "/")
|
|
156
|
+
out.add(`${p}/`);
|
|
157
|
+
}
|
|
158
|
+
out.add("/");
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
function isAllowedWebSocketPath(pathname, allowed) {
|
|
162
|
+
const p = !pathname || pathname === "" ? "/" : pathname;
|
|
163
|
+
if (allowed.has(p))
|
|
164
|
+
return true;
|
|
165
|
+
const trimmed = p.replace(/\/+$/, "") || "/";
|
|
166
|
+
return allowed.has(trimmed) || allowed.has(`${trimmed}/`);
|
|
167
|
+
}
|
|
143
168
|
const MAX_MEDIA_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
144
169
|
async function readJsonBody(req) {
|
|
145
170
|
const buf = await readRawBody(req, MAX_MEDIA_UPLOAD_BYTES);
|
|
@@ -773,6 +798,10 @@ export class TomorrowOS extends EventEmitter {
|
|
|
773
798
|
const server = http.createServer((req, res) => {
|
|
774
799
|
void this.handleHttp(req, res);
|
|
775
800
|
});
|
|
801
|
+
// Match Vercel Functions WebSocket pattern: attach ws to the http.Server
|
|
802
|
+
// (https://vercel.com/docs/functions/websockets). Path filter keeps non-CMS
|
|
803
|
+
// upgrades from being accepted when the same process hosts other routes.
|
|
804
|
+
const allowedWsPaths = normalizeWebSocketPaths(options.webSocketPaths);
|
|
776
805
|
const wss = new WebSocketServer({ noServer: true });
|
|
777
806
|
server.on("upgrade", (request, socket, head) => {
|
|
778
807
|
if (!request.url) {
|
|
@@ -780,23 +809,30 @@ export class TomorrowOS extends EventEmitter {
|
|
|
780
809
|
return;
|
|
781
810
|
}
|
|
782
811
|
const { pathname } = new URL(request.url, `http://${request.headers.host}`);
|
|
783
|
-
if (pathname
|
|
784
|
-
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
785
|
-
wss.emit("connection", ws, request);
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
else {
|
|
812
|
+
if (!isAllowedWebSocketPath(pathname, allowedWsPaths)) {
|
|
789
813
|
socket.destroy();
|
|
814
|
+
return;
|
|
790
815
|
}
|
|
816
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
817
|
+
wss.emit("connection", ws, request);
|
|
818
|
+
});
|
|
791
819
|
});
|
|
792
820
|
wss.on("connection", (ws) => {
|
|
793
821
|
this.handleConnection(ws);
|
|
794
822
|
});
|
|
795
823
|
this.serverStartedAt = new Date().toISOString();
|
|
796
|
-
|
|
824
|
+
const onVercel = Boolean(process.env.VERCEL);
|
|
825
|
+
const shouldListen = options.autoListen ?? !onVercel;
|
|
826
|
+
if (shouldListen) {
|
|
827
|
+
server.listen(port, host, () => {
|
|
828
|
+
// eslint-disable-next-line no-console
|
|
829
|
+
console.log(`[TomorrowOS] listening on http://${host}:${port}`);
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
797
833
|
// eslint-disable-next-line no-console
|
|
798
|
-
console.log(`[TomorrowOS]
|
|
799
|
-
}
|
|
834
|
+
console.log(`[TomorrowOS] http.Server ready (autoListen=false${onVercel ? ", Vercel" : ""}; export default server)`);
|
|
835
|
+
}
|
|
800
836
|
this.httpServer = server;
|
|
801
837
|
this.wss = wss;
|
|
802
838
|
return server;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.30",
|
|
4
4
|
"description": "TomorrowOS CMS server SDK - WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-cms",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.30",
|
|
4
4
|
"description": "CMS server on @tomorrowos/sdk. Add your UI (React, static files, etc.) alongside this server.",
|
|
5
5
|
"private": true,
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build-player": "tomorrowos build --platform tizen"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@tomorrowos/sdk": "^0.9.
|
|
16
|
+
"@tomorrowos/sdk": "^0.9.30",
|
|
17
17
|
"dotenv": "^17.2.3",
|
|
18
18
|
"tsx": "^4.19.0"
|
|
19
19
|
},
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# TomorrowOS CMS Starter (Vercel / v0)
|
|
2
2
|
|
|
3
|
-
This starter is for **Vercel Publish** and **v0
|
|
3
|
+
This starter is for **Vercel Publish** and **v0**. It follows
|
|
4
|
+
[Vercel Functions WebSockets](https://vercel.com/docs/functions/websockets):
|
|
4
5
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
- **`
|
|
8
|
-
- **`
|
|
6
|
+
- **`cms-app.ts`** — shared `TomorrowOS.listen` + `export const server`
|
|
7
|
+
- **`api/index.ts`** — Production Function: `export default server` (Fluid)
|
|
8
|
+
- **`server.ts`** — local / Preview: same server (`npm start`)
|
|
9
|
+
- **`cms-panel/`** — Control Panel static files (not `public/`)
|
|
10
|
+
- **`preview/`** — Next.js shell for v0 Preview only
|
|
11
|
+
- **`vercel.json`** — `fluid`, rewrites → `/api`, `maxDuration`
|
|
9
12
|
|
|
10
13
|
## Scaffold
|
|
11
14
|
|
|
12
15
|
```bash
|
|
13
16
|
npx @tomorrowos/sdk init my-cms --hosting v0
|
|
14
|
-
# or: --hosting vercel
|
|
15
17
|
```
|
|
16
18
|
|
|
17
19
|
## Local development
|
|
@@ -21,39 +23,27 @@ npm install
|
|
|
21
23
|
npm run dev
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
Open `http://localhost:3000
|
|
26
|
+
Open `http://localhost:3000`. Pairing uses `ws://localhost:3000/` (root path).
|
|
25
27
|
|
|
26
|
-
### v0-style Preview
|
|
28
|
+
### v0-style Preview
|
|
27
29
|
|
|
28
30
|
```bash
|
|
29
31
|
npm run dev:preview
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
Pair devices against the **Publish URL**, not Preview.
|
|
33
35
|
|
|
34
|
-
## Production
|
|
36
|
+
## Production acceptance
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
1. `GET /status` → JSON
|
|
39
|
+
2. `GET /` → Control Panel
|
|
40
|
+
3. WebSocket upgrade on `/` or `/api` → **101** (not 200 HTML)
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
## Database
|
|
43
|
-
|
|
44
|
-
Use Vercel Environment Variables (Neon or Supabase pooled URL recommended):
|
|
45
|
-
|
|
46
|
-
```env
|
|
47
|
-
TOMORROWOS_STORE=postgres
|
|
48
|
-
DATABASE_URL=postgresql://...pooler...
|
|
49
|
-
DATABASE_SSL=true
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
SQLite under `data/` is for local demos only — not for production on Vercel.
|
|
42
|
+
TV endpoint: `https://YOUR.vercel.app/` (players also try `/api` on `*.vercel.app`).
|
|
53
43
|
|
|
54
44
|
## Replit / Railway
|
|
55
45
|
|
|
56
|
-
Use the default starter
|
|
46
|
+
Use the default starter (unchanged Node listen, no `api/` Function):
|
|
57
47
|
|
|
58
48
|
```bash
|
|
59
49
|
npx @tomorrowos/sdk init my-cms --hosting replit
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel Function entry (Fluid + WebSockets).
|
|
3
|
+
* Mounted at `/api`. vercel.json rewrites `/` and `/status` etc. here so the
|
|
4
|
+
* Control Panel and device `wss://` upgrades hit the same Node http.Server.
|
|
5
|
+
*
|
|
6
|
+
* Pattern: https://vercel.com/docs/functions/websockets
|
|
7
|
+
* const server = …; export default server;
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { server as default } from "../cms-app.js";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared TomorrowOS app for local Node and Vercel Functions.
|
|
3
|
+
* - Local / Railway-style: `server.ts` imports this (autoListen binds PORT).
|
|
4
|
+
* - Vercel Production: `api/index.ts` re-exports `server` (VERCEL → no listen;
|
|
5
|
+
* Fluid Function owns the socket — see https://vercel.com/docs/functions/websockets).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import "dotenv/config";
|
|
9
|
+
import { readFileSync } from "fs";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
import { createTomorrowOSStore, TomorrowOS } from "@tomorrowos/sdk";
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
|
|
16
|
+
const store = createTomorrowOSStore({
|
|
17
|
+
databaseUrl:
|
|
18
|
+
process.env.SUPABASE_URL ||
|
|
19
|
+
process.env.NEON_DATABASE_URL ||
|
|
20
|
+
process.env.DATABASE_URL,
|
|
21
|
+
sqlitePath: join(__dirname, "data", "tomorrowos.db")
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const tomorrowos = new TomorrowOS({ brand, store });
|
|
25
|
+
|
|
26
|
+
const port = Number(
|
|
27
|
+
process.env.TOMORROWOS_INTERNAL_PORT || process.env.PORT || 3000
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const server = tomorrowos.listen({
|
|
31
|
+
port,
|
|
32
|
+
host: "0.0.0.0",
|
|
33
|
+
staticRoot: join(__dirname, "cms-panel")
|
|
34
|
+
// autoListen defaults to false when process.env.VERCEL is set
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
tomorrowos.on("device.paired", (event) => {
|
|
38
|
+
console.log(`[TomorrowOS] device paired: ${event.deviceId}`);
|
|
39
|
+
void tomorrowos.pushLatestPolicyToDevice(event.deviceId).then((r) => {
|
|
40
|
+
if (r.pushed) {
|
|
41
|
+
console.log(`[TomorrowOS] pushed latest policy to ${event.deviceId} (paired)`);
|
|
42
|
+
}
|
|
43
|
+
}).catch((err) => {
|
|
44
|
+
console.error(`[TomorrowOS] policy push on paired failed for ${event.deviceId}:`, err);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
tomorrowos.on("device.online", (event) => {
|
|
49
|
+
console.log(`[TomorrowOS] device online: ${event.deviceId}`);
|
|
50
|
+
void tomorrowos.pushLatestPolicyToDevice(event.deviceId).then((r) => {
|
|
51
|
+
if (r.pushed) {
|
|
52
|
+
console.log(`[TomorrowOS] pushed latest policy to ${event.deviceId}`);
|
|
53
|
+
}
|
|
54
|
+
}).catch((err) => {
|
|
55
|
+
console.error(`[TomorrowOS] latest policy push failed for ${event.deviceId}:`, err);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
tomorrowos.on("device.offline", (event) => {
|
|
60
|
+
console.log(
|
|
61
|
+
`[TomorrowOS] device offline: ${event.deviceId} (lastSeen: ${event.lastSeen})`
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
tomorrowos.on("command.verified", (event) => {
|
|
66
|
+
console.log(
|
|
67
|
+
`[TomorrowOS] command verified: ${event.commandId} (${event.method})`
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
tomorrowos.on("command.failed", (event) => {
|
|
72
|
+
console.error(
|
|
73
|
+
`[TomorrowOS] command failed: ${event.commandId} (${event.method}) — ${event.error.message}`
|
|
74
|
+
);
|
|
75
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-cms",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "TomorrowOS CMS for Vercel / v0 Publish (
|
|
3
|
+
"version": "0.9.30",
|
|
4
|
+
"description": "TomorrowOS CMS for Vercel / v0 Publish (Fluid Function + WebSockets + optional Next Preview).",
|
|
5
5
|
"private": true,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"build-player": "tomorrowos build --platform tizen"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@tomorrowos/sdk": "^0.9.
|
|
20
|
+
"@tomorrowos/sdk": "^0.9.30",
|
|
21
21
|
"dotenv": "^17.2.3",
|
|
22
22
|
"tsx": "^4.19.0"
|
|
23
23
|
},
|
|
@@ -1,75 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Production
|
|
4
|
-
* Preview: npm run dev:preview (Next in preview/ proxies to this server on :3001).
|
|
2
|
+
* Local / non-Vercel entry (`npm start`, Preview internal port).
|
|
3
|
+
* On Vercel Production the Function entry is `api/index.ts` (see vercel.json).
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
import { readFileSync } from "fs";
|
|
9
|
-
import { fileURLToPath } from "url";
|
|
10
|
-
import { dirname, join } from "path";
|
|
11
|
-
import { createTomorrowOSStore, TomorrowOS } from "@tomorrowos/sdk";
|
|
12
|
-
|
|
13
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
|
|
15
|
-
const store = createTomorrowOSStore({
|
|
16
|
-
databaseUrl:
|
|
17
|
-
process.env.SUPABASE_URL ||
|
|
18
|
-
process.env.NEON_DATABASE_URL ||
|
|
19
|
-
process.env.DATABASE_URL,
|
|
20
|
-
sqlitePath: join(__dirname, "data", "tomorrowos.db")
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const tomorrowos = new TomorrowOS({ brand, store });
|
|
24
|
-
|
|
25
|
-
const port = Number(
|
|
26
|
-
process.env.TOMORROWOS_INTERNAL_PORT || process.env.PORT || 3000
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
const server = tomorrowos.listen({
|
|
30
|
-
port,
|
|
31
|
-
host: "0.0.0.0",
|
|
32
|
-
staticRoot: join(__dirname, "cms-panel")
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
tomorrowos.on("device.paired", (event) => {
|
|
36
|
-
console.log(`[TomorrowOS] device paired: ${event.deviceId}`);
|
|
37
|
-
void tomorrowos.pushLatestPolicyToDevice(event.deviceId).then((r) => {
|
|
38
|
-
if (r.pushed) {
|
|
39
|
-
console.log(`[TomorrowOS] pushed latest policy to ${event.deviceId} (paired)`);
|
|
40
|
-
}
|
|
41
|
-
}).catch((err) => {
|
|
42
|
-
console.error(`[TomorrowOS] policy push on paired failed for ${event.deviceId}:`, err);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
tomorrowos.on("device.online", (event) => {
|
|
47
|
-
console.log(`[TomorrowOS] device online: ${event.deviceId}`);
|
|
48
|
-
void tomorrowos.pushLatestPolicyToDevice(event.deviceId).then((r) => {
|
|
49
|
-
if (r.pushed) {
|
|
50
|
-
console.log(`[TomorrowOS] pushed latest policy to ${event.deviceId}`);
|
|
51
|
-
}
|
|
52
|
-
}).catch((err) => {
|
|
53
|
-
console.error(`[TomorrowOS] latest policy push failed for ${event.deviceId}:`, err);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
tomorrowos.on("device.offline", (event) => {
|
|
58
|
-
console.log(
|
|
59
|
-
`[TomorrowOS] device offline: ${event.deviceId} (lastSeen: ${event.lastSeen})`
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
tomorrowos.on("command.verified", (event) => {
|
|
64
|
-
console.log(
|
|
65
|
-
`[TomorrowOS] command verified: ${event.commandId} (${event.method})`
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
tomorrowos.on("command.failed", (event) => {
|
|
70
|
-
console.error(
|
|
71
|
-
`[TomorrowOS] command failed: ${event.commandId} (${event.method}) — ${event.error.message}`
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
export default server;
|
|
6
|
+
export { server as default } from "./cms-app.js";
|
|
@@ -3,5 +3,15 @@
|
|
|
3
3
|
"fluid": true,
|
|
4
4
|
"framework": null,
|
|
5
5
|
"installCommand": "npm install",
|
|
6
|
-
"buildCommand": "npm run build"
|
|
6
|
+
"buildCommand": "npm run build",
|
|
7
|
+
"functions": {
|
|
8
|
+
"api/index.ts": {
|
|
9
|
+
"maxDuration": 800,
|
|
10
|
+
"memory": 1024,
|
|
11
|
+
"includeFiles": "{cms-panel/**,brand.json,assets/**}"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"rewrites": [
|
|
15
|
+
{ "source": "/((?!api/).*)", "destination": "/api" }
|
|
16
|
+
]
|
|
7
17
|
}
|