@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @telemetryos/cli
|
|
2
2
|
|
|
3
|
+
## 1.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Revamp the dev host
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- @telemetryos/development-application-host-ui@1.12.0
|
|
13
|
+
|
|
14
|
+
## 1.11.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- Added media select component, improved tos init tos publish and tos auth commands, add new store hook
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Updated dependencies
|
|
23
|
+
- @telemetryos/development-application-host-ui@1.11.0
|
|
24
|
+
|
|
3
25
|
## 1.10.0
|
|
4
26
|
|
|
5
27
|
### 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
|
}
|