@telemetryos/cli 1.10.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 +11 -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/package.json +2 -2
- 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-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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @telemetryos/cli
|
|
2
2
|
|
|
3
|
+
## 1.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Added media select component, improved tos init tos publish and tos auth commands, add new store hook
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- @telemetryos/development-application-host-ui@1.11.0
|
|
13
|
+
|
|
3
14
|
## 1.10.0
|
|
4
15
|
|
|
5
16
|
### Minor Changes
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
|
-
import { loadCliConfig, saveCliConfig } from '../services/cli-config.js';
|
|
3
|
+
import { apiTokenSchema, loadCliConfig, saveCliConfig } from '../services/cli-config.js';
|
|
4
4
|
export const authCommand = new Command('auth')
|
|
5
5
|
.description('Authenticate with TelemetryOS by saving your API token')
|
|
6
6
|
.option('-t, --token <string>', 'API token (skip interactive prompt)')
|
|
@@ -12,22 +12,15 @@ function maskToken(token) {
|
|
|
12
12
|
return `${token.slice(0, 4)}...${token.slice(-4)}`;
|
|
13
13
|
}
|
|
14
14
|
function validateToken(input) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (!/^[a-f0-9-]+$/i.test(trimmed)) {
|
|
20
|
-
return 'Token must be a valid string';
|
|
21
|
-
}
|
|
22
|
-
if (trimmed.length < 24) {
|
|
23
|
-
return 'Token must be at least 24 characters';
|
|
24
|
-
}
|
|
25
|
-
return true;
|
|
15
|
+
const result = apiTokenSchema.safeParse(input.trim());
|
|
16
|
+
if (result.success)
|
|
17
|
+
return true;
|
|
18
|
+
return result.error.issues[0].message;
|
|
26
19
|
}
|
|
27
20
|
async function handleAuthCommand(options) {
|
|
28
21
|
let token = options.token;
|
|
29
22
|
const existingCliConfig = await loadCliConfig();
|
|
30
|
-
const existingToken = existingCliConfig
|
|
23
|
+
const existingToken = existingCliConfig.apiToken;
|
|
31
24
|
if (existingToken && !options.force) {
|
|
32
25
|
const { confirm } = await inquirer.prompt([
|
|
33
26
|
{
|
package/dist/commands/init.js
CHANGED
|
@@ -27,6 +27,7 @@ export const initCommand = new Command('init')
|
|
|
27
27
|
.option('-a, --author <string>', 'The author of the application', '')
|
|
28
28
|
.option('-v, --version <string>', 'The version of the application', '0.1.0')
|
|
29
29
|
.option('-t, --template <string>', 'The template to use (vite-react-typescript)', '')
|
|
30
|
+
.option('-y, --yes', 'Skip all prompts and use defaults', false)
|
|
30
31
|
.action(handleInitCommand);
|
|
31
32
|
async function handleInitCommand(projectPathArg, options) {
|
|
32
33
|
// Step 1: Resolve path and derive name
|
|
@@ -79,60 +80,74 @@ async function handleInitCommand(projectPathArg, options) {
|
|
|
79
80
|
let author = options.author;
|
|
80
81
|
let version = options.version;
|
|
81
82
|
let template = options.template;
|
|
82
|
-
// Step 5: Build prompt questions
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
name: 'description',
|
|
100
|
-
message: 'What is the description of your application?',
|
|
101
|
-
default: 'A telemetryOS application',
|
|
102
|
-
});
|
|
103
|
-
if (!author)
|
|
104
|
-
questions.push({
|
|
105
|
-
type: 'input',
|
|
106
|
-
name: 'author',
|
|
107
|
-
message: 'Who is the author of your application?',
|
|
108
|
-
default: '',
|
|
109
|
-
});
|
|
110
|
-
if (!version)
|
|
83
|
+
// Step 5: Build prompt questions (skipped with --yes)
|
|
84
|
+
if (options.yes) {
|
|
85
|
+
if (!name) {
|
|
86
|
+
console.error('Project name is required when using --yes');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
if (!description)
|
|
90
|
+
description = 'A telemetryOS application';
|
|
91
|
+
if (!template)
|
|
92
|
+
template = 'vite-react-typescript';
|
|
93
|
+
if (!version)
|
|
94
|
+
version = '0.1.0';
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const questions = [];
|
|
98
|
+
// Always show the derived name with option to override
|
|
99
|
+
// This provides transparency and control
|
|
111
100
|
questions.push({
|
|
112
101
|
type: 'input',
|
|
113
|
-
name: '
|
|
114
|
-
message: '
|
|
115
|
-
default:
|
|
102
|
+
name: 'name',
|
|
103
|
+
message: 'Project name:',
|
|
104
|
+
default: derivedName,
|
|
116
105
|
validate: (input) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return 'Version must be in semver format (e.g. 1.0.0)';
|
|
106
|
+
const result = validateProjectName(input);
|
|
107
|
+
return result === true ? true : result;
|
|
120
108
|
},
|
|
121
109
|
});
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
110
|
+
if (!description)
|
|
111
|
+
questions.push({
|
|
112
|
+
type: 'input',
|
|
113
|
+
name: 'description',
|
|
114
|
+
message: 'What is the description of your application?',
|
|
115
|
+
default: 'A telemetryOS application',
|
|
116
|
+
});
|
|
117
|
+
if (!author)
|
|
118
|
+
questions.push({
|
|
119
|
+
type: 'input',
|
|
120
|
+
name: 'author',
|
|
121
|
+
message: 'Who is the author of your application?',
|
|
122
|
+
default: '',
|
|
123
|
+
});
|
|
124
|
+
if (!version)
|
|
125
|
+
questions.push({
|
|
126
|
+
type: 'input',
|
|
127
|
+
name: 'version',
|
|
128
|
+
message: 'What is the version of your application?',
|
|
129
|
+
default: '0.1.0',
|
|
130
|
+
validate: (input) => {
|
|
131
|
+
if (/^\d+\.\d+\.\d+(-.+)?$/.test(input))
|
|
132
|
+
return true;
|
|
133
|
+
return 'Version must be in semver format (e.g. 1.0.0)';
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
if (!template)
|
|
137
|
+
questions.push({
|
|
138
|
+
type: 'list',
|
|
139
|
+
name: 'template',
|
|
140
|
+
message: 'Which template would you like to use?',
|
|
141
|
+
choices: [{ name: 'Vite + React + TypeScript', value: 'vite-react-typescript' }],
|
|
142
|
+
});
|
|
143
|
+
// Step 6: Prompt user
|
|
144
|
+
const answers = await inquirer.prompt(questions);
|
|
145
|
+
name = answers.name || name;
|
|
146
|
+
version = answers.version || version;
|
|
147
|
+
description = answers.description || description;
|
|
148
|
+
author = answers.author || author;
|
|
149
|
+
template = answers.template || template;
|
|
150
|
+
}
|
|
136
151
|
// Step 7: Generate application
|
|
137
152
|
await generateApplication({
|
|
138
153
|
name,
|
|
@@ -1,2 +1,22 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
export declare const publishCommand: Command;
|
|
3
|
+
type PublishOptions = {
|
|
4
|
+
interactive?: boolean;
|
|
5
|
+
name?: string;
|
|
6
|
+
baseImage?: string;
|
|
7
|
+
buildScript?: string;
|
|
8
|
+
buildOutput?: string;
|
|
9
|
+
workingPath?: string;
|
|
10
|
+
};
|
|
11
|
+
type PublishCallbacks = {
|
|
12
|
+
onLog?: (line: string) => void;
|
|
13
|
+
onStateChange?: (state: string) => void;
|
|
14
|
+
onComplete?: (data: {
|
|
15
|
+
success: boolean;
|
|
16
|
+
buildIndex?: number;
|
|
17
|
+
duration?: string;
|
|
18
|
+
}) => void;
|
|
19
|
+
onError?: (error: Error) => void;
|
|
20
|
+
};
|
|
21
|
+
declare function handlePublishCommand(projectPath: string, options: PublishOptions, callbacks?: PublishCallbacks): Promise<void>;
|
|
22
|
+
export { handlePublishCommand, type PublishCallbacks, type PublishOptions };
|
package/dist/commands/publish.js
CHANGED
|
@@ -35,12 +35,14 @@ function getSpecifiedBuildOptions(options) {
|
|
|
35
35
|
specified.push('--working-path');
|
|
36
36
|
return specified;
|
|
37
37
|
}
|
|
38
|
-
async function handlePublishCommand(projectPath, options) {
|
|
39
|
-
var _a, _b, _c, _d;
|
|
38
|
+
async function handlePublishCommand(projectPath, options, callbacks) {
|
|
39
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
40
40
|
projectPath = path.resolve(process.cwd(), projectPath);
|
|
41
|
+
const logMessage = (callbacks === null || callbacks === void 0 ? void 0 : callbacks.onLog) || ((msg) => console.log(msg));
|
|
41
42
|
try {
|
|
42
43
|
// Step 1: Load project config
|
|
43
|
-
|
|
44
|
+
(_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _a === void 0 ? void 0 : _a.call(callbacks, 'loading');
|
|
45
|
+
logMessage(`\n${ansi.cyan}Loading project configuration...${ansi.reset}`);
|
|
44
46
|
const config = await loadProjectConfig(projectPath);
|
|
45
47
|
// Step 2: Resolve build configuration
|
|
46
48
|
let buildConfig = {
|
|
@@ -67,93 +69,121 @@ async function handlePublishCommand(projectPath, options) {
|
|
|
67
69
|
buildConfig.name = answers.name.trim();
|
|
68
70
|
}
|
|
69
71
|
if (!buildConfig.name) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
const error = new Error('Application name is required');
|
|
73
|
+
logMessage(`${ansi.red}Error: Application name is required${ansi.reset}`);
|
|
74
|
+
(_b = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onError) === null || _b === void 0 ? void 0 : _b.call(callbacks, error);
|
|
75
|
+
if (!callbacks)
|
|
76
|
+
process.exit(1);
|
|
77
|
+
return;
|
|
72
78
|
}
|
|
73
|
-
|
|
79
|
+
logMessage(` Project: ${ansi.bold}${buildConfig.name}${ansi.reset}`);
|
|
74
80
|
if (config.version) {
|
|
75
|
-
|
|
81
|
+
logMessage(` Version: ${config.version}`);
|
|
76
82
|
}
|
|
77
83
|
// Step 3: Initialize API client
|
|
78
|
-
|
|
84
|
+
(_c = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _c === void 0 ? void 0 : _c.call(callbacks, 'authenticating');
|
|
85
|
+
logMessage(`\n${ansi.cyan}Authenticating...${ansi.reset}`);
|
|
79
86
|
const apiClient = await ApiClient.create();
|
|
80
|
-
|
|
87
|
+
logMessage(` ${ansi.green}Authenticated${ansi.reset}`);
|
|
81
88
|
// Step 4: Create archive
|
|
82
|
-
|
|
89
|
+
(_d = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _d === void 0 ? void 0 : _d.call(callbacks, 'archiving');
|
|
90
|
+
logMessage(`\n${ansi.cyan}Archiving project...${ansi.reset}`);
|
|
83
91
|
const { buffer, filename } = await createArchive(projectPath, (msg) => {
|
|
84
|
-
|
|
92
|
+
logMessage(` ${ansi.dim}${msg}${ansi.reset}`);
|
|
85
93
|
});
|
|
86
94
|
// Step 5: Find or create application
|
|
87
|
-
|
|
95
|
+
logMessage(`\n${ansi.cyan}Checking for existing application...${ansi.reset}`);
|
|
88
96
|
const appsRes = await apiClient.get('/application');
|
|
89
97
|
const apps = (await appsRes.json());
|
|
90
98
|
let app = apps.find((a) => a.title === buildConfig.name) || null;
|
|
91
99
|
if (app) {
|
|
92
|
-
|
|
100
|
+
logMessage(` Found existing application: ${ansi.dim}${app.id}${ansi.reset}`);
|
|
93
101
|
const specifiedOptions = getSpecifiedBuildOptions(options);
|
|
94
102
|
if (specifiedOptions.length > 0) {
|
|
95
|
-
|
|
103
|
+
logMessage(` ${ansi.yellow}Warning: ${specifiedOptions.join(', ')} ignored for existing application${ansi.reset}`);
|
|
96
104
|
}
|
|
97
105
|
}
|
|
98
106
|
else {
|
|
99
|
-
|
|
107
|
+
logMessage(` Creating new application...`);
|
|
100
108
|
const createRequest = {
|
|
101
109
|
kind: 'uploaded',
|
|
102
110
|
title: buildConfig.name,
|
|
103
|
-
baseImage: (
|
|
111
|
+
baseImage: (_e = buildConfig.baseImage) !== null && _e !== void 0 ? _e : APP_BUILD_DEFAULTS.baseImage,
|
|
104
112
|
baseImageRegistryAuth: '',
|
|
105
|
-
buildWorkingPath: (
|
|
106
|
-
buildScript: (
|
|
107
|
-
buildOutputPath: (
|
|
113
|
+
buildWorkingPath: (_f = buildConfig.workingPath) !== null && _f !== void 0 ? _f : APP_BUILD_DEFAULTS.workingPath,
|
|
114
|
+
buildScript: (_g = buildConfig.buildScript) !== null && _g !== void 0 ? _g : APP_BUILD_DEFAULTS.buildScript,
|
|
115
|
+
buildOutputPath: (_h = buildConfig.buildOutput) !== null && _h !== void 0 ? _h : APP_BUILD_DEFAULTS.buildOutput,
|
|
108
116
|
};
|
|
109
117
|
const createRes = await apiClient.post('/application', { body: createRequest });
|
|
110
118
|
app = (await createRes.json());
|
|
111
|
-
|
|
119
|
+
logMessage(` ${ansi.green}Created application:${ansi.reset} ${ansi.dim}${app.id}${ansi.reset}`);
|
|
112
120
|
}
|
|
113
121
|
// Step 6: Upload archive
|
|
114
|
-
|
|
122
|
+
(_j = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _j === void 0 ? void 0 : _j.call(callbacks, 'uploading');
|
|
123
|
+
logMessage(`\n${ansi.cyan}Uploading archive...${ansi.reset}`);
|
|
115
124
|
const formData = new FormData();
|
|
116
125
|
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
117
126
|
const blob = new Blob([arrayBuffer], { type: 'application/gzip' });
|
|
118
127
|
formData.append('file', blob, filename);
|
|
119
128
|
await apiClient.post(`/application/${app.id}/archive`, { body: formData });
|
|
120
|
-
|
|
129
|
+
logMessage(` ${ansi.green}Upload complete${ansi.reset}`);
|
|
121
130
|
// Step 7: Poll for build status and stream logs
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
(_k = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _k === void 0 ? void 0 : _k.call(callbacks, 'building');
|
|
132
|
+
logMessage(`\n${ansi.cyan}Building...${ansi.reset}`);
|
|
133
|
+
logMessage(`${ansi.white}Build Logs${ansi.reset}`);
|
|
124
134
|
const build = await pollBuild(apiClient, app.id, {
|
|
125
135
|
onLog: (line) => {
|
|
126
|
-
|
|
136
|
+
logMessage(` ${ansi.dim}${line}${ansi.reset}`);
|
|
127
137
|
},
|
|
128
138
|
onStateChange: () => { },
|
|
129
139
|
onComplete: () => { },
|
|
130
140
|
onError: (error) => {
|
|
131
|
-
|
|
141
|
+
var _a;
|
|
142
|
+
logMessage(`${ansi.red}Polling error: ${error.message}${ansi.reset}`);
|
|
143
|
+
(_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onError) === null || _a === void 0 ? void 0 : _a.call(callbacks, error);
|
|
132
144
|
},
|
|
133
145
|
});
|
|
134
146
|
// Step 8: Report result
|
|
135
147
|
if (build.state === 'success') {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
(_l = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _l === void 0 ? void 0 : _l.call(callbacks, 'success');
|
|
149
|
+
logMessage(`\n${ansi.green}${ansi.bold}Build successful!${ansi.reset}`);
|
|
150
|
+
logMessage(`\n Application: ${buildConfig.name}`);
|
|
151
|
+
logMessage(` Build: #${build.index}`);
|
|
152
|
+
let duration = '';
|
|
139
153
|
if (build.finishedAt && build.startedAt) {
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
duration = calculateDuration(build.startedAt, build.finishedAt);
|
|
155
|
+
logMessage(` Duration: ${duration}`);
|
|
142
156
|
}
|
|
143
|
-
|
|
157
|
+
logMessage('');
|
|
158
|
+
(_m = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onComplete) === null || _m === void 0 ? void 0 : _m.call(callbacks, {
|
|
159
|
+
success: true,
|
|
160
|
+
buildIndex: build.index,
|
|
161
|
+
duration,
|
|
162
|
+
});
|
|
144
163
|
}
|
|
145
164
|
else {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
165
|
+
(_o = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onStateChange) === null || _o === void 0 ? void 0 : _o.call(callbacks, 'failure');
|
|
166
|
+
logMessage(`\n${ansi.red}${ansi.bold}Build failed${ansi.reset}`);
|
|
167
|
+
logMessage(`\n State: ${build.state}`);
|
|
168
|
+
logMessage('');
|
|
169
|
+
const error = new Error(`Build failed with state: ${build.state}`);
|
|
170
|
+
(_p = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onError) === null || _p === void 0 ? void 0 : _p.call(callbacks, error);
|
|
171
|
+
if (!callbacks) {
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
150
174
|
}
|
|
151
175
|
}
|
|
152
176
|
catch (error) {
|
|
153
|
-
|
|
154
|
-
|
|
177
|
+
const err = error;
|
|
178
|
+
logMessage(`\n${ansi.red}Error: ${err.message}${ansi.reset}\n`);
|
|
179
|
+
(_q = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onError) === null || _q === void 0 ? void 0 : _q.call(callbacks, err);
|
|
180
|
+
if (!callbacks) {
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
155
183
|
}
|
|
156
184
|
}
|
|
185
|
+
// Export for use by server
|
|
186
|
+
export { handlePublishCommand };
|
|
157
187
|
async function promptBuildConfig(defaults) {
|
|
158
188
|
var _a, _b, _c, _d;
|
|
159
189
|
const answers = await inquirer.prompt([
|
|
@@ -7,7 +7,7 @@ export class ApiClient {
|
|
|
7
7
|
}
|
|
8
8
|
static async create() {
|
|
9
9
|
const config = await loadCliConfig();
|
|
10
|
-
if (!
|
|
10
|
+
if (!config.apiToken) {
|
|
11
11
|
throw new Error('Not authenticated. Run `tos auth` first.');
|
|
12
12
|
}
|
|
13
13
|
const baseUrl = process.env.TOS_API_URL || config.apiUrl || DEFAULT_API_URL;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export declare const apiTokenSchema: z.ZodString;
|
|
3
|
+
declare const cliConfigSchema: z.ZodObject<{
|
|
4
|
+
apiToken: z.ZodOptional<z.ZodString>;
|
|
5
|
+
apiUrl: z.ZodOptional<z.ZodURL>;
|
|
6
|
+
}, z.z.core.$strip>;
|
|
7
|
+
export type CliConfig = z.infer<typeof cliConfigSchema>;
|
|
8
|
+
export declare function loadCliConfig(): Promise<CliConfig>;
|
|
6
9
|
export declare function saveCliConfig(config: CliConfig): Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -1,23 +1,45 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import z from 'zod';
|
|
4
5
|
function getCliConfigDir() {
|
|
5
6
|
const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(homedir(), '.config');
|
|
6
|
-
return path.join(xdgConfig, 'telemetryos
|
|
7
|
+
return path.join(xdgConfig, 'telemetryos', 'cli');
|
|
7
8
|
}
|
|
8
9
|
function getCliConfigPath() {
|
|
9
10
|
return path.join(getCliConfigDir(), 'config.json');
|
|
10
11
|
}
|
|
12
|
+
export const apiTokenSchema = z
|
|
13
|
+
.string()
|
|
14
|
+
.regex(/^u[a-f0-9]{32}$/i, 'Token must start with "u" followed by 32 hex characters');
|
|
15
|
+
const cliConfigSchema = z.object({
|
|
16
|
+
apiToken: z.string().optional(),
|
|
17
|
+
apiUrl: z.url().optional(),
|
|
18
|
+
});
|
|
11
19
|
export async function loadCliConfig() {
|
|
12
20
|
try {
|
|
13
|
-
const
|
|
14
|
-
|
|
21
|
+
const configPath = getCliConfigPath();
|
|
22
|
+
const content = await readFile(configPath, 'utf-8');
|
|
23
|
+
const parsed = JSON.parse(content);
|
|
24
|
+
return cliConfigSchema.parse(parsed);
|
|
15
25
|
}
|
|
16
|
-
catch {
|
|
17
|
-
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT') {
|
|
28
|
+
return cliConfigSchema.parse({});
|
|
29
|
+
}
|
|
30
|
+
const configPath = getCliConfigPath();
|
|
31
|
+
if (error instanceof SyntaxError) {
|
|
32
|
+
throw new Error(`Invalid JSON in CLI config file (${configPath}):\n ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
if (error instanceof z.ZodError) {
|
|
35
|
+
const issues = error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n');
|
|
36
|
+
throw new Error(`Invalid CLI config file (${configPath}):\n${issues}`);
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
18
39
|
}
|
|
19
40
|
}
|
|
20
41
|
export async function saveCliConfig(config) {
|
|
42
|
+
const validated = cliConfigSchema.parse(config);
|
|
21
43
|
await mkdir(getCliConfigDir(), { recursive: true });
|
|
22
|
-
await writeFile(getCliConfigPath(), JSON.stringify(
|
|
44
|
+
await writeFile(getCliConfigPath(), JSON.stringify(validated, null, 2));
|
|
23
45
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetryos/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.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.11.0",
|
|
29
29
|
"@types/serve-handler": "^6.1.4",
|
|
30
30
|
"commander": "^14.0.0",
|
|
31
31
|
"ignore": "^6.0.2",
|
|
@@ -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
|