@tomorrowos/sdk 0.2.5 → 0.3.0
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/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/playlist-catalog.d.ts +35 -0
- package/dist/playlist-catalog.d.ts.map +1 -0
- package/dist/playlist-catalog.js +133 -0
- package/dist/store/memory-store.d.ts +9 -1
- package/dist/store/memory-store.d.ts.map +1 -1
- package/dist/store/memory-store.js +35 -0
- package/dist/store/types.d.ts +41 -0
- package/dist/store/types.d.ts.map +1 -1
- package/dist/tomorrowos.d.ts +14 -0
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +136 -0
- package/package.json +1 -1
- package/templates/cms-starter/public/index.html +42 -14
- package/templates/cms-starter/public/methods.js +451 -372
- package/templates/cms-starter/public/panel.css +130 -1
- package/templates/cms-starter/server.ts +7 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { TomorrowOS } from "./tomorrowos.js";
|
|
2
2
|
export type { DeviceListItem, ListenOptions, TomorrowOSBrand, TomorrowOSOptions } from "./tomorrowos.js";
|
|
3
|
-
export type { DeviceRegistryRecord, PairedDeviceRecord, PendingCodeRecord, TomorrowOSStore } from "./store/types.js";
|
|
3
|
+
export type { DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceRecord, PendingCodeRecord, PlaylistItemRecord, PlaylistSchedule, PublishedPlaylistSnapshot, StoredPlaylist, TomorrowOSStore } from "./store/types.js";
|
|
4
|
+
export { PlaylistCatalog } from "./playlist-catalog.js";
|
|
5
|
+
export type { BuiltDevicePolicy, SavePlaylistInput } from "./playlist-catalog.js";
|
|
4
6
|
export { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH } from "./pairing-code.js";
|
|
5
7
|
export { MemoryStore } from "./store/memory-store.js";
|
|
6
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { TomorrowOS } from "./tomorrowos.js";
|
|
2
|
+
export { PlaylistCatalog } from "./playlist-catalog.js";
|
|
2
3
|
export { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH } from "./pairing-code.js";
|
|
3
4
|
export { MemoryStore } from "./store/memory-store.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DevicePlaylistAssignment, PlaylistItemRecord, PlaylistSchedule, PublishedPlaylistSnapshot, StoredPlaylist, TomorrowOSStore } from "./store/types.js";
|
|
2
|
+
export interface SavePlaylistInput {
|
|
3
|
+
id?: string;
|
|
4
|
+
name: string;
|
|
5
|
+
schedule?: PlaylistSchedule;
|
|
6
|
+
items: PlaylistItemRecord[];
|
|
7
|
+
}
|
|
8
|
+
export interface BuiltDevicePolicy {
|
|
9
|
+
policy: {
|
|
10
|
+
playlists: PublishedPlaylistSnapshot[];
|
|
11
|
+
fallback: {
|
|
12
|
+
type: "brand";
|
|
13
|
+
};
|
|
14
|
+
revision?: number;
|
|
15
|
+
syncMode?: "latest" | "snapshot";
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare class PlaylistCatalog {
|
|
19
|
+
private readonly store;
|
|
20
|
+
constructor(store: TomorrowOSStore);
|
|
21
|
+
listPlaylists(): Promise<StoredPlaylist[]>;
|
|
22
|
+
listPlaylistsIncludingRetired(): Promise<StoredPlaylist[]>;
|
|
23
|
+
getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
|
|
24
|
+
savePlaylist(input: SavePlaylistInput): Promise<StoredPlaylist>;
|
|
25
|
+
retirePlaylist(id: string): Promise<StoredPlaylist>;
|
|
26
|
+
getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
|
|
27
|
+
publishPlaylistsToDevice(deviceId: string, playlistIds: string[]): Promise<BuiltDevicePolicy>;
|
|
28
|
+
removePlaylistFromDevice(deviceId: string, playlistId: string): Promise<BuiltDevicePolicy>;
|
|
29
|
+
buildPolicyForDevice(deviceId: string, options?: {
|
|
30
|
+
useLatest?: boolean;
|
|
31
|
+
}): Promise<BuiltDevicePolicy>;
|
|
32
|
+
private buildPolicyFromAssignments;
|
|
33
|
+
canPublishPlaylistToNewDevice(playlistId: string): Promise<boolean>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=playlist-catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playlist-catalog.d.ts","sourceRoot":"","sources":["../src/playlist-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE;QACN,SAAS,EAAE,yBAAyB,EAAE,CAAC;QACvC,QAAQ,EAAE;YAAE,IAAI,EAAE,OAAO,CAAA;SAAE,CAAC;QAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;KAClC,CAAC;CACH;AAYD,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,eAAe;IAE7C,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAK1C,6BAA6B,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IAgC/D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAcnD,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAIhC,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,iBAAiB,CAAC;IA8BvB,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC;IAOvB,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO,GACpC,OAAO,CAAC,iBAAiB,CAAC;YAKf,0BAA0B;IA+BxC,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGpE"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
function cloneSnapshot(playlist) {
|
|
3
|
+
return {
|
|
4
|
+
id: playlist.id,
|
|
5
|
+
name: playlist.name,
|
|
6
|
+
version: playlist.version,
|
|
7
|
+
schedule: playlist.schedule ? { ...playlist.schedule } : undefined,
|
|
8
|
+
items: playlist.items.map((item) => ({ ...item }))
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export class PlaylistCatalog {
|
|
12
|
+
store;
|
|
13
|
+
constructor(store) {
|
|
14
|
+
this.store = store;
|
|
15
|
+
}
|
|
16
|
+
async listPlaylists() {
|
|
17
|
+
const all = await this.store.listPlaylists();
|
|
18
|
+
return all.filter((p) => !p.retired);
|
|
19
|
+
}
|
|
20
|
+
async listPlaylistsIncludingRetired() {
|
|
21
|
+
return this.store.listPlaylists();
|
|
22
|
+
}
|
|
23
|
+
async getPlaylist(id) {
|
|
24
|
+
return this.store.getPlaylist(id);
|
|
25
|
+
}
|
|
26
|
+
async savePlaylist(input) {
|
|
27
|
+
const name = String(input.name || "").trim();
|
|
28
|
+
if (!name) {
|
|
29
|
+
throw Object.assign(new Error("Playlist name is required"), {
|
|
30
|
+
code: "PLAYLIST_INVALID"
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const id = input.id?.trim() || randomUUID();
|
|
34
|
+
const taken = await this.store.isPlaylistNameTaken(name, id);
|
|
35
|
+
if (taken) {
|
|
36
|
+
throw Object.assign(new Error("Playlist name already exists"), {
|
|
37
|
+
code: "PLAYLIST_NAME_CONFLICT"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const existing = await this.store.getPlaylist(id);
|
|
41
|
+
const now = new Date().toISOString();
|
|
42
|
+
const record = {
|
|
43
|
+
id,
|
|
44
|
+
name,
|
|
45
|
+
schedule: input.schedule,
|
|
46
|
+
items: Array.isArray(input.items) ? input.items : [],
|
|
47
|
+
version: (existing?.version ?? 0) + 1,
|
|
48
|
+
updatedAt: now,
|
|
49
|
+
retired: false
|
|
50
|
+
};
|
|
51
|
+
await this.store.setPlaylist(record);
|
|
52
|
+
return record;
|
|
53
|
+
}
|
|
54
|
+
async retirePlaylist(id) {
|
|
55
|
+
const existing = await this.store.getPlaylist(id);
|
|
56
|
+
if (!existing) {
|
|
57
|
+
throw Object.assign(new Error("Playlist not found"), { code: "PLAYLIST_NOT_FOUND" });
|
|
58
|
+
}
|
|
59
|
+
const record = {
|
|
60
|
+
...existing,
|
|
61
|
+
retired: true,
|
|
62
|
+
retiredAt: new Date().toISOString()
|
|
63
|
+
};
|
|
64
|
+
await this.store.setPlaylist(record);
|
|
65
|
+
return record;
|
|
66
|
+
}
|
|
67
|
+
async getDeviceAssignments(deviceId) {
|
|
68
|
+
return this.store.getDeviceAssignments(deviceId);
|
|
69
|
+
}
|
|
70
|
+
async publishPlaylistsToDevice(deviceId, playlistIds) {
|
|
71
|
+
const ids = [...new Set(playlistIds.map((x) => String(x).trim()).filter(Boolean))];
|
|
72
|
+
if (ids.length === 0) {
|
|
73
|
+
throw Object.assign(new Error("Select at least one playlist"), {
|
|
74
|
+
code: "PLAYLIST_INVALID"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const assignments = [];
|
|
78
|
+
for (const playlistId of ids) {
|
|
79
|
+
const playlist = await this.store.getPlaylist(playlistId);
|
|
80
|
+
if (!playlist || playlist.retired) {
|
|
81
|
+
throw Object.assign(new Error(`Playlist not available: ${playlistId}`), {
|
|
82
|
+
code: "PLAYLIST_NOT_FOUND"
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
assignments.push({
|
|
86
|
+
playlistId,
|
|
87
|
+
publishedVersion: playlist.version,
|
|
88
|
+
publishedAt: new Date().toISOString(),
|
|
89
|
+
snapshot: cloneSnapshot(playlist)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
await this.store.setDeviceAssignments(deviceId, assignments);
|
|
93
|
+
return this.buildPolicyFromAssignments(assignments, { useLatest: false });
|
|
94
|
+
}
|
|
95
|
+
async removePlaylistFromDevice(deviceId, playlistId) {
|
|
96
|
+
const assignments = await this.store.getDeviceAssignments(deviceId);
|
|
97
|
+
const next = assignments.filter((a) => a.playlistId !== playlistId);
|
|
98
|
+
await this.store.setDeviceAssignments(deviceId, next);
|
|
99
|
+
return this.buildPolicyFromAssignments(next, { useLatest: false });
|
|
100
|
+
}
|
|
101
|
+
async buildPolicyForDevice(deviceId, options = {}) {
|
|
102
|
+
const assignments = await this.store.getDeviceAssignments(deviceId);
|
|
103
|
+
return this.buildPolicyFromAssignments(assignments, options);
|
|
104
|
+
}
|
|
105
|
+
async buildPolicyFromAssignments(assignments, options) {
|
|
106
|
+
const useLatest = options.useLatest === true;
|
|
107
|
+
const playlists = [];
|
|
108
|
+
for (const assignment of assignments) {
|
|
109
|
+
if (useLatest) {
|
|
110
|
+
const current = await this.store.getPlaylist(assignment.playlistId);
|
|
111
|
+
if (current && !current.retired) {
|
|
112
|
+
playlists.push(cloneSnapshot(current));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
playlists.push({
|
|
117
|
+
...assignment.snapshot,
|
|
118
|
+
items: assignment.snapshot.items.map((item) => ({ ...item }))
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
policy: {
|
|
123
|
+
playlists,
|
|
124
|
+
fallback: { type: "brand" },
|
|
125
|
+
revision: Date.now(),
|
|
126
|
+
syncMode: useLatest ? "latest" : "snapshot"
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
canPublishPlaylistToNewDevice(playlistId) {
|
|
131
|
+
return this.store.getPlaylist(playlistId).then((p) => !!p && !p?.retired);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeRecord, TomorrowOSStore } from "./types.js";
|
|
1
|
+
import type { DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeRecord, StoredPlaylist, TomorrowOSStore } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Default store for development / single-node demos.
|
|
4
4
|
* Data is lost on process restart — not for multi-instance production.
|
|
@@ -8,6 +8,8 @@ export declare class MemoryStore implements TomorrowOSStore {
|
|
|
8
8
|
private readonly deviceRegistry;
|
|
9
9
|
private readonly codeToDeviceId;
|
|
10
10
|
private readonly pairedDevices;
|
|
11
|
+
private readonly playlists;
|
|
12
|
+
private readonly deviceAssignments;
|
|
11
13
|
setPendingCode(code: string, record: PendingCodeRecord): Promise<void>;
|
|
12
14
|
getPendingCode(code: string): Promise<PendingCodeRecord | undefined>;
|
|
13
15
|
deletePendingCode(code: string): Promise<void>;
|
|
@@ -21,5 +23,11 @@ export declare class MemoryStore implements TomorrowOSStore {
|
|
|
21
23
|
getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined>;
|
|
22
24
|
deletePairedDevice(deviceId: string): Promise<void>;
|
|
23
25
|
listPairedDevices(): Promise<PairedDeviceEntry[]>;
|
|
26
|
+
listPlaylists(): Promise<StoredPlaylist[]>;
|
|
27
|
+
getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
|
|
28
|
+
setPlaylist(record: StoredPlaylist): Promise<void>;
|
|
29
|
+
isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
|
|
30
|
+
getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
|
|
31
|
+
setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
|
|
24
32
|
}
|
|
25
33
|
//# sourceMappingURL=memory-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;IACrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2C;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;
|
|
1
|
+
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;IACrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2C;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;IACvE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;IAC/D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAiD;IAE7E,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAItC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IASV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAQpE,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAIV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAIpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAOjD,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUvE,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAIhC,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,wBAAwB,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC;CASjB"}
|
|
@@ -7,6 +7,8 @@ export class MemoryStore {
|
|
|
7
7
|
deviceRegistry = new Map();
|
|
8
8
|
codeToDeviceId = new Map();
|
|
9
9
|
pairedDevices = new Map();
|
|
10
|
+
playlists = new Map();
|
|
11
|
+
deviceAssignments = new Map();
|
|
10
12
|
async setPendingCode(code, record) {
|
|
11
13
|
this.pendingCodes.set(code, record);
|
|
12
14
|
}
|
|
@@ -51,4 +53,37 @@ export class MemoryStore {
|
|
|
51
53
|
record
|
|
52
54
|
}));
|
|
53
55
|
}
|
|
56
|
+
async listPlaylists() {
|
|
57
|
+
return [...this.playlists.values()];
|
|
58
|
+
}
|
|
59
|
+
async getPlaylist(id) {
|
|
60
|
+
return this.playlists.get(id);
|
|
61
|
+
}
|
|
62
|
+
async setPlaylist(record) {
|
|
63
|
+
this.playlists.set(record.id, record);
|
|
64
|
+
}
|
|
65
|
+
async isPlaylistNameTaken(name, excludeId) {
|
|
66
|
+
const target = name.trim().toLowerCase();
|
|
67
|
+
for (const playlist of this.playlists.values()) {
|
|
68
|
+
if (playlist.retired)
|
|
69
|
+
continue;
|
|
70
|
+
if (excludeId && playlist.id === excludeId)
|
|
71
|
+
continue;
|
|
72
|
+
if (playlist.name.trim().toLowerCase() === target)
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
async getDeviceAssignments(deviceId) {
|
|
78
|
+
return [...(this.deviceAssignments.get(deviceId) ?? [])];
|
|
79
|
+
}
|
|
80
|
+
async setDeviceAssignments(deviceId, assignments) {
|
|
81
|
+
this.deviceAssignments.set(deviceId, assignments.map((a) => ({
|
|
82
|
+
...a,
|
|
83
|
+
snapshot: {
|
|
84
|
+
...a.snapshot,
|
|
85
|
+
items: a.snapshot.items.map((item) => ({ ...item }))
|
|
86
|
+
}
|
|
87
|
+
})));
|
|
88
|
+
}
|
|
54
89
|
}
|
package/dist/store/types.d.ts
CHANGED
|
@@ -29,6 +29,41 @@ export interface PairedDeviceEntry {
|
|
|
29
29
|
deviceId: string;
|
|
30
30
|
record: PairedDeviceRecord;
|
|
31
31
|
}
|
|
32
|
+
export interface PlaylistItemRecord {
|
|
33
|
+
url: string;
|
|
34
|
+
type?: string;
|
|
35
|
+
durationMs?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface PlaylistSchedule {
|
|
38
|
+
startDate?: string;
|
|
39
|
+
endDate?: string;
|
|
40
|
+
daysOfWeek?: number[];
|
|
41
|
+
start?: string;
|
|
42
|
+
end?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface StoredPlaylist {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
schedule?: PlaylistSchedule;
|
|
48
|
+
items: PlaylistItemRecord[];
|
|
49
|
+
version: number;
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
retired?: boolean;
|
|
52
|
+
retiredAt?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface PublishedPlaylistSnapshot {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
version: number;
|
|
58
|
+
schedule?: PlaylistSchedule;
|
|
59
|
+
items: PlaylistItemRecord[];
|
|
60
|
+
}
|
|
61
|
+
export interface DevicePlaylistAssignment {
|
|
62
|
+
playlistId: string;
|
|
63
|
+
publishedVersion: number;
|
|
64
|
+
publishedAt: string;
|
|
65
|
+
snapshot: PublishedPlaylistSnapshot;
|
|
66
|
+
}
|
|
32
67
|
/**
|
|
33
68
|
* Implement with Postgres, Redis, etc. Defaults to MemoryStore (Map).
|
|
34
69
|
* All methods async so DB backends do not need sync adapters.
|
|
@@ -47,5 +82,11 @@ export interface TomorrowOSStore {
|
|
|
47
82
|
getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined>;
|
|
48
83
|
deletePairedDevice(deviceId: string): Promise<void>;
|
|
49
84
|
listPairedDevices(): Promise<PairedDeviceEntry[]>;
|
|
85
|
+
listPlaylists(): Promise<StoredPlaylist[]>;
|
|
86
|
+
getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
|
|
87
|
+
setPlaylist(record: StoredPlaylist): Promise<void>;
|
|
88
|
+
isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
|
|
89
|
+
getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
|
|
90
|
+
setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
|
|
50
91
|
}
|
|
51
92
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wFAAwF;AACxF,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IACrE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAAC;IAC/E,iBAAiB,CACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,uBAAuB,CACrB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;IAC3E,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;IAC3E,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
|
package/dist/tomorrowos.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
2
|
import http from "http";
|
|
3
|
+
import { PlaylistCatalog, type BuiltDevicePolicy } from "./playlist-catalog.js";
|
|
3
4
|
import type { TomorrowOSStore } from "./store/types.js";
|
|
4
5
|
export interface TomorrowOSBrand {
|
|
5
6
|
name?: string;
|
|
@@ -45,10 +46,17 @@ export interface DeviceListItem {
|
|
|
45
46
|
screenOnlineLabel: string;
|
|
46
47
|
/** Same as lastBootAt while connected; used for uptime display. */
|
|
47
48
|
screenOnlineSince: string | null;
|
|
49
|
+
publishedPlaylists: Array<{
|
|
50
|
+
playlistId: string;
|
|
51
|
+
name: string;
|
|
52
|
+
version: number;
|
|
53
|
+
publishedAt: string;
|
|
54
|
+
}>;
|
|
48
55
|
}
|
|
49
56
|
export declare class TomorrowOS extends EventEmitter {
|
|
50
57
|
readonly brand: TomorrowOSBrand;
|
|
51
58
|
private readonly store;
|
|
59
|
+
readonly playlists: PlaylistCatalog;
|
|
52
60
|
private readonly devices;
|
|
53
61
|
private readonly pendingDeviceMeta;
|
|
54
62
|
private httpServer;
|
|
@@ -56,6 +64,12 @@ export declare class TomorrowOS extends EventEmitter {
|
|
|
56
64
|
private staticRoot;
|
|
57
65
|
private staticIndexFile;
|
|
58
66
|
constructor(options: TomorrowOSOptions);
|
|
67
|
+
/** Push all device assignments using latest playlist definitions (e.g. after reboot). */
|
|
68
|
+
pushLatestPolicyToDevice(deviceId: string): Promise<{
|
|
69
|
+
pushed: boolean;
|
|
70
|
+
policy?: BuiltDevicePolicy["policy"];
|
|
71
|
+
}>;
|
|
72
|
+
private sendDeviceCommand;
|
|
59
73
|
/** Verify a 6-character alphanumeric pairing code (POST /pairing/verify). */
|
|
60
74
|
pairingVerify(code: string): Promise<{
|
|
61
75
|
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;AAUxB,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
|
package/dist/tomorrowos.js
CHANGED
|
@@ -5,6 +5,7 @@ import http from "http";
|
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { WebSocket, WebSocketServer } from "ws";
|
|
7
7
|
import { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode } from "./pairing-code.js";
|
|
8
|
+
import { PlaylistCatalog } from "./playlist-catalog.js";
|
|
8
9
|
import { MemoryStore } from "./store/memory-store.js";
|
|
9
10
|
function formatDurationMs(ms) {
|
|
10
11
|
if (!Number.isFinite(ms) || ms < 0)
|
|
@@ -80,6 +81,7 @@ function parseDevicePath(pathname) {
|
|
|
80
81
|
export class TomorrowOS extends EventEmitter {
|
|
81
82
|
brand;
|
|
82
83
|
store;
|
|
84
|
+
playlists;
|
|
83
85
|
devices = new Map();
|
|
84
86
|
pendingDeviceMeta = new Map();
|
|
85
87
|
httpServer = null;
|
|
@@ -90,6 +92,31 @@ export class TomorrowOS extends EventEmitter {
|
|
|
90
92
|
super();
|
|
91
93
|
this.brand = options.brand;
|
|
92
94
|
this.store = options.store ?? new MemoryStore();
|
|
95
|
+
this.playlists = new PlaylistCatalog(this.store);
|
|
96
|
+
}
|
|
97
|
+
/** Push all device assignments using latest playlist definitions (e.g. after reboot). */
|
|
98
|
+
async pushLatestPolicyToDevice(deviceId) {
|
|
99
|
+
const id = String(deviceId || "").trim();
|
|
100
|
+
const assignments = await this.store.getDeviceAssignments(id);
|
|
101
|
+
if (assignments.length === 0)
|
|
102
|
+
return { pushed: false };
|
|
103
|
+
const built = await this.playlists.buildPolicyForDevice(id, { useLatest: true });
|
|
104
|
+
const ws = this.devices.get(id);
|
|
105
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
106
|
+
return { pushed: false, policy: built.policy };
|
|
107
|
+
}
|
|
108
|
+
await this.sendDeviceCommand(id, "device.content.setPolicy", {
|
|
109
|
+
policy: built.policy
|
|
110
|
+
});
|
|
111
|
+
await this.recordPolicyPush(id);
|
|
112
|
+
return { pushed: true, policy: built.policy };
|
|
113
|
+
}
|
|
114
|
+
async sendDeviceCommand(deviceId, method, params) {
|
|
115
|
+
const ws = this.devices.get(deviceId);
|
|
116
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
117
|
+
throw Object.assign(new Error("Device not connected"), { code: "DEVICE_OFFLINE" });
|
|
118
|
+
}
|
|
119
|
+
return this.sendCommandToSocket(ws, deviceId, method, params);
|
|
93
120
|
}
|
|
94
121
|
/** Verify a 6-character alphanumeric pairing code (POST /pairing/verify). */
|
|
95
122
|
async pairingVerify(code) {
|
|
@@ -175,6 +202,7 @@ export class TomorrowOS extends EventEmitter {
|
|
|
175
202
|
return Promise.all(entries.map(async ({ deviceId, record }) => {
|
|
176
203
|
const connected = this.isDeviceConnected(deviceId);
|
|
177
204
|
const reg = await this.store.getDeviceRegistry(deviceId);
|
|
205
|
+
const assignments = await this.store.getDeviceAssignments(deviceId);
|
|
178
206
|
let screenOnlineActive = false;
|
|
179
207
|
let screenOnlineLabel = "Not active";
|
|
180
208
|
if (connected && record.lastBootAt) {
|
|
@@ -188,6 +216,12 @@ export class TomorrowOS extends EventEmitter {
|
|
|
188
216
|
return {
|
|
189
217
|
deviceId,
|
|
190
218
|
pairingCode: reg?.permanentPairingCode ?? null,
|
|
219
|
+
publishedPlaylists: assignments.map((a) => ({
|
|
220
|
+
playlistId: a.playlistId,
|
|
221
|
+
name: a.snapshot.name,
|
|
222
|
+
version: a.publishedVersion,
|
|
223
|
+
publishedAt: a.publishedAt
|
|
224
|
+
})),
|
|
191
225
|
connected,
|
|
192
226
|
deviceName: record.deviceName ?? null,
|
|
193
227
|
platform: record.platform ?? null,
|
|
@@ -550,6 +584,105 @@ export class TomorrowOS extends EventEmitter {
|
|
|
550
584
|
sendJson(res, 200, { status: "success", devices });
|
|
551
585
|
return;
|
|
552
586
|
}
|
|
587
|
+
if (req.method === "GET" && pathname === "/playlists") {
|
|
588
|
+
const playlists = await this.playlists.listPlaylists();
|
|
589
|
+
sendJson(res, 200, { status: "success", playlists });
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (req.method === "POST" && pathname === "/playlists") {
|
|
593
|
+
const body = (await readJsonBody(req));
|
|
594
|
+
try {
|
|
595
|
+
const saved = await this.playlists.savePlaylist({
|
|
596
|
+
id: typeof body.id === "string" ? body.id : undefined,
|
|
597
|
+
name: String(body.name ?? ""),
|
|
598
|
+
schedule: body.schedule && typeof body.schedule === "object"
|
|
599
|
+
? body.schedule
|
|
600
|
+
: undefined,
|
|
601
|
+
items: Array.isArray(body.items) ? body.items : []
|
|
602
|
+
});
|
|
603
|
+
sendJson(res, 200, { status: "success", playlist: saved });
|
|
604
|
+
}
|
|
605
|
+
catch (e) {
|
|
606
|
+
const msg = e instanceof Error ? e.message : "Save failed";
|
|
607
|
+
sendJson(res, 400, { status: "failed", error: msg });
|
|
608
|
+
}
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const playlistDelete = /^\/playlists\/([^/]+)$/.exec(pathname);
|
|
612
|
+
if (req.method === "DELETE" && playlistDelete) {
|
|
613
|
+
const playlistId = decodeURIComponent(playlistDelete[1]);
|
|
614
|
+
try {
|
|
615
|
+
const retired = await this.playlists.retirePlaylist(playlistId);
|
|
616
|
+
sendJson(res, 200, { status: "success", playlist: retired });
|
|
617
|
+
}
|
|
618
|
+
catch (e) {
|
|
619
|
+
const msg = e instanceof Error ? e.message : "Delete failed";
|
|
620
|
+
sendJson(res, 400, { status: "failed", error: msg });
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const deviceAssignmentsGet = /^\/device\/([^/]+)\/assignments$/.exec(pathname);
|
|
625
|
+
if (req.method === "GET" && deviceAssignmentsGet) {
|
|
626
|
+
const deviceId = decodeURIComponent(deviceAssignmentsGet[1]);
|
|
627
|
+
const assignments = await this.playlists.getDeviceAssignments(deviceId);
|
|
628
|
+
sendJson(res, 200, { status: "success", assignments });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const deviceAssignmentsPost = /^\/device\/([^/]+)\/assignments$/.exec(pathname);
|
|
632
|
+
if (req.method === "POST" && deviceAssignmentsPost) {
|
|
633
|
+
const deviceId = decodeURIComponent(deviceAssignmentsPost[1]);
|
|
634
|
+
const body = (await readJsonBody(req));
|
|
635
|
+
const ws = this.devices.get(deviceId);
|
|
636
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
637
|
+
sendJson(res, 404, { status: "failed", error: "Device not connected" });
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
try {
|
|
641
|
+
let built;
|
|
642
|
+
if (body.useLatest === true) {
|
|
643
|
+
built = await this.playlists.buildPolicyForDevice(deviceId, {
|
|
644
|
+
useLatest: true
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
const ids = Array.isArray(body.playlistIds) ? body.playlistIds : [];
|
|
649
|
+
built = await this.playlists.publishPlaylistsToDevice(deviceId, ids);
|
|
650
|
+
}
|
|
651
|
+
const result = await this.sendDeviceCommand(deviceId, "device.content.setPolicy", {
|
|
652
|
+
policy: built.policy
|
|
653
|
+
});
|
|
654
|
+
await this.recordPolicyPush(deviceId);
|
|
655
|
+
sendJson(res, 200, { status: "success", policy: built.policy, result });
|
|
656
|
+
}
|
|
657
|
+
catch (e) {
|
|
658
|
+
const msg = e instanceof Error ? e.message : "Publish failed";
|
|
659
|
+
sendJson(res, 400, { status: "failed", error: msg });
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const deviceAssignmentDelete = /^\/device\/([^/]+)\/assignments\/([^/]+)$/.exec(pathname);
|
|
664
|
+
if (req.method === "DELETE" && deviceAssignmentDelete) {
|
|
665
|
+
const deviceId = decodeURIComponent(deviceAssignmentDelete[1]);
|
|
666
|
+
const playlistId = decodeURIComponent(deviceAssignmentDelete[2]);
|
|
667
|
+
const ws = this.devices.get(deviceId);
|
|
668
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
669
|
+
sendJson(res, 404, { status: "failed", error: "Device not connected" });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
const built = await this.playlists.removePlaylistFromDevice(deviceId, playlistId);
|
|
674
|
+
const result = await this.sendDeviceCommand(deviceId, "device.content.setPolicy", {
|
|
675
|
+
policy: built.policy
|
|
676
|
+
});
|
|
677
|
+
await this.recordPolicyPush(deviceId);
|
|
678
|
+
sendJson(res, 200, { status: "success", policy: built.policy, result });
|
|
679
|
+
}
|
|
680
|
+
catch (e) {
|
|
681
|
+
const msg = e instanceof Error ? e.message : "Remove failed";
|
|
682
|
+
sendJson(res, 400, { status: "failed", error: msg });
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
553
686
|
if (req.method === "POST") {
|
|
554
687
|
const parsed = parseDevicePath(pathname);
|
|
555
688
|
if (parsed) {
|
|
@@ -701,6 +834,9 @@ export class TomorrowOS extends EventEmitter {
|
|
|
701
834
|
this.sendBrandSnapshot(ws);
|
|
702
835
|
void this.refreshPairedDeviceInfo(deviceId);
|
|
703
836
|
this.emit("device.online", { deviceId });
|
|
837
|
+
void this.pushLatestPolicyToDevice(deviceId).catch((err) => {
|
|
838
|
+
console.error("[TomorrowOS] pushLatestPolicy on resume failed:", err);
|
|
839
|
+
});
|
|
704
840
|
})();
|
|
705
841
|
}
|
|
706
842
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|