@telemetryos/cli 1.8.3 → 1.10.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 (35) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/commands/auth.js +4 -4
  3. package/dist/commands/init.js +90 -42
  4. package/dist/commands/publish.d.ts +2 -0
  5. package/dist/commands/publish.js +208 -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 +6 -0
  16. package/dist/services/cli-config.js +23 -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 +6 -5
  32. package/templates/vite-react-typescript/_claude/settings.local.json +2 -1
  33. package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +304 -12
  34. package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +367 -130
  35. package/templates/vite-react-typescript/_claude/skills/tos-weather-api/SKILL.md +443 -269
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Utility functions for path handling and project name derivation
3
+ */
4
+ import path from 'node:path';
5
+ /**
6
+ * Converts a string to kebab-case
7
+ *
8
+ * @param str - The string to convert
9
+ * @returns The kebab-cased string
10
+ *
11
+ * @example
12
+ * toKebabCase('MyApp') // 'my-app'
13
+ * toKebabCase('my_app') // 'my-app'
14
+ * toKebabCase('My App!') // 'my-app'
15
+ * toKebabCase('my--app') // 'my-app'
16
+ */
17
+ export function toKebabCase(str) {
18
+ return (str
19
+ .trim()
20
+ // Remove special characters except spaces, underscores, and hyphens
21
+ .replace(/[^a-zA-Z0-9\s_-]/g, '')
22
+ // Replace spaces and underscores with hyphens
23
+ .replace(/[\s_]+/g, '-')
24
+ // Insert hyphen before uppercase letters preceded by lowercase
25
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
26
+ // Convert to lowercase
27
+ .toLowerCase()
28
+ // Replace multiple consecutive hyphens with single hyphen
29
+ .replace(/-+/g, '-')
30
+ // Remove leading/trailing hyphens
31
+ .replace(/^-+|-+$/g, ''));
32
+ }
33
+ /**
34
+ * Derives a project name from a given path
35
+ *
36
+ * @param projectPath - The path to derive the name from
37
+ * @param currentWorkingDirectory - The current working directory (defaults to process.cwd())
38
+ * @returns The derived project name in kebab-case
39
+ *
40
+ * @example
41
+ * deriveProjectName('my-project') // 'my-project'
42
+ * deriveProjectName('apps/MyApp') // 'my-app'
43
+ * deriveProjectName('./', '/Users/test/MyProject') // 'my-project'
44
+ * deriveProjectName('../parent') // 'parent'
45
+ * deriveProjectName('/absolute/path/to/app') // 'app'
46
+ */
47
+ export function deriveProjectName(projectPath, currentWorkingDirectory = process.cwd()) {
48
+ // Resolve the path to handle relative paths
49
+ const resolvedPath = path.resolve(currentWorkingDirectory, projectPath);
50
+ // Get the last segment of the path
51
+ const basename = path.basename(resolvedPath);
52
+ // Convert to kebab-case
53
+ return toKebabCase(basename);
54
+ }
55
+ /**
56
+ * Resolves a project path and derives the name
57
+ *
58
+ * @param projectPath - The path to resolve
59
+ * @param currentWorkingDirectory - The current working directory (defaults to process.cwd())
60
+ * @returns An object containing the resolved path and derived name
61
+ *
62
+ * @example
63
+ * resolveProjectPathAndName('apps/MyApp')
64
+ * // { resolvedPath: '/Users/user/cwd/apps/MyApp', derivedName: 'my-app' }
65
+ */
66
+ export function resolveProjectPathAndName(projectPath, currentWorkingDirectory = process.cwd()) {
67
+ const resolvedPath = path.resolve(currentWorkingDirectory, projectPath);
68
+ const derivedName = deriveProjectName(projectPath, currentWorkingDirectory);
69
+ return { resolvedPath, derivedName };
70
+ }
71
+ /**
72
+ * Validates a project name according to npm package name requirements
73
+ *
74
+ * @param name - The project name to validate
75
+ * @returns true if valid, or an error message string if invalid
76
+ *
77
+ * @example
78
+ * validateProjectName('my-app') // true
79
+ * validateProjectName('') // 'Project name cannot be empty'
80
+ * validateProjectName('MyApp') // 'Project name must contain only lowercase letters, numbers, and hyphens'
81
+ */
82
+ export function validateProjectName(name) {
83
+ if (!name || name.length === 0) {
84
+ return 'Project name cannot be empty';
85
+ }
86
+ if (name.length > 214) {
87
+ return 'Project name must be 214 characters or less';
88
+ }
89
+ if (name.startsWith('.') || name.startsWith('_')) {
90
+ return 'Project name cannot start with . or _';
91
+ }
92
+ if (!/^[a-z0-9-]+$/.test(name)) {
93
+ return 'Project name must contain only lowercase letters, numbers, and hyphens';
94
+ }
95
+ if (name.startsWith('-') || name.endsWith('-')) {
96
+ return 'Project name cannot start or end with a hyphen';
97
+ }
98
+ return true;
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetryos/cli",
3
- "version": "1.8.3",
3
+ "version": "1.10.0",
4
4
  "description": "The official TelemetryOS application CLI package. Use it to build applications that run on the TelemetryOS platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,11 +25,13 @@
25
25
  "license": "",
26
26
  "repository": "github:TelemetryTV/Application-API",
27
27
  "dependencies": {
28
- "@telemetryos/development-application-host-ui": "^1.8.3",
28
+ "@telemetryos/development-application-host-ui": "^1.10.0",
29
29
  "@types/serve-handler": "^6.1.4",
30
30
  "commander": "^14.0.0",
31
+ "ignore": "^6.0.2",
31
32
  "inquirer": "^12.9.6",
32
33
  "serve-handler": "^6.1.6",
34
+ "tar": "^7.4.3",
33
35
  "zod": "^4.1.12"
34
36
  },
35
37
  "devDependencies": {
@@ -13,8 +13,9 @@ tos serve # Start dev server (or: npm run dev)
13
13
  **IMPORTANT:** Always run `npm run build` after making changes to check for TypeScript errors. Do not rely solely on the dev server.
14
14
 
15
15
  **Development Host:** http://localhost:2026
16
- - Settings: http://localhost:2026/settings
17
- - Render: http://localhost:2026/render
16
+ Both the render and settings mounts points are visible in the development host.
17
+ The Render mount point is presented in a resizable pane.
18
+ The Settings mount point shows in the settings tab of the right sidebar.
18
19
 
19
20
  ## Architecture
20
21
 
@@ -101,13 +102,13 @@ const response = await proxy().fetch('https://api.example.com/data')
101
102
 
102
103
  | Task | Required Skill | Why |
103
104
  |------|----------------|-----|
105
+ | Starting new project | `tos-requirements` | Gather requirements before coding MUST USE |
104
106
  | Building Render views | `tos-render-design` | Digital signage constraints, UI scaling, no hover/scroll |
105
107
  | Adding ANY Settings UI | `tos-settings-ui` | SDK components are required - raw HTML won't work |
106
- | Calling external APIs | `tos-proxy-fetch` | Proxy patterns prevent CORS errors |
107
108
  | Adding store keys | `tos-store-sync` | Hook patterns ensure Settings↔Render sync |
108
- | Weather integration | `tos-weather-api` | API-specific patterns and credentials |
109
+ | Calling external APIs | `tos-proxy-fetch` | Proxy patterns prevent CORS errors |
109
110
  | Media library access | `tos-media-api` | SDK media methods and types |
110
- | Starting new project | `tos-requirements` | Gather requirements before coding |
111
+ | Weather integration | `tos-weather-api` | API-specific patterns and credentials |
111
112
  | Debugging issues | `tos-debugging` | Common errors and fixes |
112
113
 
113
114
  **Never write Render layouts, Settings components, or proxy.fetch code without invoking the skill first.**
@@ -11,7 +11,8 @@
11
11
  "Skill(tos-store-sync)",
12
12
  "Skill(tos-weather-api)",
13
13
  "Bash(npm run build:*)",
14
- "Bash(npm run dev:*)"
14
+ "Bash(npm run dev:*)",
15
+ "WebFetch(domain:docs.telemetryos.com)"
15
16
  ]
16
17
  }
17
18
  }
@@ -1,33 +1,63 @@
1
1
  ---
2
2
  name: tos-render-design
3
- description: Design patterns for TelemetryOS Render views. Use when building or reviewing Render view layouts, handling responsive scaling, or ensuring digital signage best practices.
3
+ description: Design patterns for TelemetryOS Render views. Use when building display-only digital signage OR interactive kiosks. Covers responsive scaling, UI patterns, and interaction handling for both models.
4
4
  ---
5
5
 
6
6
  # Render View Design
7
7
 
8
- TelemetryOS Render views display on digital signage—TVs, video walls, and displays viewed from a distance with no user interaction. This fundamentally shapes how to design them.
8
+ TelemetryOS Render views support both **digital signage** (display-only) and **interactive kiosks** (touch-enabled). Understanding which pattern you're building determines how you handle user interaction and state management.
9
9
 
10
10
  > **Note:** The init project already provides base styles in `index.css` (viewport scaling, box-sizing) and `Render.css` (`.render` class with padding, overflow, flexbox). Build on these—don't override them.
11
11
 
12
- ## Digital Signage Constraints
12
+ ---
13
+
14
+ ## Interaction Models
15
+
16
+ TelemetryOS render views support two interaction models. Both use `@telemetryos/sdk` with the same architecture—the difference is whether the render view includes onClick handlers.
17
+
18
+ ### Digital Signage (Display-Only)
19
+
20
+ **Use for:** Information displays, dashboards, menu boards, announcements
21
+
22
+ - Content updates automatically via store subscriptions
23
+ - No user interaction (no mouse, keyboard, touch input)
24
+ - No onClick handlers in render view
25
+ - Viewed from a distance
26
+ - Updates driven by timers, external data, or Settings changes
27
+
28
+ ### Interactive Kiosk (Touch-Enabled)
29
+
30
+ **Use for:** Wayfinding, directories, check-in systems, search interfaces
31
+
32
+ - Users can touch/click elements on screen
33
+ - onClick handlers for buttons, navigation, forms
34
+ - Idle timeout returns to home screen after inactivity
35
+ - Touch feedback with :active states (not :hover)
36
+ - Manages interaction state (current screen, navigation history)
37
+
38
+ ---
39
+
40
+ ## Digital Signage Pattern (Display-Only)
41
+
42
+ For apps where users only **view** content from a distance.
13
43
 
14
44
  ### No User Interaction
15
45
 
16
- Unless building for kiosk/touchscreen scenarios, assume **no mouse, keyboard, or touch input**:
46
+ Assume **no mouse, keyboard, or touch input**:
17
47
 
18
48
  ```css
19
- /* WRONG - No one will hover */
49
+ /* WRONG - No one will hover on digital signage */
20
50
  .button:hover {
21
51
  background: blue;
22
52
  }
23
53
 
24
- /* WRONG - No one will focus */
54
+ /* WRONG - No one will focus elements */
25
55
  .input:focus {
26
56
  outline: 2px solid blue;
27
57
  }
28
58
  ```
29
59
 
30
- Avoid `:hover`, `:focus`, `:active`, and similar interaction pseudo-classes.
60
+ Avoid `:hover`, `:focus`, `:active`, and similar interaction pseudo-classes for display-only apps.
31
61
 
32
62
  ### No Scrolling
33
63
 
@@ -55,8 +85,150 @@ Content **must fit the viewport**. There's no user to scroll:
55
85
 
56
86
  If content might overflow, truncate it or conditionally hide elements—never show a scrollbar.
57
87
 
88
+ ---
89
+
90
+ ## Interactive Kiosk Pattern (Touch-Enabled)
91
+
92
+ For apps where users **interact** with the screen via touch or click.
93
+
94
+ ### onClick Handlers
95
+
96
+ Add click handlers to interactive elements:
97
+
98
+ ```typescript
99
+ function Render() {
100
+ const [screen, setScreen] = useState('home')
101
+
102
+ return (
103
+ <div className="render">
104
+ {screen === 'home' && (
105
+ <button
106
+ className="kiosk-button"
107
+ onClick={() => setScreen('search')}
108
+ >
109
+ Search Directory
110
+ </button>
111
+ )}
112
+ {screen === 'search' && (
113
+ <SearchScreen onBack={() => setScreen('home')} />
114
+ )}
115
+ </div>
116
+ )
117
+ }
118
+ ```
119
+
120
+ ### Touch Feedback (:active states)
121
+
122
+ Use `:active` pseudo-class for touch feedback (NOT `:hover`):
123
+
124
+ ```css
125
+ .kiosk-button {
126
+ padding: 2rem 4rem;
127
+ font-size: 3rem;
128
+ background: blue;
129
+ color: white;
130
+ border: none;
131
+ border-radius: 1rem;
132
+ transition: transform 0.1s, background 0.1s;
133
+ }
134
+
135
+ /* Touch feedback - user sees visual response when tapping */
136
+ .kiosk-button:active {
137
+ transform: scale(0.95);
138
+ background: darkblue;
139
+ }
140
+ ```
141
+
142
+ **Why :active instead of :hover?**
143
+ - Touch devices don't have hover
144
+ - :active triggers on touch/click
145
+ - Provides immediate visual feedback
146
+
147
+ ### Idle Timeout Pattern
148
+
149
+ Return to home screen after inactivity:
150
+
151
+ ```typescript
152
+ function Render() {
153
+ const [screen, setScreen] = useState('home')
154
+ const [lastInteraction, setLastInteraction] = useState(Date.now())
155
+
156
+ // Return to home after 30 seconds of inactivity
157
+ useEffect(() => {
158
+ const timeout = setTimeout(() => {
159
+ const elapsed = Date.now() - lastInteraction
160
+ if (elapsed > 30000 && screen !== 'home') {
161
+ setScreen('home')
162
+ }
163
+ }, 30000)
164
+
165
+ return () => clearTimeout(timeout)
166
+ }, [lastInteraction, screen])
167
+
168
+ const handleInteraction = (newScreen: string) => {
169
+ setScreen(newScreen)
170
+ setLastInteraction(Date.now())
171
+ }
172
+
173
+ return (
174
+ <div className="render">
175
+ {screen === 'home' && (
176
+ <button onClick={() => handleInteraction('search')}>
177
+ Search
178
+ </button>
179
+ )}
180
+ </div>
181
+ )
182
+ }
183
+ ```
184
+
185
+ ### Touch Target Sizing
186
+
187
+ Make touch targets large enough to tap accurately:
188
+
189
+ ```css
190
+ /* Minimum sizes for touch targets */
191
+ .kiosk-button {
192
+ min-width: 15rem; /* Large enough to tap */
193
+ min-height: 8rem;
194
+ padding: 2rem 4rem;
195
+ font-size: 3rem; /* Large, readable text */
196
+ }
197
+ ```
198
+
199
+ **Guidelines:**
200
+ - Minimum 8rem height for buttons
201
+ - 2rem padding minimum
202
+ - 3rem font size minimum for buttons
203
+ - Gap of at least 1rem between interactive elements
204
+
205
+ ### Store State for Navigation
206
+
207
+ Use device store to persist state across composition changes:
208
+
209
+ ```typescript
210
+ // hooks/store.ts
211
+ import { createUseDeviceStoreState } from '@telemetryos/sdk/react'
212
+
213
+ export const useKioskScreenState = createUseDeviceStoreState<string>(
214
+ 'kiosk-screen',
215
+ 'home'
216
+ )
217
+
218
+ // Render.tsx
219
+ const [_isLoading, screen, setScreen] = useKioskScreenState()
220
+ ```
221
+
222
+ **Why device store?**
223
+ - Persists state on the device (survives composition changes)
224
+ - Doesn't sync to other devices (screen state is device-local)
225
+
226
+ ---
227
+
58
228
  ## UI Scale Hooks
59
229
 
230
+ **Applies to both digital signage and interactive kiosks.**
231
+
60
232
  Displays range from tablets to 8K video walls. Standard CSS pixels create inconsistent sizing. The SDK provides hooks that redefine `rem` as viewport-relative:
61
233
 
62
234
  ### useUiScaleToSetRem(uiScale)
@@ -101,8 +273,12 @@ export function Render() {
101
273
  }
102
274
  ```
103
275
 
276
+ ---
277
+
104
278
  ## Best Practices
105
279
 
280
+ **Applies to both digital signage and interactive kiosks.**
281
+
106
282
  ### Use rem for Everything
107
283
 
108
284
  All sizing should use `rem` to scale with the UI scale setting:
@@ -200,10 +376,14 @@ function Dashboard() {
200
376
  }
201
377
  ```
202
378
 
203
- ## Complete Example
379
+ ---
380
+
381
+ ## Complete Examples
382
+
383
+ ### Digital Signage Example (Display-Only)
204
384
 
205
385
  ```typescript
206
- // Render.tsx - Building on the init project's .render class
386
+ // Render.tsx - Display-only dashboard
207
387
  import { useUiScaleToSetRem, useUiAspectRatio } from '@telemetryos/sdk/react'
208
388
  import { useUiScaleStoreState } from '../hooks/store'
209
389
  import './Render.css'
@@ -241,7 +421,7 @@ export function Render() {
241
421
  ```
242
422
 
243
423
  ```css
244
- /* Render.css - Add to existing styles, don't override .render base */
424
+ /* Render.css - Display-only styles */
245
425
  .render__header {
246
426
  flex-shrink: 0;
247
427
  margin-bottom: 2rem;
@@ -278,6 +458,112 @@ export function Render() {
278
458
  }
279
459
  ```
280
460
 
461
+ ### Interactive Kiosk Example (Touch-Enabled)
462
+
463
+ ```typescript
464
+ // Render.tsx - Interactive kiosk with navigation
465
+ import { useState, useEffect } from 'react'
466
+ import { useUiScaleToSetRem } from '@telemetryos/sdk/react'
467
+ import { useUiScaleStoreState } from '../hooks/store'
468
+ import './Render.css'
469
+
470
+ export function Render() {
471
+ const [isLoading, uiScale] = useUiScaleStoreState()
472
+ const [screen, setScreen] = useState('home')
473
+ const [lastInteraction, setLastInteraction] = useState(Date.now())
474
+
475
+ useUiScaleToSetRem(uiScale)
476
+
477
+ // Idle timeout - return to home after 30 seconds
478
+ useEffect(() => {
479
+ const timeout = setTimeout(() => {
480
+ const elapsed = Date.now() - lastInteraction
481
+ if (elapsed > 30000 && screen !== 'home') {
482
+ setScreen('home')
483
+ }
484
+ }, 30000)
485
+
486
+ return () => clearTimeout(timeout)
487
+ }, [lastInteraction, screen])
488
+
489
+ const handleInteraction = (newScreen: string) => {
490
+ setScreen(newScreen)
491
+ setLastInteraction(Date.now())
492
+ }
493
+
494
+ if (isLoading) return null
495
+
496
+ return (
497
+ <div className="render">
498
+ {screen === 'home' && (
499
+ <div className="kiosk-home">
500
+ <h1 className="kiosk-home__title">Welcome</h1>
501
+ <button
502
+ className="kiosk-button"
503
+ onClick={() => handleInteraction('search')}
504
+ >
505
+ Search Directory
506
+ </button>
507
+ <button
508
+ className="kiosk-button"
509
+ onClick={() => handleInteraction('map')}
510
+ >
511
+ View Map
512
+ </button>
513
+ </div>
514
+ )}
515
+
516
+ {screen === 'search' && (
517
+ <SearchScreen onBack={() => handleInteraction('home')} />
518
+ )}
519
+
520
+ {screen === 'map' && (
521
+ <MapScreen onBack={() => handleInteraction('home')} />
522
+ )}
523
+ </div>
524
+ )
525
+ }
526
+ ```
527
+
528
+ ```css
529
+ /* Render.css - Interactive kiosk styles */
530
+ .kiosk-home {
531
+ display: flex;
532
+ flex-direction: column;
533
+ align-items: center;
534
+ justify-content: center;
535
+ gap: 3rem;
536
+ height: 100%;
537
+ }
538
+
539
+ .kiosk-home__title {
540
+ font-size: 6rem;
541
+ margin: 0;
542
+ }
543
+
544
+ /* Touch-friendly button with :active feedback */
545
+ .kiosk-button {
546
+ min-width: 20rem;
547
+ min-height: 8rem;
548
+ padding: 2rem 4rem;
549
+ font-size: 3rem;
550
+ background: #0066cc;
551
+ color: white;
552
+ border: none;
553
+ border-radius: 1rem;
554
+ cursor: pointer;
555
+ transition: transform 0.1s, background 0.1s;
556
+ }
557
+
558
+ /* Touch feedback - scales down when tapped */
559
+ .kiosk-button:active {
560
+ transform: scale(0.95);
561
+ background: #0052a3;
562
+ }
563
+ ```
564
+
565
+ ---
566
+
281
567
  ## Store Hook for UI Scale
282
568
 
283
569
  Create a store hook to let admins adjust the UI scale:
@@ -318,15 +604,21 @@ export function Settings() {
318
604
  }
319
605
  ```
320
606
 
607
+ ---
608
+
321
609
  ## Common Mistakes
322
610
 
323
611
  | Mistake | Problem | Fix |
324
612
  |---------|---------|-----|
325
613
  | Using `px` units | Won't scale across resolutions | Use `rem` everywhere |
326
- | Adding `:hover` styles | No mouse on digital signage | Remove interaction states |
614
+ | Adding `:hover` styles on digital signage | No mouse on display-only apps | Remove interaction states for display-only |
615
+ | Using `:hover` on interactive kiosks | No hover on touch devices | Use `:active` instead for touch feedback |
327
616
  | Using `overflow: scroll` | No user to scroll | Use `overflow: hidden`, truncate content |
328
617
  | Fixed heights in `px` | Breaks on different aspect ratios | Use `vh`, `%`, or flex |
329
- | Forgetting `useUiScaleToSetRem()` | `rem` units won't scale properly | Call it once in Render view with the uiScale from `useUiScaleStoreState()` |
618
+ | Forgetting `useUiScaleToSetRem()` | `rem` units won't scale properly | Call it once in Render view with uiScale |
330
619
  | Text below 2rem | Unreadable from viewing distance | Minimum 2rem for body text |
620
+ | Small touch targets on kiosks | Hard to tap accurately | Minimum 8rem height, 2rem padding, 3rem font |
621
+ | No idle timeout on kiosks | Kiosk stays on user's screen | Add useEffect timeout logic |
622
+ | No touch feedback on kiosks | User unsure if tap registered | Add :active state animations |
331
623
  | Removing `.render` padding | Content cut off by bezels | Keep the ~3rem padding from init project |
332
624
  | Overriding `index.css` base styles | Breaks viewport scaling | Add new styles, don't modify base setup |