@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 +2 -0
- package/README.md +25 -8
- package/REPLIT_SETUP.md +341 -0
- package/dist/store/factory.d.ts +5 -1
- package/dist/store/factory.d.ts.map +1 -1
- package/dist/store/factory.js +2 -2
- package/package.json +2 -1
- package/templates/cms-starter/.env.example +10 -3
- package/templates/cms-starter/README.md +6 -2
- package/templates/cms-starter/package.json +2 -2
- package/templates/cms-starter/public/index.html +1 -0
- package/templates/cms-starter/public/methods.js +71 -0
- package/templates/cms-starter/public/panel.css +40 -0
- package/templates/cms-starter/server.ts +2 -0
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. **
|
|
14
|
-
2. **
|
|
15
|
-
3. **Read `
|
|
16
|
-
4.
|
|
17
|
-
5.
|
|
18
|
-
6. **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/REPLIT_SETUP.md
ADDED
|
@@ -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`.
|
package/dist/store/factory.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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"}
|
package/dist/store/factory.js
CHANGED
|
@@ -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.
|
|
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
|
+
"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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
+
"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.
|
|
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
|
|