@telemetryos/cli 1.9.0 → 1.11.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/CHANGELOG.md +25 -0
- package/dist/commands/auth.js +8 -15
- package/dist/commands/init.js +131 -68
- package/dist/commands/publish.d.ts +22 -0
- package/dist/commands/publish.js +238 -0
- package/dist/index.js +2 -0
- package/dist/plugins/math-tools.d.ts +2 -0
- package/dist/plugins/math-tools.js +18 -0
- package/dist/services/api-client.d.ts +18 -0
- package/dist/services/api-client.js +70 -0
- package/dist/services/archiver.d.ts +4 -0
- package/dist/services/archiver.js +65 -0
- package/dist/services/build-poller.d.ts +10 -0
- package/dist/services/build-poller.js +63 -0
- package/dist/services/cli-config.d.ts +10 -0
- package/dist/services/cli-config.js +45 -0
- package/dist/services/generate-application.d.ts +2 -1
- package/dist/services/generate-application.js +31 -32
- package/dist/services/project-config.d.ts +24 -0
- package/dist/services/project-config.js +51 -0
- package/dist/services/run-server.js +29 -73
- package/dist/types/api.d.ts +44 -0
- package/dist/types/api.js +1 -0
- package/dist/types/applications.d.ts +44 -0
- package/dist/types/applications.js +1 -0
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +10 -0
- package/dist/utils/path-utils.d.ts +55 -0
- package/dist/utils/path-utils.js +99 -0
- package/package.json +4 -2
- package/templates/vite-react-typescript/CLAUDE.md +14 -6
- package/templates/vite-react-typescript/_claude/skills/tos-architecture/SKILL.md +4 -28
- package/templates/vite-react-typescript/_claude/skills/tos-multi-mode/SKILL.md +359 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +304 -12
- package/templates/vite-react-typescript/_claude/skills/tos-render-kiosk-design/SKILL.md +384 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-signage-design/SKILL.md +515 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-ui-design/SKILL.md +325 -0
- package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +405 -125
- package/templates/vite-react-typescript/_claude/skills/tos-store-sync/SKILL.md +96 -5
- package/templates/vite-react-typescript/_claude/skills/tos-weather-api/SKILL.md +443 -269
- package/templates/vite-react-typescript/index.html +1 -1
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tos-multi-mode
|
|
3
|
+
description: Architecture for apps with multiple Render view modes sharing data through a top-level entity. Use when building apps where different devices show different views (e.g., kiosk vs display) with shared data scoped to an entity like location or topic.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Multi-Mode App Architecture
|
|
7
|
+
|
|
8
|
+
Some TelemetryOS apps need multiple Render view modes — the same app configured once, but different devices show different views. A queue manager is a classic example: one device runs a ticket kiosk, another shows a "now serving" display. Both share the same queue data scoped to a location.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
Listen for these indicators during requirements gathering:
|
|
13
|
+
|
|
14
|
+
- "One screen shows X while another shows Y"
|
|
15
|
+
- "Kiosk and display" or "control panel and viewer"
|
|
16
|
+
- "Different devices need different views of the same data"
|
|
17
|
+
- Data that's organized by location, department, topic, or similar grouping
|
|
18
|
+
|
|
19
|
+
If ANY of these come up, this is a multi-mode app.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Choosing the Top-Level Entity
|
|
24
|
+
|
|
25
|
+
**This is the most important architectural decision for a multi-mode app.**
|
|
26
|
+
|
|
27
|
+
Every multi-mode app needs a top-level organizing entity that scopes all shared data. The developer will almost never think of this on their own — they'll describe the modes ("a kiosk screen and a display screen") but won't mention what scopes the shared data. **You must actively discover or propose one.**
|
|
28
|
+
|
|
29
|
+
### How to discover the entity
|
|
30
|
+
|
|
31
|
+
Ask: "What organizes the data these modes share? For example, is this per-location, per-department, per-topic?"
|
|
32
|
+
|
|
33
|
+
If the developer doesn't have a clear answer, **propose one** based on the app domain:
|
|
34
|
+
|
|
35
|
+
| App Domain | Likely Entity | Examples |
|
|
36
|
+
|------------|---------------|----------|
|
|
37
|
+
| Queue / service | location, branch | "Downtown Office", "Main Lobby" |
|
|
38
|
+
| Content / editorial | topic, channel | "Sports", "Company News" |
|
|
39
|
+
| Event / scheduling | event, venue | "Conference 2025", "Room A" |
|
|
40
|
+
| Organizational | department, team | "Engineering", "Sales" |
|
|
41
|
+
| Retail / menu | store, menu | "Store #42", "Lunch Menu" |
|
|
42
|
+
|
|
43
|
+
### Why the entity matters
|
|
44
|
+
|
|
45
|
+
Explain to the developer: "This lets you run the same app at multiple [locations] — each with its own independent data — without creating separate app instances. An admin just selects which [location] each device belongs to."
|
|
46
|
+
|
|
47
|
+
The entity name flows into everything:
|
|
48
|
+
- **Namespace helper**: `locationNamespace('Downtown Office')` → `'queue-manager-Downtown Office'`
|
|
49
|
+
- **Store keys**: `useSelectedLocationStoreState`
|
|
50
|
+
- **Settings labels**: "Location" dropdown, "Locations" management section
|
|
51
|
+
- **Mental model**: admins think in terms of the entity
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Architecture Overview
|
|
56
|
+
|
|
57
|
+
Multi-mode apps use a 3-tier store pattern:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
61
|
+
│ Instance Scope (per-device) │
|
|
62
|
+
│ ┌─────────────────┐ ┌──────────────────────────────────┐ │
|
|
63
|
+
│ │ mode: 'kiosk' │ │ selectedLocation: 'Location A' │ │
|
|
64
|
+
│ └─────────────────┘ └──────────────────────────────────┘ │
|
|
65
|
+
└─────────────────────────────────────────────────────────────┘
|
|
66
|
+
|
|
67
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
68
|
+
│ Application Scope (shared across all instances) │
|
|
69
|
+
│ ┌───────────────────────────────────────────────────────┐ │
|
|
70
|
+
│ │ locations: ['Location A', 'Location B'] │ │
|
|
71
|
+
│ └───────────────────────────────────────────────────────┘ │
|
|
72
|
+
└─────────────────────────────────────────────────────────────┘
|
|
73
|
+
|
|
74
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
75
|
+
│ Dynamic Namespace Scope (per-entity, shared between modes) │
|
|
76
|
+
│ namespace: 'queue-manager-Location A' │
|
|
77
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────────┐ │
|
|
78
|
+
│ │ serviceTypes │ │ counters │ │ calledHistory │ │
|
|
79
|
+
│ └──────────────┘ └──────────────┘ └─────────────────────┘ │
|
|
80
|
+
└─────────────────────────────────────────────────────────────┘
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**How it connects:**
|
|
84
|
+
|
|
85
|
+
1. Admin creates locations in Settings (application scope)
|
|
86
|
+
2. Each device selects a mode AND a location (instance scope)
|
|
87
|
+
3. All devices on the same location share the same data (dynamic namespace scope)
|
|
88
|
+
4. A kiosk at "Location A" and a display at "Location A" see the same queues
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Store Layer Pattern
|
|
93
|
+
|
|
94
|
+
### hooks/store.ts
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import {
|
|
98
|
+
createUseInstanceStoreState,
|
|
99
|
+
createUseApplicationStoreState,
|
|
100
|
+
createUseDynamicNamespaceStoreState,
|
|
101
|
+
} from '@telemetryos/sdk/react'
|
|
102
|
+
|
|
103
|
+
// --- Instance scope (per-device) ---
|
|
104
|
+
|
|
105
|
+
// Which mode this device displays
|
|
106
|
+
export const useModeStoreState = createUseInstanceStoreState<'kiosk' | 'display'>('mode', 'kiosk')
|
|
107
|
+
|
|
108
|
+
// Which entity this device belongs to
|
|
109
|
+
export const useSelectedLocationStoreState = createUseInstanceStoreState<string>('selected-location', 'Location A')
|
|
110
|
+
|
|
111
|
+
// UI Scale (for display modes)
|
|
112
|
+
export const useUiScaleStoreState = createUseInstanceStoreState<number>('ui-scale', 1)
|
|
113
|
+
|
|
114
|
+
// --- Application scope (shared across all instances) ---
|
|
115
|
+
|
|
116
|
+
// The list of all entities — managed by admins
|
|
117
|
+
export const useLocationsStoreState = createUseApplicationStoreState<string[]>('locations', ['Location A'])
|
|
118
|
+
|
|
119
|
+
// --- Dynamic namespace scope (per-entity, shared between modes) ---
|
|
120
|
+
|
|
121
|
+
// All data that's scoped to an entity uses dynamic namespace hooks.
|
|
122
|
+
// The namespace is passed at call time, not at definition time.
|
|
123
|
+
|
|
124
|
+
export const useServiceTypesStoreState = createUseDynamicNamespaceStoreState<ServiceType[]>('service-types', [])
|
|
125
|
+
export const useCountersStoreState = createUseDynamicNamespaceStoreState<Counter[]>('counters', [])
|
|
126
|
+
export const useCalledHistoryStoreState = createUseDynamicNamespaceStoreState<CalledNumber[]>('called-history', [])
|
|
127
|
+
|
|
128
|
+
// --- Namespace helper ---
|
|
129
|
+
|
|
130
|
+
// Derives a unique namespace from the entity name.
|
|
131
|
+
// All hooks for the same entity use the same namespace.
|
|
132
|
+
export function locationNamespace(location: string): string {
|
|
133
|
+
return `queue-manager-${location}`
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Key rules
|
|
138
|
+
|
|
139
|
+
- **Mode** is always instance scope — each device picks its own mode
|
|
140
|
+
- **Entity selection** is always instance scope — each device picks its entity
|
|
141
|
+
- **Entity list** is always application scope — admins manage it globally
|
|
142
|
+
- **All shared data** uses dynamic namespace — scoped to the entity
|
|
143
|
+
- **Namespace helper** is a pure function: entity name in, namespace string out
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Entity Selection Validation
|
|
148
|
+
|
|
149
|
+
The selected entity might not exist anymore (admin removed it). Always validate:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// In Settings or Render
|
|
153
|
+
const [isLoadingLocations, locations] = useLocationsStoreState()
|
|
154
|
+
const [isLoadingLocation, selectedLocation, setSelectedLocation] = useSelectedLocationStoreState()
|
|
155
|
+
|
|
156
|
+
// Safe fallback
|
|
157
|
+
const safeLocations = locations || ['Location A']
|
|
158
|
+
const effectiveLocation = safeLocations.includes(selectedLocation || '')
|
|
159
|
+
? (selectedLocation || 'Location A')
|
|
160
|
+
: safeLocations[0]
|
|
161
|
+
|
|
162
|
+
// Derive namespace from validated entity
|
|
163
|
+
const ns = locationNamespace(effectiveLocation)
|
|
164
|
+
|
|
165
|
+
// Now use ns with all dynamic namespace hooks
|
|
166
|
+
const [isLoadingTypes, serviceTypes, setServiceTypes] = useServiceTypesStoreState(ns, 250)
|
|
167
|
+
const [isLoadingCounters, counters, setCounters] = useCountersStoreState(ns, 250)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Render View Pattern
|
|
173
|
+
|
|
174
|
+
Load all hooks, aggregate loading state, then branch on mode:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { useUiScaleToSetRem } from '@telemetryos/sdk/react'
|
|
178
|
+
import {
|
|
179
|
+
useModeStoreState,
|
|
180
|
+
useUiScaleStoreState,
|
|
181
|
+
useSelectedLocationStoreState,
|
|
182
|
+
useServiceTypesStoreState,
|
|
183
|
+
useCountersStoreState,
|
|
184
|
+
useCalledHistoryStoreState,
|
|
185
|
+
locationNamespace,
|
|
186
|
+
} from '../hooks/store'
|
|
187
|
+
|
|
188
|
+
export function Render() {
|
|
189
|
+
// Instance-scoped hooks
|
|
190
|
+
const [isLoadingScale, uiScale] = useUiScaleStoreState()
|
|
191
|
+
const [isLoadingMode, mode] = useModeStoreState()
|
|
192
|
+
const [isLoadingLocation, selectedLocation] = useSelectedLocationStoreState()
|
|
193
|
+
|
|
194
|
+
// Derive namespace
|
|
195
|
+
const ns = locationNamespace(selectedLocation || 'Location A')
|
|
196
|
+
|
|
197
|
+
// Dynamic namespace hooks (shared data)
|
|
198
|
+
const [isLoadingTypes, serviceTypes, setServiceTypes] = useServiceTypesStoreState(ns, 0)
|
|
199
|
+
const [isLoadingCounters, counters] = useCountersStoreState(ns, 0)
|
|
200
|
+
const [isLoadingHistory, calledHistory, setCalledHistory] = useCalledHistoryStoreState(ns, 0)
|
|
201
|
+
|
|
202
|
+
useUiScaleToSetRem(uiScale ?? 1)
|
|
203
|
+
|
|
204
|
+
// Aggregate loading state
|
|
205
|
+
const isLoading = isLoadingScale || isLoadingMode || isLoadingLocation ||
|
|
206
|
+
isLoadingTypes || isLoadingCounters || isLoadingHistory
|
|
207
|
+
|
|
208
|
+
if (isLoading) {
|
|
209
|
+
return <div className="render render--loading">Loading...</div>
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Branch on mode AFTER loading completes
|
|
213
|
+
if (mode === 'kiosk') {
|
|
214
|
+
return (
|
|
215
|
+
<TicketKiosk
|
|
216
|
+
serviceTypes={serviceTypes}
|
|
217
|
+
setServiceTypes={setServiceTypes}
|
|
218
|
+
calledHistory={calledHistory}
|
|
219
|
+
setCalledHistory={setCalledHistory}
|
|
220
|
+
/>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<NowServingDisplay
|
|
226
|
+
serviceTypes={serviceTypes}
|
|
227
|
+
counters={counters}
|
|
228
|
+
calledHistory={calledHistory}
|
|
229
|
+
/>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Important patterns
|
|
235
|
+
|
|
236
|
+
- **All hooks load before branching** — both modes may need the same shared data
|
|
237
|
+
- **Namespace is derived from validated entity** — not directly from the raw store value
|
|
238
|
+
- **Mode components receive data as props** — they don't call store hooks themselves
|
|
239
|
+
- **setters are passed only to modes that write** — display mode typically only reads
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Settings Organization
|
|
244
|
+
|
|
245
|
+
Order Settings sections intentionally:
|
|
246
|
+
|
|
247
|
+
### 1. Mode selector (top — always visible)
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
<SettingsHeading>Display Mode</SettingsHeading>
|
|
251
|
+
<SettingsField>
|
|
252
|
+
<SettingsLabel>Mode</SettingsLabel>
|
|
253
|
+
<SettingsSelectFrame>
|
|
254
|
+
<select value={mode} onChange={(e) => setMode(e.target.value as AppMode)}>
|
|
255
|
+
<option value="kiosk">Ticket Kiosk</option>
|
|
256
|
+
<option value="display">Now Serving Display</option>
|
|
257
|
+
</select>
|
|
258
|
+
</SettingsSelectFrame>
|
|
259
|
+
<SettingsHint>
|
|
260
|
+
{mode === 'kiosk'
|
|
261
|
+
? 'Customer-facing ticket dispenser'
|
|
262
|
+
: 'Announcement board showing current numbers'}
|
|
263
|
+
</SettingsHint>
|
|
264
|
+
</SettingsField>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 2. Entity selector (which entity this device belongs to)
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
<SettingsField>
|
|
271
|
+
<SettingsLabel>Location</SettingsLabel>
|
|
272
|
+
<SettingsSelectFrame>
|
|
273
|
+
<select value={effectiveLocation} onChange={(e) => setSelectedLocation(e.target.value)}>
|
|
274
|
+
{safeLocations.map(loc => (
|
|
275
|
+
<option key={loc} value={loc}>{loc}</option>
|
|
276
|
+
))}
|
|
277
|
+
</select>
|
|
278
|
+
</SettingsSelectFrame>
|
|
279
|
+
<SettingsHint>Data is managed separately per location</SettingsHint>
|
|
280
|
+
</SettingsField>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### 3. Mode-specific and entity-scoped settings (middle)
|
|
284
|
+
|
|
285
|
+
The bulk of the settings — queue management, display options, etc. All scoped to the selected entity via the derived namespace.
|
|
286
|
+
|
|
287
|
+
### 4. Entity management (bottom)
|
|
288
|
+
|
|
289
|
+
Add/rename/remove entities. Always at the bottom because it's infrequent admin work.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Entity Management Pattern
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// Add
|
|
297
|
+
const addLocation = () => {
|
|
298
|
+
const newName = `Location ${String.fromCharCode(65 + safeLocations.length)}`
|
|
299
|
+
setLocations([...safeLocations, newName])
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Rename (update selection if the renamed entity was selected)
|
|
303
|
+
const renameLocation = (index: number, newName: string) => {
|
|
304
|
+
const oldName = safeLocations[index]
|
|
305
|
+
const updated = [...safeLocations]
|
|
306
|
+
updated[index] = newName
|
|
307
|
+
setLocations(updated)
|
|
308
|
+
if (effectiveLocation === oldName) {
|
|
309
|
+
setSelectedLocation(newName)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Remove (fall back to first if the removed entity was selected)
|
|
314
|
+
const removeLocation = (index: number) => {
|
|
315
|
+
if (safeLocations.length <= 1) return // Always keep at least one
|
|
316
|
+
const removedName = safeLocations[index]
|
|
317
|
+
const updated = safeLocations.filter((_, i) => i !== index)
|
|
318
|
+
setLocations(updated)
|
|
319
|
+
if (effectiveLocation === removedName) {
|
|
320
|
+
setSelectedLocation(updated[0])
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Rules
|
|
326
|
+
|
|
327
|
+
- **Always keep at least 1 entity** — disable remove when only 1 remains
|
|
328
|
+
- **Update selection on rename** — if the selected entity was renamed, update the selection to match
|
|
329
|
+
- **Fall back on remove** — if the selected entity was removed, select the first remaining
|
|
330
|
+
- **New entity naming** — use a sensible default like "Location A", "Location B", etc.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Mode Type Definition
|
|
335
|
+
|
|
336
|
+
Define modes as a union type in your types file:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// types/queue.ts (or types/app.ts)
|
|
340
|
+
export type AppMode = 'kiosk' | 'display'
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Use this type for the store hook and any mode-dependent logic. Add new modes to the union as needed — the pattern scales to 3+ modes.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Checklist
|
|
348
|
+
|
|
349
|
+
Before implementing a multi-mode app, confirm:
|
|
350
|
+
|
|
351
|
+
- [ ] Modes identified (what Render views exist)
|
|
352
|
+
- [ ] Top-level entity chosen (what scopes the shared data)
|
|
353
|
+
- [ ] Mode type defined as union type
|
|
354
|
+
- [ ] Store hooks organized in 3 tiers (instance / application / dynamic namespace)
|
|
355
|
+
- [ ] Namespace helper function created
|
|
356
|
+
- [ ] Entity selection validation with safe fallback
|
|
357
|
+
- [ ] Render view loads all hooks before branching on mode
|
|
358
|
+
- [ ] Settings ordered: mode → entity selector → config → entity management
|
|
359
|
+
- [ ] Entity management has add/rename/remove with at-least-one validation
|