@tomorrowos/sdk 0.7.3 → 0.7.5

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/LLM_PROMPT.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  > **This file is the contract between TomorrowOS and any LLM generating code using this package. You must follow it literally. Do not skip questions, do not infer answers, do not start writing code until the protocol is complete.**
4
4
 
5
+ > **Replit Agent:** if the user is setting up a CMS on Replit (or pastes a prompt that mentions `REPLIT_SETUP.md`), follow **`REPLIT_SETUP.md`** instead of this file. That protocol covers SQLite vs Supabase by screen count, `SUPABASE_URL`, Cloudinary vs Object Storage, and `brand.json`.
6
+
5
7
  You are generating a digital signage CMS using `@tomorrowos/sdk`. The user has asked you to build something like "a CMS for my restaurant" or "a signage dashboard for my retail network". Before you write any code, you must gather five pieces of information from the user.
6
8
 
7
9
  Ask each question in order. If the user answers multiple at once, record those and skip ahead. If the user hasn't given you an answer to a specific field, do not invent one — ask the question explicitly.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  **You are building a digital signage CMS using TomorrowOS. Before writing any code, follow the elicitation protocol in `LLM_PROMPT.md`. Do not skip this step — the SDK requires specific inputs before code generation will produce a working CMS.**
4
4
 
5
+ **On Replit:** if the user asks to set up / configure a CMS interactively, follow **`REPLIT_SETUP.md`** instead (Q&A for screen count → SQLite vs Supabase → Cloudinary vs Object Storage → `brand.json`).
6
+
5
7
  TomorrowOS is the open standard for digital signage. This SDK lets you build a CMS that controls Samsung Tizen, BrightSign, LG webOS, Android, ChromeOS, and Windows screens through a single unified API.
6
8
 
7
9
  ---
@@ -10,12 +12,13 @@ TomorrowOS is the open standard for digital signage. This SDK lets you build a C
10
12
 
11
13
  When you see this package referenced in a user prompt, your workflow is:
12
14
 
13
- 1. **Read `LLM_PROMPT.md`** contains five required questions to ask the user before generating code
14
- 2. **Read `BUILD_GUARDRAILS.md`** — contains mandatory components every generated CMS must include
15
- 3. **Read `brand.schema.json`** — contains the validated shape of `brand.json`
16
- 4. Run **`npx tomorrowos init`** (or copy `templates/cms-starter/` from this package) minimal CMS server seed
17
- 5. **Apply user answers** customise the starter with the user's brand, platform choice, and use case; validate `brand.json` against `brand.schema.json`
18
- 6. **Show `PLAYER_INSTALL.md`**after the CMS is working, show how to build and install the player on a screen
15
+ 1. **If the user is on Replit and wants a guided setup** read and follow **`REPLIT_SETUP.md`** (preferred for Replit Agent)
16
+ 2. **Otherwise read `LLM_PROMPT.md`** — five required questions before generating code
17
+ 3. **Read `BUILD_GUARDRAILS.md`** — mandatory components every generated CMS must include
18
+ 4. **Read `brand.schema.json`** — validated shape of `brand.json`
19
+ 5. Run **`npx tomorrowos init`** (or copy `templates/cms-starter/` from this package) minimal CMS server seed
20
+ 6. **Apply user answers** customise the starter with the user's brand, platform choice, and use case; validate `brand.json` against `brand.schema.json`
21
+ 7. **Show `PLAYER_INSTALL.md`** — after the CMS is working, show how to build and install the player on a screen
19
22
 
20
23
  You must not write WebSocket transport, pairing wire format, or platform-specific player bridge code in the CMS — the SDK handles server-side device sessions and HTTP helpers. You customise UI, branding, and how you call the SDK.
21
24
 
@@ -42,12 +45,26 @@ npm run dev
42
45
  schema. The starter server uses that SQLite database by default, so pairings,
43
46
  playlists, and device assignments survive normal server restarts.
44
47
 
45
- To switch to Supabase/Postgres, edit `.env`:
48
+ ### Replit guided setup
49
+
50
+ In Replit Agent, paste:
51
+
52
+ ```text
53
+ Follow @tomorrowos/sdk REPLIT_SETUP.md and set up my TomorrowOS CMS.
54
+ Ask me the questions in order. Do not skip steps.
55
+ ```
56
+
57
+ That wizard chooses SQLite (≤5 screens) or Supabase (>5), prefers Cloudinary for
58
+ media, and writes `brand.json` from your answers. On Replit, Supabase must use
59
+ Secret **`SUPABASE_URL`** (not the reserved `DATABASE_URL`).
60
+
61
+ To switch to Supabase/Postgres manually, edit `.env` / Secrets:
46
62
 
47
63
  ```bash
48
64
  TOMORROWOS_STORE=supabase
49
- DATABASE_URL=postgresql://...
65
+ SUPABASE_URL=postgresql://...
50
66
  DATABASE_SSL=true
67
+ # DATABASE_URL=... # optional fallback outside Replit
51
68
  ```
52
69
 
53
70
  For throwaway demos/tests, set `TOMORROWOS_STORE=memory`. The generated starter
@@ -0,0 +1,341 @@
1
+ # Replit Setup Protocol for TomorrowOS CMS
2
+
3
+ > **This file is a contract for Replit Agent / Replit AI.**
4
+ > Follow it literally. Ask questions in order. Do not invent answers. Do not write or rewrite application logic until the Q&A is complete. Prefer configuring Secrets, `.env`, `brand.json`, and `server.ts` store wiring over inventing a custom CMS.
5
+
6
+ ## User trigger (copy-paste)
7
+
8
+ When a human pastes a prompt like this, **read this file and start Question 1 immediately**:
9
+
10
+ ```text
11
+ Follow @tomorrowos/sdk REPLIT_SETUP.md and set up my TomorrowOS CMS.
12
+ Ask me the questions in order. Do not skip steps.
13
+ ```
14
+
15
+ Alternate triggers that also activate this protocol:
16
+
17
+ - “Set up TomorrowOS on Replit using REPLIT_SETUP.md”
18
+ - “Configure my TomorrowOS CMS (Replit wizard)”
19
+
20
+ ---
21
+
22
+ ## Hard rules
23
+
24
+ 1. **One question at a time.** Wait for the user’s answer before asking the next question (unless they already answered several in one message).
25
+ 2. **Do not invent** screen counts, Cloudinary credentials, Supabase URLs, or brand colours.
26
+ 3. **Do not use Replit’s reserved `DATABASE_URL` Secret for Supabase.** On Replit, `DATABASE_URL` is often pre-claimed. Always use **`SUPABASE_URL`** for TomorrowOS ↔ Supabase.
27
+ 4. **Prefer `npx @tomorrowos/sdk init`** (or the package’s `templates/cms-starter`) as the project seed. Do not rebuild pairing, WebSocket, or playlist APIs from scratch.
28
+ 5. **Never commit secrets** into git. Put credentials only in Replit **Secrets** (and optionally mirror non-secret flags in `.env` that reference env vars).
29
+ 6. After Q&A, apply configuration, install deps, start the server, and verify:
30
+ - HTTP Control Panel loads
31
+ - `GET /brand.json` returns 200
32
+ - WebSocket upgrade on `wss://<repl-host>/` succeeds (critical for TV pairing)
33
+
34
+ ---
35
+
36
+ ## Question 1 — How many screens / devices?
37
+
38
+ **Ask exactly:**
39
+
40
+ > How many TomorrowOS screens (devices) do you expect to manage with this CMS?
41
+ >
42
+ > - **5 or fewer** → I’ll use the default **SQLite** database (simple, good for demos and small venues).
43
+ > - **More than 5** → I’ll configure **Supabase (Postgres)** so pairings and playlists scale more reliably.
44
+
45
+ **Branching:**
46
+
47
+ | Answer | Store driver | Next |
48
+ |--------|--------------|------|
49
+ | ≤ 5 | `TOMORROWOS_STORE=sqlite` | Skip Question 2 → go to Question 3 |
50
+ | > 5 | `TOMORROWOS_STORE=supabase` | Ask Question 2 |
51
+ | Unclear | Ask again | Do not proceed |
52
+
53
+ **Store in `brand.json`:** `cms.expectedScreens` (number), `cms.hostingTarget`: `"here"`.
54
+
55
+ ---
56
+
57
+ ## Question 2 — Supabase connection (only if > 5 screens)
58
+
59
+ **Ask exactly:**
60
+
61
+ > Paste your Supabase Postgres connection string. I will store it as the Replit Secret **`SUPABASE_URL`** (not `DATABASE_URL` — that name is often reserved by Replit).
62
+ >
63
+ > Example shape: `postgresql://postgres.[PROJECT]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres`
64
+ >
65
+ > Also confirm whether SSL is required (usually **yes** for Supabase).
66
+
67
+ **You must then:**
68
+
69
+ 1. Create / update Replit Secret: `SUPABASE_URL=<user value>`
70
+ 2. Create / update Replit Secret or `.env`:
71
+ - `TOMORROWOS_STORE=supabase`
72
+ - `DATABASE_SSL=true` (unless the user explicitly says SSL is off)
73
+ 3. Update `server.ts` so the store uses Supabase via `SUPABASE_URL`:
74
+
75
+ ```ts
76
+ import "dotenv/config";
77
+ import { readFileSync } from "fs";
78
+ import { fileURLToPath } from "url";
79
+ import { dirname, join } from "path";
80
+ import { createTomorrowOSStore, TomorrowOS } from "@tomorrowos/sdk";
81
+
82
+ const __dirname = dirname(fileURLToPath(import.meta.url));
83
+ const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
84
+
85
+ const store = createTomorrowOSStore({
86
+ // Prefer SUPABASE_URL on Replit. Do NOT rely on Replit's reserved DATABASE_URL.
87
+ databaseUrl: process.env.SUPABASE_URL || process.env.DATABASE_URL,
88
+ sqlitePath: join(__dirname, "data", "tomorrowos.db")
89
+ });
90
+
91
+ const tomorrowos = new TomorrowOS({ brand, store });
92
+
93
+ tomorrowos.listen({
94
+ port: Number(process.env.PORT) || 3000,
95
+ host: "0.0.0.0",
96
+ staticRoot: join(__dirname, "public")
97
+ });
98
+ ```
99
+
100
+ 4. Ensure `.env` / Secrets include:
101
+
102
+ ```env
103
+ TOMORROWOS_STORE=supabase
104
+ SUPABASE_URL=postgresql://...
105
+ DATABASE_SSL=true
106
+ ```
107
+
108
+ 5. **Do not** write the real password into committed files.
109
+
110
+ If the user refuses Supabase but chose > 5 screens, warn them SQLite on Replit is fragile for fleets, then offer SQLite only after they confirm.
111
+
112
+ ---
113
+
114
+ ## Question 3 — Media storage (uploads / thumbnails)
115
+
116
+ **Ask exactly:**
117
+
118
+ > How should playlist media (images/videos) be stored?
119
+ >
120
+ > **Recommended: Cloudinary** — public HTTPS URLs, survives Replit restarts/redeploys, avoids Object Storage permission issues.
121
+ >
122
+ > Alternatives:
123
+ > - **Replit Object Storage** — keep files under `public/uploads` backed by Object Storage (no Cloudinary account).
124
+ > - **Local disk only** — `public/uploads` on the Repl filesystem (fine for quick tests; files may disappear on rebuild if not persistent).
125
+ >
126
+ > Do you want me to set up **Cloudinary**? (yes / no)
127
+
128
+ ### If YES — Cloudinary
129
+
130
+ **Ask for (or open a Secrets form for):**
131
+
132
+ - `CLOUDINARY_CLOUD_NAME`
133
+ - `CLOUDINARY_API_KEY`
134
+ - `CLOUDINARY_API_SECRET`
135
+ - Optional: `CLOUDINARY_FOLDER` (e.g. `tomorrowos`)
136
+
137
+ Store these as Replit Secrets. The SDK auto-detects them and uploads media to Cloudinary instead of local disk.
138
+
139
+ Tell the user:
140
+
141
+ > After Cloudinary Secrets are set, restart the Repl. New uploads should return `https://res.cloudinary.com/...` URLs.
142
+
143
+ ### If NO — Replit Object Storage (or local)
144
+
145
+ **Do this:**
146
+
147
+ 1. Prefer configuring **Replit Object Storage** so writes to `public/uploads/` succeed and persist.
148
+ 2. Create the directory if missing: `mkdir -p public/uploads`
149
+ 3. Warn: if Object Storage IAM denies `storage.objects.create`, uploads will fail with a Google Cloud Storage permission error — fix Object Storage permissions or switch to Cloudinary.
150
+ 4. Do **not** invent Cloudinary credentials.
151
+
152
+ **Also persist metadata:** keep `data/tomorrowos.db` (SQLite path) on durable storage when using SQLite.
153
+
154
+ ---
155
+
156
+ ## Question 4 — Brand / TomorrowOS app look
157
+
158
+ **Ask exactly:**
159
+
160
+ > Let’s brand your TomorrowOS experience. Please provide:
161
+ >
162
+ > 1. **Product / venue name** (shown on screens and the Control Panel)
163
+ > 2. **Tagline** (optional)
164
+ > 3. **Primary colour** (hex, e.g. `#FF8A3D`)
165
+ > 4. **Background colour** (hex, optional — default `#FAFAF9`)
166
+ > 5. **Text colour** (hex, optional — default `#0A0908`)
167
+ > 6. **Secondary / accent colour** (hex, optional)
168
+ > 7. **Logo** — upload an SVG/PNG into the project, or give a URL I can fetch into `./assets/`
169
+ > 8. **Target platforms** — `tizen`, `brightsign`, or both
170
+ > 9. **Use case** — one of: `restaurant`, `retail`, `corporate`, `wayfinding`, `transit`, `other`
171
+
172
+ If the user only gives a name and primary colour, use defaults for the rest and say what you assumed.
173
+
174
+ **Then write / update `brand.json`** at the project root (validate mentally against `brand.schema.json` in the SDK). Minimum example:
175
+
176
+ ```json
177
+ {
178
+ "name": "<user name>",
179
+ "tagline": "<user tagline or Digital signage>",
180
+ "targetPlatforms": ["tizen"],
181
+ "primaryColor": "#FF8A3D",
182
+ "secondaryColor": "#F5F3EF",
183
+ "backgroundColor": "#FAFAF9",
184
+ "textColor": "#0A0908",
185
+ "logoPath": "./assets/logo.svg",
186
+ "fontFamily": "Inter",
187
+ "cms": {
188
+ "useCase": "other",
189
+ "hostingTarget": "here",
190
+ "expectedScreens": 5,
191
+ "features": {
192
+ "bulkCommands": false,
193
+ "proofOfPlay": false,
194
+ "contentScheduling": true,
195
+ "userManagement": false
196
+ }
197
+ },
198
+ "protocolVersion": "1.0"
199
+ }
200
+ ```
201
+
202
+ Set `cms.expectedScreens` from Question 1. Set `cmsEndpoint` only if the user already knows their public `wss://…replit.app` URL; otherwise leave it out and tell them to pair TVs using the published HTTPS URL (player converts `https://` → `wss://`).
203
+
204
+ ---
205
+
206
+ ## After all answers — execution checklist
207
+
208
+ Run these steps in order. Tick mentally; do not skip.
209
+
210
+ ### A. Seed the project
211
+
212
+ ```bash
213
+ npx @tomorrowos/sdk@latest init .
214
+ # If the directory is not empty and the user confirms overwrite of starter files only:
215
+ # npx @tomorrowos/sdk@latest init . --force
216
+ ```
217
+
218
+ If `init` is inappropriate (existing customised project), merge carefully:
219
+
220
+ - Keep `data/tomorrowos.db` and `public/uploads/`
221
+ - Update `brand.json`, Secrets, and store wiring only
222
+
223
+ ### B. Apply store + media Secrets
224
+
225
+ **SQLite (≤5):**
226
+
227
+ ```env
228
+ TOMORROWOS_STORE=sqlite
229
+ TOMORROWOS_DB_PATH=./data/tomorrowos.db
230
+ ```
231
+
232
+ **Supabase (>5):**
233
+
234
+ ```env
235
+ TOMORROWOS_STORE=supabase
236
+ SUPABASE_URL=...
237
+ DATABASE_SSL=true
238
+ ```
239
+
240
+ **Cloudinary (if chosen):**
241
+
242
+ ```env
243
+ CLOUDINARY_CLOUD_NAME=...
244
+ CLOUDINARY_API_KEY=...
245
+ CLOUDINARY_API_SECRET=...
246
+ ```
247
+
248
+ ### C. Wire `server.ts`
249
+
250
+ - Always: `host: "0.0.0.0"`, `port: Number(process.env.PORT) || 3000`, `staticRoot: public`
251
+ - Supabase path: pass `databaseUrl: process.env.SUPABASE_URL || process.env.DATABASE_URL` into `createTomorrowOSStore`
252
+ - Keep TomorrowOS event handlers from the starter (paired / online policy push)
253
+
254
+ ### D. Install and run
255
+
256
+ ```bash
257
+ npm install
258
+ npm run dev
259
+ ```
260
+
261
+ Confirm logs contain something like:
262
+
263
+ ```text
264
+ [TomorrowOS] listening on http://0.0.0.0:<PORT>
265
+ ```
266
+
267
+ ### E. Verify before telling the user “done”
268
+
269
+ 1. Open the Control Panel URL.
270
+ 2. `GET /brand.json` → 200 and correct `name` / colours.
271
+ 3. In browser console on the Control Panel origin:
272
+
273
+ ```javascript
274
+ const ws = new WebSocket(location.origin.replace(/^http/, "ws") + "/");
275
+ ws.onopen = () => console.log("WS OK");
276
+ ws.onerror = (e) => console.log("WS ERR", e);
277
+ ```
278
+
279
+ If WebSocket fails, fix Run command / process crash / sleep **before** asking the user to pair a TV.
280
+
281
+ ### F. Tell the user next steps (no code)
282
+
283
+ 1. Keep this Repl **running** (Always On if available).
284
+ 2. On the TomorrowOS player, enter the CMS URL: `https://<your-repl>.replit.app/`
285
+ 3. Enter the 6-character pairing code from the screen into the Control Panel.
286
+ 4. Create a playlist, upload media, Publish to the device.
287
+ 5. Point them at `PLAYER_INSTALL.md` in the SDK for player install details.
288
+
289
+ ---
290
+
291
+ ## What not to ask / change
292
+
293
+ Do **not** ask the user how to implement:
294
+
295
+ - WebSocket framing
296
+ - Pairing crypto
297
+ - `device.content.setPolicy` payload shape
298
+ - Which WS library to use
299
+
300
+ Do **not**:
301
+
302
+ - Replace `@tomorrowos/sdk` with a hand-rolled server
303
+ - Point Supabase at Replit’s reserved `DATABASE_URL` when `SUPABASE_URL` should be used
304
+ - Skip Cloudinary recommendation without stating the persistence tradeoff
305
+ - Claim uploads are “permanent” on Replit local disk without Object Storage or Cloudinary
306
+
307
+ ---
308
+
309
+ ## SQLite path (≤5) — `server.ts` reference
310
+
311
+ Default starter is fine:
312
+
313
+ ```ts
314
+ const store = createTomorrowOSStore({
315
+ sqlitePath: join(__dirname, "data", "tomorrowos.db")
316
+ });
317
+ ```
318
+
319
+ With Secrets / `.env`:
320
+
321
+ ```env
322
+ TOMORROWOS_STORE=sqlite
323
+ ```
324
+
325
+ ---
326
+
327
+ ## Failure recovery cheat sheet
328
+
329
+ | Symptom | Likely cause | Fix |
330
+ |---------|--------------|-----|
331
+ | TV / browser: WebSocket handshake timeout | Node CMS not running or crash-loop | Fix Run → `npm start` / `npm run dev`; check logs for `listening` |
332
+ | Upload: `storage.objects.create` denied | Replit Object Storage IAM | Fix Object Storage permissions **or** switch to Cloudinary |
333
+ | Upload OK then thumbnails vanish later | Non-persistent `public/uploads` | Object Storage or Cloudinary |
334
+ | Supabase connection errors | Wrong URL or using reserved `DATABASE_URL` | Use `SUPABASE_URL` + `TOMORROWOS_STORE=supabase` |
335
+ | Pairing works on one Repl but not another | That Repl’s WS/backend only | Compare Run command and logs with a known-good Repl |
336
+
337
+ ---
338
+
339
+ ## Protocol version
340
+
341
+ `replit-setup/1.0` — aligned with TomorrowOS protocol `1.0` and `@tomorrowos/sdk` store drivers `sqlite` | `supabase` | `postgres` | `memory`.
@@ -9,7 +9,11 @@ export interface CreateTomorrowOSStoreOptions {
9
9
  driver?: TomorrowOSStoreDriver;
10
10
  /** Defaults to TOMORROWOS_DB_PATH, then ./data/tomorrowos.db. */
11
11
  sqlitePath?: string;
12
- /** Supabase/Postgres connection string. Defaults to DATABASE_URL. */
12
+ /**
13
+ * Supabase/Postgres connection string.
14
+ * Defaults to SUPABASE_URL, then DATABASE_URL.
15
+ * Prefer SUPABASE_URL on Replit (DATABASE_URL is often a reserved Secret).
16
+ */
13
17
  databaseUrl?: string;
14
18
  /** Defaults from DATABASE_SSL / DATABASE_URL. */
15
19
  postgresSsl?: PoolConfig["ssl"];
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/store/factory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAKrC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;AAElF,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,WAAW,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,wBAAgB,iBAAiB,CAAC,GAAG,SAAgB,GAAG,MAAM,CAE7D;AAED,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,eAAe,CAmCjB"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/store/factory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAKrC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;AAElF,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,WAAW,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,wBAAgB,iBAAiB,CAAC,GAAG,SAAgB,GAAG,MAAM,CAE7D;AAED,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,eAAe,CAoCjB"}
@@ -7,7 +7,7 @@ export function defaultSQLitePath(cwd = process.cwd()) {
7
7
  }
8
8
  export function createTomorrowOSStore(options = {}) {
9
9
  const env = options.env ?? process.env;
10
- const databaseUrl = options.databaseUrl ?? env.DATABASE_URL;
10
+ const databaseUrl = options.databaseUrl ?? env.SUPABASE_URL ?? env.DATABASE_URL;
11
11
  const driver = (options.driver ??
12
12
  env.TOMORROWOS_STORE ??
13
13
  (databaseUrl ? "postgres" : "sqlite")).toLowerCase();
@@ -16,7 +16,7 @@ export function createTomorrowOSStore(options = {}) {
16
16
  }
17
17
  if (driver === "postgres" || driver === "supabase") {
18
18
  if (!databaseUrl) {
19
- throw new Error(`TOMORROWOS_STORE=${driver} requires DATABASE_URL. Set DATABASE_URL in .env or unset TOMORROWOS_STORE to use SQLite.`);
19
+ throw new Error(`TOMORROWOS_STORE=${driver} requires SUPABASE_URL or DATABASE_URL. On Replit prefer SUPABASE_URL (DATABASE_URL is often reserved). Or unset TOMORROWOS_STORE to use SQLite.`);
20
20
  }
21
21
  return new PostgresStore({
22
22
  connectionString: databaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
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",
@@ -19,6 +19,7 @@
19
19
  "templates",
20
20
  "README.md",
21
21
  "LLM_PROMPT.md",
22
+ "REPLIT_SETUP.md",
22
23
  "BUILD_GUARDRAILS.md",
23
24
  "PLAYER_INSTALL.md",
24
25
  "brand.schema.json",
@@ -1,12 +1,19 @@
1
- # Supabase/Postgres option.
1
+ # Default: SQLite (good for ≤5 screens / demos).
2
+ # TOMORROWOS_STORE=sqlite
3
+ # TOMORROWOS_DB_PATH=./data/tomorrowos.db
4
+
5
+ # Supabase/Postgres (recommended for >5 screens).
6
+ # On Replit use SUPABASE_URL — DATABASE_URL is often a reserved Secret.
2
7
  # TOMORROWOS_STORE=supabase
3
- # DATABASE_URL=postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres
8
+ # SUPABASE_URL=postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres
4
9
  # DATABASE_SSL=true
10
+ # DATABASE_URL=postgresql://... # optional fallback outside Replit
5
11
 
6
12
  # Optional: override the default deterministic pairing-code secret.
7
13
  # PAIRING_CODE_SECRET=
8
14
 
9
- # Optional: upload media to Cloudinary instead of local storage.
15
+ # Optional: upload media to Cloudinary instead of local / Object Storage.
10
16
  # CLOUDINARY_CLOUD_NAME=
11
17
  # CLOUDINARY_API_KEY=
12
18
  # CLOUDINARY_API_SECRET=
19
+ # CLOUDINARY_FOLDER=tomorrowos
@@ -16,7 +16,7 @@ startup.
16
16
 
17
17
  ## Choose A Database
18
18
 
19
- Use `.env` to select the database.
19
+ Use `.env` (or Replit Secrets) to select the database.
20
20
 
21
21
  Default local SQLite:
22
22
 
@@ -29,10 +29,14 @@ Supabase/Postgres:
29
29
 
30
30
  ```env
31
31
  TOMORROWOS_STORE=supabase
32
- DATABASE_URL=postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres
32
+ SUPABASE_URL=postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres
33
33
  DATABASE_SSL=true
34
+ # DATABASE_URL=... # optional fallback; on Replit prefer SUPABASE_URL
34
35
  ```
35
36
 
37
+ On Replit, follow the package root file **`REPLIT_SETUP.md`** for a guided Agent
38
+ setup (screen count → SQLite vs Supabase → Cloudinary vs Object Storage → brand).
39
+
36
40
  Throwaway in-memory store for tests only:
37
41
 
38
42
  ```env
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
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",
@@ -10,7 +10,7 @@
10
10
  "build-player": "tomorrowos build --platform tizen"
11
11
  },
12
12
  "dependencies": {
13
- "@tomorrowos/sdk": "^0.7.3",
13
+ "@tomorrowos/sdk": "^0.7.5",
14
14
  "dotenv": "^17.2.3"
15
15
  },
16
16
  "devDependencies": {
@@ -103,6 +103,7 @@
103
103
  <h2>Assets</h2>
104
104
  <button type="button" class="primary" id="addAssetBtn" title="Add image or video">+</button>
105
105
  </div>
106
+ <p class="hint playlist-assets-hint">Drag ⋮⋮ to reorder assets. Save playlist to keep the new order.</p>
106
107
  <input
107
108
  type="file"
108
109
  id="fileInput"
@@ -15,6 +15,9 @@ let playlistDraftActive = false;
15
15
  /** @type {{ id: string, assetId?: string, url: string, name: string, type: string, durationMs: number }[]} */
16
16
  let editorItems = [];
17
17
 
18
+ /** Item id currently being dragged in the asset list (reorder). */
19
+ let draggingEditorItemId = null;
20
+
18
21
  /** @type {string|null} */
19
22
  let publishModalDeviceId = null;
20
23
  let publishInProgress = false;
@@ -670,9 +673,75 @@ function loadEditorFromSelection() {
670
673
  updatePlaylistEditorVisibility();
671
674
  }
672
675
 
676
+ function clearPlaylistItemDropTargets() {
677
+ document
678
+ .querySelectorAll(".playlist-item--drop-target, .playlist-item--dragging")
679
+ .forEach((el) => {
680
+ el.classList.remove("playlist-item--drop-target", "playlist-item--dragging");
681
+ });
682
+ }
683
+
684
+ function reorderEditorItems(fromId, toId) {
685
+ const fromIdx = editorItems.findIndex((x) => x.id === fromId);
686
+ const toIdx = editorItems.findIndex((x) => x.id === toId);
687
+ if (fromIdx < 0 || toIdx < 0 || fromIdx === toIdx) return;
688
+ const [moved] = editorItems.splice(fromIdx, 1);
689
+ editorItems.splice(toIdx, 0, moved);
690
+ renderEditorAssets();
691
+ }
692
+
693
+ function attachPlaylistItemDragDrop(li, item) {
694
+ li.dataset.itemId = item.id;
695
+
696
+ const handle = document.createElement("button");
697
+ handle.type = "button";
698
+ handle.className = "playlist-item-drag-handle";
699
+ handle.setAttribute("aria-label", "Drag to reorder");
700
+ handle.title = "Drag to reorder";
701
+ handle.textContent = "⋮⋮";
702
+ handle.draggable = true;
703
+
704
+ handle.addEventListener("dragstart", (e) => {
705
+ e.dataTransfer.setData("text/plain", item.id);
706
+ e.dataTransfer.effectAllowed = "move";
707
+ draggingEditorItemId = item.id;
708
+ li.classList.add("playlist-item--dragging");
709
+ });
710
+
711
+ handle.addEventListener("dragend", () => {
712
+ draggingEditorItemId = null;
713
+ clearPlaylistItemDropTargets();
714
+ });
715
+
716
+ li.addEventListener("dragover", (e) => {
717
+ e.preventDefault();
718
+ e.dataTransfer.dropEffect = "move";
719
+ if (draggingEditorItemId && draggingEditorItemId !== item.id) {
720
+ li.classList.add("playlist-item--drop-target");
721
+ }
722
+ });
723
+
724
+ li.addEventListener("dragleave", (e) => {
725
+ if (!li.contains(e.relatedTarget)) {
726
+ li.classList.remove("playlist-item--drop-target");
727
+ }
728
+ });
729
+
730
+ li.addEventListener("drop", (e) => {
731
+ e.preventDefault();
732
+ li.classList.remove("playlist-item--drop-target");
733
+ const fromId = e.dataTransfer.getData("text/plain") || draggingEditorItemId;
734
+ if (!fromId || fromId === item.id) return;
735
+ reorderEditorItems(fromId, item.id);
736
+ });
737
+
738
+ li.insertBefore(handle, li.firstChild);
739
+ }
740
+
673
741
  function renderEditorAssets() {
674
742
  const list = document.getElementById("playlistList");
675
743
  const empty = document.getElementById("playlistEmpty");
744
+ draggingEditorItemId = null;
676
745
  list.querySelectorAll(".playlist-item").forEach((el) => el.remove());
677
746
 
678
747
  if (!isPlaylistEditorOpen()) {
@@ -696,6 +765,7 @@ function renderEditorAssets() {
696
765
  if (item.type === "image" || item.type === "video") {
697
766
  const thumb = document.createElement(item.type === "video" ? "video" : "img");
698
767
  thumb.className = "playlist-item-thumb";
768
+ thumb.draggable = false;
699
769
  try {
700
770
  thumb.src = absoluteMediaUrl(item.url);
701
771
  } catch {
@@ -752,6 +822,7 @@ function renderEditorAssets() {
752
822
  li.appendChild(name);
753
823
  li.appendChild(meta);
754
824
  li.appendChild(actions);
825
+ attachPlaylistItemDragDrop(li, item);
755
826
  list.appendChild(li);
756
827
  }
757
828
  }
@@ -429,12 +429,52 @@ button.danger {
429
429
  font-size: 0.875rem;
430
430
  }
431
431
 
432
+ .playlist-assets-hint {
433
+ margin: 0 0 0.75rem;
434
+ font-size: 0.78rem;
435
+ }
436
+
432
437
  .playlist-item {
433
438
  border: 1px solid #e4e1dc;
434
439
  border-radius: 8px;
435
440
  padding: 0.65rem;
436
441
  margin-bottom: 0.5rem;
437
442
  background: #fafaf9;
443
+ position: relative;
444
+ }
445
+
446
+ .playlist-item--dragging {
447
+ opacity: 0.55;
448
+ }
449
+
450
+ .playlist-item--drop-target {
451
+ border-color: #ff8a3d;
452
+ box-shadow: 0 0 0 2px rgba(255, 138, 61, 0.25);
453
+ }
454
+
455
+ .playlist-item-drag-handle {
456
+ display: block;
457
+ width: 100%;
458
+ margin: 0 0 0.45rem;
459
+ padding: 0.2rem 0;
460
+ border: 0;
461
+ border-radius: 4px;
462
+ background: transparent;
463
+ color: #888;
464
+ font-size: 0.95rem;
465
+ line-height: 1;
466
+ letter-spacing: 0.08em;
467
+ cursor: grab;
468
+ touch-action: none;
469
+ }
470
+
471
+ .playlist-item-drag-handle:active {
472
+ cursor: grabbing;
473
+ }
474
+
475
+ .playlist-item-drag-handle:hover {
476
+ color: #444;
477
+ background: #f0eeea;
438
478
  }
439
479
 
440
480
  .playlist-item-thumb {
@@ -12,6 +12,8 @@ import { createTomorrowOSStore, TomorrowOS } from "@tomorrowos/sdk";
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
13
  const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
14
14
  const store = createTomorrowOSStore({
15
+ // On Replit prefer SUPABASE_URL — DATABASE_URL is often a reserved Secret.
16
+ databaseUrl: process.env.SUPABASE_URL || process.env.DATABASE_URL,
15
17
  sqlitePath: join(__dirname, "data", "tomorrowos.db")
16
18
  });
17
19