@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.
@@ -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 `/` are unchanged.
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;
@@ -1 +1 @@
1
- {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
@@ -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 === "/" || 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
- server.listen(port, host, () => {
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] listening on http://${host}:${port}`);
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.29",
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.29",
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.29",
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
- This starter is for **Vercel Publish** and **v0** no-code setup. It uses:
4
-
5
- - Root **`server.ts`** with `export default server` (Vercel Fluid Node)
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 development
13
+ ## Local / v0 Preview
18
14
 
19
15
  ```bash
20
16
  npm install
21
17
  npm run dev
22
18
  ```
23
19
 
24
- Open `http://localhost:3000` TomorrowOS Control Panel.
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
- ## Production (Vercel / v0 Publish)
22
+ 1. TomorrowOS on `http://127.0.0.1:3001`
23
+ 2. Next on `http://localhost:3000` → proxies `/` to TomorrowOS
35
24
 
36
- - **Start:** `npm run start` `tsx server.ts`
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
- Follow **`VERCEL_SETUP.md`** in `@tomorrowos/sdk` for database, media, and branding Q&A.
27
+ ## Production
41
28
 
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.
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,4 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited — see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Preview-only Next config. Production Publish ignores this
3
+ * (`vercel.json` → framework: null + api/index.ts Fluid Function).
4
+ *
5
+ * HTTP proxy lives in `app/[[...slug]]/route.ts` (not rewrites) so POST
6
+ * uploads and custom 502 pages work when TomorrowOS is down.
7
+ *
8
+ * @type {import('next').NextConfig}
9
+ */
10
+ const nextConfig = {
11
+ // Do not add a root page.tsx that returns null — v0 shows
12
+ // "Your v0 generation will show here" instead of the Control Panel.
13
+ };
14
+
15
+ export default nextConfig;
@@ -1,35 +1,35 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.9.29",
4
- "description": "TomorrowOS CMS for Vercel / v0 Publish (Node Fluid + optional Next Preview shell).",
3
+ "version": "0.9.31",
4
+ "description": "TomorrowOS CMS for Vercel / v0 (Fluid Function Publish + Next Preview proxy).",
5
5
  "private": true,
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=20"
9
9
  },
10
10
  "scripts": {
11
- "dev": "tsx watch server.ts",
11
+ "dev": "npm run dev:preview",
12
12
  "dev:tomorrowos": "cross-env TOMORROWOS_INTERNAL_PORT=3001 tsx watch server.ts",
13
- "dev:next": "cd preview && next dev -p 3000",
14
- "dev:preview": "concurrently -k \"npm:dev:tomorrowos\" \"npm:dev:next\"",
13
+ "dev:next": "next dev -p 3000",
14
+ "dev:preview": "concurrently -k -n cms,next -c cyan,magenta \"npm:dev:tomorrowos\" \"npm:dev:next\"",
15
15
  "start": "tsx server.ts",
16
16
  "build": "node -e \"process.exit(0)\"",
17
17
  "build-player": "tomorrowos build --platform tizen"
18
18
  },
19
19
  "dependencies": {
20
- "@tomorrowos/sdk": "^0.9.29",
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
- * TomorrowOS CMS server Vercel / v0 starter.
3
- * Production: root server.ts + Fluid (see vercel.json).
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
- import "dotenv/config";
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";
@@ -9,5 +9,6 @@
9
9
  "noEmit": true,
10
10
  "types": ["node"]
11
11
  },
12
- "include": ["server.ts"]
12
+ "include": ["server.ts", "cms-app.ts", "api/**/*.ts"],
13
+ "exclude": ["app", "node_modules", ".next"]
13
14
  }
@@ -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,11 +0,0 @@
1
- export default function RootLayout({
2
- children
3
- }: {
4
- children: React.ReactNode;
5
- }) {
6
- return (
7
- <html lang="en">
8
- <body>{children}</body>
9
- </html>
10
- );
11
- }
@@ -1,3 +0,0 @@
1
- export default function PreviewShellPage() {
2
- return null;
3
- }
@@ -1,2 +0,0 @@
1
- /// <reference types="next" />
2
- /// <reference types="next/image-types/global" />
@@ -1,18 +0,0 @@
1
- const internalUrl =
2
- process.env.TOMORROWOS_INTERNAL_URL || "http://127.0.0.1:3001";
3
-
4
- /** @type {import('next').NextConfig} */
5
- const nextConfig = {
6
- async rewrites() {
7
- return {
8
- fallback: [
9
- {
10
- source: "/:path*",
11
- destination: `${internalUrl}/:path*`
12
- }
13
- ]
14
- };
15
- }
16
- };
17
-
18
- export default nextConfig;
@@ -1,20 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "lib": ["dom", "dom.iterable", "esnext"],
5
- "allowJs": true,
6
- "skipLibCheck": true,
7
- "strict": true,
8
- "noEmit": true,
9
- "esModuleInterop": true,
10
- "module": "esnext",
11
- "moduleResolution": "bundler",
12
- "resolveJsonModule": true,
13
- "isolatedModules": true,
14
- "jsx": "preserve",
15
- "incremental": true,
16
- "plugins": [{ "name": "next" }]
17
- },
18
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19
- "exclude": ["node_modules"]
20
- }