@tomorrowos/sdk 0.9.29 → 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 +266 -208
- 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 +10 -35
- package/templates/cms-starter-v0/api/index.ts +10 -0
- 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/cms-app.ts +75 -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/server.ts +3 -72
- package/templates/cms-starter-v0/tsconfig.json +2 -1
- package/templates/cms-starter-v0/vercel.json +11 -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/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.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,60 +1,35 @@
|
|
|
1
1
|
# TomorrowOS CMS Starter (Vercel / v0)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
- Control Panel static files in **`cms-panel/`** (not `public/` — avoids Vercel static-output trap)
|
|
7
|
-
- **`preview/`** — thin Next.js shell for v0 Preview only (`npm run dev:preview`)
|
|
8
|
-
- **`vercel.json`** — minimal Fluid config (no `startCommand`; use `package.json` `start`)
|
|
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/`)
|
|
9
6
|
|
|
10
7
|
## Scaffold
|
|
11
8
|
|
|
12
9
|
```bash
|
|
13
10
|
npx @tomorrowos/sdk init my-cms --hosting v0
|
|
14
|
-
# or: --hosting vercel
|
|
15
11
|
```
|
|
16
12
|
|
|
17
|
-
## Local
|
|
13
|
+
## Local / v0 Preview
|
|
18
14
|
|
|
19
15
|
```bash
|
|
20
16
|
npm install
|
|
21
17
|
npm run dev
|
|
22
18
|
```
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
### v0-style Preview (Next proxy + TomorrowOS on :3001)
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npm run dev:preview
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Open `http://localhost:3000` — Next proxies to TomorrowOS. Pair devices against **Publish URL**, not Preview.
|
|
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
|
-
|
|
37
|
-
- **Build:** no-op (`npm run build`)
|
|
38
|
-
- **Acceptance:** `GET /status` on your production URL must return **JSON**
|
|
25
|
+
Open Preview at `http://localhost:3000` — you should see the Control Panel, **not** “Your v0 generation will show here”.
|
|
39
26
|
|
|
40
|
-
|
|
27
|
+
## Production
|
|
41
28
|
|
|
42
|
-
|
|
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.
|
|
29
|
+
Unchanged: `vercel.json` + `api/index.ts`. Acceptance: `/status` JSON + WebSocket **101**.
|
|
53
30
|
|
|
54
31
|
## Replit / Railway
|
|
55
32
|
|
|
56
|
-
Use the default starter instead:
|
|
57
|
-
|
|
58
33
|
```bash
|
|
59
34
|
npx @tomorrowos/sdk init my-cms --hosting replit
|
|
60
35
|
```
|
|
@@ -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,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,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
|
+
});
|
|
@@ -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,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": 300,
|
|
10
|
+
"memory": 1024,
|
|
11
|
+
"includeFiles": "{cms-panel/**,brand.json,assets/**}"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"rewrites": [
|
|
15
|
+
{ "source": "/((?!api/).*)", "destination": "/api" }
|
|
16
|
+
]
|
|
7
17
|
}
|
|
@@ -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
|
-
}
|