@telemetryos/cli 1.10.0 → 1.12.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 +22 -0
- package/dist/commands/auth.js +6 -13
- package/dist/commands/init.js +64 -49
- package/dist/commands/publish.d.ts +20 -0
- package/dist/commands/publish.js +68 -38
- package/dist/services/api-client.js +1 -1
- package/dist/services/cli-config.d.ts +9 -5
- package/dist/services/cli-config.js +28 -6
- package/dist/services/run-server.js +139 -36
- package/dist/utils/ansi.d.ts +1 -0
- package/dist/utils/ansi.js +1 -0
- package/package.json +4 -4
- package/templates/vite-react-typescript/CLAUDE.md +9 -2
- package/templates/vite-react-typescript/_claude/skills/tos-architecture/SKILL.md +4 -28
- package/templates/vite-react-typescript/_claude/skills/tos-media-api/SKILL.md +94 -7
- package/templates/vite-react-typescript/_claude/skills/tos-multi-mode/SKILL.md +359 -0
- 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 +72 -29
- package/templates/vite-react-typescript/_claude/skills/tos-store-sync/SKILL.md +96 -5
- package/templates/vite-react-typescript/index.html +1 -1
- package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +0 -624
|
@@ -3,11 +3,11 @@ import { readFile } from 'fs/promises';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import http from 'http';
|
|
5
5
|
import path from 'path';
|
|
6
|
-
import
|
|
6
|
+
import { createInterface } from 'readline/promises';
|
|
7
7
|
import serveHandler from 'serve-handler';
|
|
8
8
|
import pkg from '../../package.json' with { type: 'json' };
|
|
9
9
|
import { loadProjectConfig } from './project-config.js';
|
|
10
|
-
import { ansi } from '../utils/ansi.js';
|
|
10
|
+
import { ansi, ansiRegex } from '../utils/ansi.js';
|
|
11
11
|
export async function runServer(projectPath, flags) {
|
|
12
12
|
printSplashScreen();
|
|
13
13
|
projectPath = path.resolve(process.cwd(), projectPath);
|
|
@@ -21,6 +21,8 @@ export async function runServer(projectPath, flags) {
|
|
|
21
21
|
}
|
|
22
22
|
await serveDevelopmentApplicationHostUI(projectPath, flags.port, projectConfig);
|
|
23
23
|
await serveTelemetryApplication(projectPath, projectConfig);
|
|
24
|
+
// Print ready message after Vite is confirmed ready
|
|
25
|
+
printServerInfo(flags.port);
|
|
24
26
|
}
|
|
25
27
|
async function serveDevelopmentApplicationHostUI(projectPath, port, projectConfig) {
|
|
26
28
|
const hostUiPath = await import.meta.resolve('@telemetryos/development-application-host-ui/dist');
|
|
@@ -122,49 +124,150 @@ async function serveDevelopmentApplicationHostUI(projectPath, port, projectConfi
|
|
|
122
124
|
}
|
|
123
125
|
return;
|
|
124
126
|
}
|
|
127
|
+
// TODO: Publish endpoint disabled until auth is working
|
|
128
|
+
// if (url.pathname === '/__publish__') {
|
|
129
|
+
// if (req.method !== 'GET') {
|
|
130
|
+
// res.statusCode = 405
|
|
131
|
+
// res.end('Method not allowed')
|
|
132
|
+
// return
|
|
133
|
+
// }
|
|
134
|
+
//
|
|
135
|
+
// // Set SSE headers
|
|
136
|
+
// res.setHeader('Content-Type', 'text/event-stream')
|
|
137
|
+
// res.setHeader('Cache-Control', 'no-cache')
|
|
138
|
+
// res.setHeader('Connection', 'keep-alive')
|
|
139
|
+
//
|
|
140
|
+
// const sendEvent = (event: string, data: any) => {
|
|
141
|
+
// res.write(`event: ${event}\n`)
|
|
142
|
+
// res.write(`data: ${JSON.stringify(data)}\n\n`)
|
|
143
|
+
// }
|
|
144
|
+
//
|
|
145
|
+
// try {
|
|
146
|
+
// sendEvent('state', { state: 'starting' })
|
|
147
|
+
//
|
|
148
|
+
// await handlePublishCommand(
|
|
149
|
+
// projectPath,
|
|
150
|
+
// {},
|
|
151
|
+
// {
|
|
152
|
+
// onLog: (line: string) => {
|
|
153
|
+
// sendEvent('log', { message: line })
|
|
154
|
+
// },
|
|
155
|
+
// onStateChange: (state: string) => {
|
|
156
|
+
// sendEvent('state', { state })
|
|
157
|
+
// },
|
|
158
|
+
// onComplete: (data: { success: boolean; buildIndex?: number; duration?: string }) => {
|
|
159
|
+
// sendEvent('complete', data)
|
|
160
|
+
// res.end()
|
|
161
|
+
// },
|
|
162
|
+
// onError: (error: Error) => {
|
|
163
|
+
// const isAuthError =
|
|
164
|
+
// error.message.includes('authenticate') ||
|
|
165
|
+
// error.message.includes('Not authenticated')
|
|
166
|
+
// sendEvent('error', {
|
|
167
|
+
// error: error.message,
|
|
168
|
+
// code: isAuthError ? 'AUTH_REQUIRED' : 'PUBLISH_FAILED',
|
|
169
|
+
// message: isAuthError
|
|
170
|
+
// ? "Run 'tos auth' in the terminal to authenticate"
|
|
171
|
+
// : error.message,
|
|
172
|
+
// })
|
|
173
|
+
// res.end()
|
|
174
|
+
// },
|
|
175
|
+
// },
|
|
176
|
+
// )
|
|
177
|
+
// } catch (error) {
|
|
178
|
+
// sendEvent('error', {
|
|
179
|
+
// error: (error as Error).message,
|
|
180
|
+
// code: 'UNEXPECTED_ERROR',
|
|
181
|
+
// message: (error as Error).message,
|
|
182
|
+
// })
|
|
183
|
+
// res.end()
|
|
184
|
+
// }
|
|
185
|
+
// return
|
|
186
|
+
// }
|
|
125
187
|
serveHandler(req, res, serveConfig).catch((err) => {
|
|
126
188
|
console.error('Error handling request:', err);
|
|
127
189
|
res.statusCode = 500;
|
|
128
190
|
res.end('Internal Server Error');
|
|
129
191
|
});
|
|
130
192
|
});
|
|
131
|
-
printServerInfo(port);
|
|
132
193
|
server.listen(port);
|
|
133
194
|
}
|
|
134
195
|
async function serveTelemetryApplication(rootPath, projectConfig) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
var _a;
|
|
198
|
+
if (!((_a = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.devServer) === null || _a === void 0 ? void 0 : _a.runCommand)) {
|
|
199
|
+
console.log('No value in config at devServer.runCommand');
|
|
200
|
+
resolve();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const runCommand = projectConfig.devServer.runCommand;
|
|
204
|
+
const binPath = path.join(rootPath, 'node_modules', '.bin');
|
|
205
|
+
const childProcess = spawn(runCommand, {
|
|
206
|
+
shell: true,
|
|
207
|
+
env: {
|
|
208
|
+
...process.env,
|
|
209
|
+
FORCE_COLOR: '1',
|
|
210
|
+
PATH: `${binPath}${path.delimiter}${process.env.PATH}`,
|
|
211
|
+
},
|
|
212
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
213
|
+
cwd: rootPath,
|
|
214
|
+
});
|
|
215
|
+
const stdoutReadline = createInterface({
|
|
216
|
+
input: childProcess.stdout,
|
|
217
|
+
crlfDelay: Infinity,
|
|
218
|
+
});
|
|
219
|
+
const stderrReadline = createInterface({
|
|
220
|
+
input: childProcess.stderr,
|
|
221
|
+
crlfDelay: Infinity,
|
|
222
|
+
});
|
|
223
|
+
let cleaned = false;
|
|
224
|
+
const cleanup = () => {
|
|
225
|
+
if (cleaned)
|
|
226
|
+
return;
|
|
227
|
+
cleaned = true;
|
|
228
|
+
process.removeListener('exit', onParentExit);
|
|
229
|
+
clearTimeout(timeoutHandle);
|
|
230
|
+
childProcess.kill();
|
|
231
|
+
stdoutReadline.close();
|
|
232
|
+
stderrReadline.close();
|
|
233
|
+
};
|
|
234
|
+
const onParentExit = () => {
|
|
235
|
+
console.log('Shutting down development server...');
|
|
236
|
+
cleanup();
|
|
237
|
+
};
|
|
238
|
+
process.on('exit', onParentExit);
|
|
239
|
+
const timeoutHandle = setTimeout(() => {
|
|
240
|
+
cleanup();
|
|
241
|
+
reject(new Error('Vite dev server did not start within 30 seconds'));
|
|
242
|
+
}, 30000);
|
|
243
|
+
let viteReady = false;
|
|
244
|
+
stdoutReadline.on('line', (line) => {
|
|
245
|
+
console.log(`[application]: ${line}`);
|
|
246
|
+
// Detect Vite ready signal
|
|
247
|
+
if (!viteReady) {
|
|
248
|
+
const cleanLine = line.replace(ansiRegex, '');
|
|
249
|
+
if (cleanLine.includes('VITE') && cleanLine.includes('ready in')) {
|
|
250
|
+
viteReady = true;
|
|
251
|
+
clearTimeout(timeoutHandle);
|
|
252
|
+
resolve();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
stderrReadline.on('line', (line) => {
|
|
257
|
+
console.error(`[application]: ${line}`);
|
|
258
|
+
});
|
|
259
|
+
childProcess.on('error', (error) => {
|
|
260
|
+
if (!viteReady) {
|
|
261
|
+
cleanup();
|
|
262
|
+
reject(error);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
childProcess.on('close', (code) => {
|
|
266
|
+
if (!viteReady) {
|
|
267
|
+
cleanup();
|
|
268
|
+
reject(new Error(`Dev server process exited with code ${code} before becoming ready`));
|
|
269
|
+
}
|
|
270
|
+
});
|
|
168
271
|
});
|
|
169
272
|
}
|
|
170
273
|
function printSplashScreen() {
|
package/dist/utils/ansi.d.ts
CHANGED
package/dist/utils/ansi.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetryos/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.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,7 +25,7 @@
|
|
|
25
25
|
"license": "",
|
|
26
26
|
"repository": "github:TelemetryTV/Application-API",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@telemetryos/development-application-host-ui": "^1.
|
|
28
|
+
"@telemetryos/development-application-host-ui": "^1.12.0",
|
|
29
29
|
"@types/serve-handler": "^6.1.4",
|
|
30
30
|
"commander": "^14.0.0",
|
|
31
31
|
"ignore": "^6.0.2",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"eslint-plugin-prettier": "^5.4.0",
|
|
44
44
|
"globals": "^16.0.0",
|
|
45
45
|
"prettier": "^3.5.3",
|
|
46
|
-
"typescript
|
|
47
|
-
"typescript": "^
|
|
46
|
+
"typescript": "^5.8.3",
|
|
47
|
+
"typescript-eslint": "^8.49.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"tos": "pnpm build && node dist/index.js",
|
|
@@ -15,7 +15,9 @@ tos serve # Start dev server (or: npm run dev)
|
|
|
15
15
|
**Development Host:** http://localhost:2026
|
|
16
16
|
Both the render and settings mounts points are visible in the development host.
|
|
17
17
|
The Render mount point is presented in a resizable pane.
|
|
18
|
-
The Settings mount point shows in the
|
|
18
|
+
The Settings mount point shows in the right sidebar.
|
|
19
|
+
|
|
20
|
+
**The development host is already running!** The user has already started it and the agent doesn't need to run it
|
|
19
21
|
|
|
20
22
|
## Architecture
|
|
21
23
|
|
|
@@ -100,12 +102,17 @@ const response = await proxy().fetch('https://api.example.com/data')
|
|
|
100
102
|
|
|
101
103
|
**IMPORTANT:** You MUST invoke the relevant skill BEFORE writing code for these tasks:
|
|
102
104
|
|
|
105
|
+
**For Render views:** ALWAYS read `tos-render-ui-design` first, then the appropriate specialized skill.
|
|
106
|
+
|
|
103
107
|
| Task | Required Skill | Why |
|
|
104
108
|
|------|----------------|-----|
|
|
105
109
|
| Starting new project | `tos-requirements` | Gather requirements before coding MUST USE |
|
|
106
|
-
| Building Render
|
|
110
|
+
| Building ANY Render view | `tos-render-ui-design` | UI scaling foundation - ALWAYS read first |
|
|
111
|
+
| Building digital signage | `tos-render-signage-design` | Display-only patterns (no interaction) |
|
|
112
|
+
| Building interactive kiosk | `tos-render-kiosk-design` | Touch interaction, idle timeout, navigation |
|
|
107
113
|
| Adding ANY Settings UI | `tos-settings-ui` | SDK components are required - raw HTML won't work |
|
|
108
114
|
| Adding store keys | `tos-store-sync` | Hook patterns ensure Settings↔Render sync |
|
|
115
|
+
| Building multi-mode apps | `tos-multi-mode` | Entity-scoped data, mode switching, namespace patterns |
|
|
109
116
|
| Calling external APIs | `tos-proxy-fetch` | Proxy patterns prevent CORS errors |
|
|
110
117
|
| Media library access | `tos-media-api` | SDK media methods and types |
|
|
111
118
|
| Weather integration | `tos-weather-api` | API-specific patterns and credentials |
|
|
@@ -232,15 +232,11 @@ export function App() {
|
|
|
232
232
|
|
|
233
233
|
### Local Development
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
**Development Host:** http://localhost:2026
|
|
236
|
+
Both the render and settings mounts points are visible in the development host.
|
|
237
|
+
The Render mount point is presented in a resizable pane.
|
|
238
|
+
The Settings mount point shows in the right sidebar.
|
|
239
239
|
|
|
240
|
-
# Access locally:
|
|
241
|
-
# Settings: http://localhost:3000/settings
|
|
242
|
-
# Render: http://localhost:3000/render
|
|
243
|
-
```
|
|
244
240
|
|
|
245
241
|
### Build & Deploy
|
|
246
242
|
|
|
@@ -255,26 +251,6 @@ git add . && git commit -m "Update" && git push
|
|
|
255
251
|
|
|
256
252
|
## Common Patterns
|
|
257
253
|
|
|
258
|
-
### Check Mount Point
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
const isSettings = window.location.pathname === '/settings'
|
|
262
|
-
const isRender = window.location.pathname === '/render'
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
### Conditional Features
|
|
266
|
-
|
|
267
|
-
```typescript
|
|
268
|
-
// In a shared component
|
|
269
|
-
const isRender = window.location.pathname === '/render'
|
|
270
|
-
|
|
271
|
-
// Only fetch external data in Render
|
|
272
|
-
useEffect(() => {
|
|
273
|
-
if (!isRender) return
|
|
274
|
-
fetchExternalData()
|
|
275
|
-
}, [isRender])
|
|
276
|
-
```
|
|
277
|
-
|
|
278
254
|
### Handle Missing Config
|
|
279
255
|
|
|
280
256
|
```typescript
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tos-media-api
|
|
3
|
-
description: Access TelemetryOS Media Library for images, videos, and files. Use when building apps that display user-uploaded media content.
|
|
3
|
+
description: Access TelemetryOS Media Library for images, videos, and files. Use when building apps that display or select user-uploaded media content.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# TelemetryOS Media API
|
|
@@ -23,6 +23,10 @@ const tagged = await media().getAllByTag('banner')
|
|
|
23
23
|
|
|
24
24
|
// Get single item by ID
|
|
25
25
|
const item = await media().getById('content-id')
|
|
26
|
+
|
|
27
|
+
// Open media picker (user selects from full-screen dialog)
|
|
28
|
+
const selection = await media().openPicker()
|
|
29
|
+
const imagesOnly = await media().openPicker({ accept: ['image/*'] })
|
|
26
30
|
```
|
|
27
31
|
|
|
28
32
|
## Response Types
|
|
@@ -61,6 +65,29 @@ interface MediaContent {
|
|
|
61
65
|
}
|
|
62
66
|
```
|
|
63
67
|
|
|
68
|
+
### MediaSelection
|
|
69
|
+
|
|
70
|
+
Returned by `openPicker()` and used by `SettingsMediaSelect`.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
interface MediaSelection {
|
|
74
|
+
id: string
|
|
75
|
+
name: string
|
|
76
|
+
thumbnailUrl: string // Thumbnail for preview
|
|
77
|
+
url: string // Full content URL
|
|
78
|
+
contentType: string // MIME type
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### MediaPickerOptions
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
interface MediaPickerOptions {
|
|
86
|
+
accept?: string[] // Content type filters: ['image/*', 'video/*']
|
|
87
|
+
currentValue?: MediaSelection // Pre-select current value in picker
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
64
91
|
## Common Patterns
|
|
65
92
|
|
|
66
93
|
### Folder Picker in Settings
|
|
@@ -123,6 +150,64 @@ export default function Settings() {
|
|
|
123
150
|
}
|
|
124
151
|
```
|
|
125
152
|
|
|
153
|
+
### Media Picker in Settings (SettingsMediaSelect)
|
|
154
|
+
|
|
155
|
+
Use the built-in `SettingsMediaSelect` component to let users pick a media item. Opens a full-screen picker dialog in the host window.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// hooks/store.ts
|
|
159
|
+
import { createUseInstanceStoreState } from '@telemetryos/sdk/react'
|
|
160
|
+
import type { MediaSelection } from '@telemetryos/sdk/react'
|
|
161
|
+
|
|
162
|
+
export const useMediaState = createUseInstanceStoreState<MediaSelection | null>('selectedMedia', null)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// views/Settings.tsx
|
|
167
|
+
import {
|
|
168
|
+
SettingsContainer,
|
|
169
|
+
SettingsField,
|
|
170
|
+
SettingsLabel,
|
|
171
|
+
SettingsMediaSelect,
|
|
172
|
+
} from '@telemetryos/sdk/react'
|
|
173
|
+
import { useMediaState } from '../hooks/store'
|
|
174
|
+
|
|
175
|
+
export default function Settings() {
|
|
176
|
+
const [isLoading, selectedMedia, setSelectedMedia] = useMediaState()
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<SettingsContainer>
|
|
180
|
+
<SettingsField>
|
|
181
|
+
<SettingsLabel>Background Image</SettingsLabel>
|
|
182
|
+
<SettingsMediaSelect
|
|
183
|
+
value={selectedMedia}
|
|
184
|
+
onChange={setSelectedMedia}
|
|
185
|
+
accept={['image/*']}
|
|
186
|
+
disabled={isLoading}
|
|
187
|
+
/>
|
|
188
|
+
</SettingsField>
|
|
189
|
+
</SettingsContainer>
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Using openPicker() Directly
|
|
195
|
+
|
|
196
|
+
For custom UI, call `media().openPicker()` directly instead of using the component.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { media } from '@telemetryos/sdk'
|
|
200
|
+
|
|
201
|
+
const handleSelectMedia = async () => {
|
|
202
|
+
const selection = await media().openPicker({ accept: ['image/*'] })
|
|
203
|
+
if (selection) {
|
|
204
|
+
// selection has: id, name, thumbnailUrl, url, contentType
|
|
205
|
+
console.log('Selected:', selection.name, selection.url)
|
|
206
|
+
}
|
|
207
|
+
// selection is null if user cancelled
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
126
211
|
### Image Gallery in Render
|
|
127
212
|
|
|
128
213
|
```typescript
|
|
@@ -327,9 +412,11 @@ const activeContent = content.filter(item => {
|
|
|
327
412
|
|
|
328
413
|
## Tips
|
|
329
414
|
|
|
330
|
-
1. **Use
|
|
331
|
-
2. **Use
|
|
332
|
-
3. **
|
|
333
|
-
4. **
|
|
334
|
-
5. **
|
|
335
|
-
6. **
|
|
415
|
+
1. **Use `SettingsMediaSelect` for settings** - Preferred way to let users pick media in Settings views
|
|
416
|
+
2. **Use `openPicker()` for custom UI** - When you need full control over the selection flow
|
|
417
|
+
3. **Use publicUrls[0]** - First URL is the primary CDN URL
|
|
418
|
+
4. **Use thumbnailUrl for previews** - Smaller, faster loading
|
|
419
|
+
5. **Filter by contentType** - Ensure you're displaying compatible content
|
|
420
|
+
6. **Handle empty folders** - Show appropriate message when no content
|
|
421
|
+
7. **Lazy load images** - Use `loading="lazy"` for galleries
|
|
422
|
+
8. **Respect hidden flag** - Filter out hidden items unless intentional
|