@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/commands/auth.js +8 -15
  3. package/dist/commands/init.js +131 -68
  4. package/dist/commands/publish.d.ts +22 -0
  5. package/dist/commands/publish.js +238 -0
  6. package/dist/index.js +2 -0
  7. package/dist/plugins/math-tools.d.ts +2 -0
  8. package/dist/plugins/math-tools.js +18 -0
  9. package/dist/services/api-client.d.ts +18 -0
  10. package/dist/services/api-client.js +70 -0
  11. package/dist/services/archiver.d.ts +4 -0
  12. package/dist/services/archiver.js +65 -0
  13. package/dist/services/build-poller.d.ts +10 -0
  14. package/dist/services/build-poller.js +63 -0
  15. package/dist/services/cli-config.d.ts +10 -0
  16. package/dist/services/cli-config.js +45 -0
  17. package/dist/services/generate-application.d.ts +2 -1
  18. package/dist/services/generate-application.js +31 -32
  19. package/dist/services/project-config.d.ts +24 -0
  20. package/dist/services/project-config.js +51 -0
  21. package/dist/services/run-server.js +29 -73
  22. package/dist/types/api.d.ts +44 -0
  23. package/dist/types/api.js +1 -0
  24. package/dist/types/applications.d.ts +44 -0
  25. package/dist/types/applications.js +1 -0
  26. package/dist/utils/ansi.d.ts +10 -0
  27. package/dist/utils/ansi.js +10 -0
  28. package/dist/utils/path-utils.d.ts +55 -0
  29. package/dist/utils/path-utils.js +99 -0
  30. package/package.json +4 -2
  31. package/templates/vite-react-typescript/CLAUDE.md +14 -6
  32. package/templates/vite-react-typescript/_claude/skills/tos-architecture/SKILL.md +4 -28
  33. package/templates/vite-react-typescript/_claude/skills/tos-multi-mode/SKILL.md +359 -0
  34. package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +304 -12
  35. package/templates/vite-react-typescript/_claude/skills/tos-render-kiosk-design/SKILL.md +384 -0
  36. package/templates/vite-react-typescript/_claude/skills/tos-render-signage-design/SKILL.md +515 -0
  37. package/templates/vite-react-typescript/_claude/skills/tos-render-ui-design/SKILL.md +325 -0
  38. package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +405 -125
  39. package/templates/vite-react-typescript/_claude/skills/tos-store-sync/SKILL.md +96 -5
  40. package/templates/vite-react-typescript/_claude/skills/tos-weather-api/SKILL.md +443 -269
  41. 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