@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 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
@@ -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
- if (!input || input.trim().length === 0) {
16
- return 'Token cannot be empty';
17
- }
18
- const trimmed = input.trim();
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 === null || existingCliConfig === void 0 ? void 0 : existingCliConfig.apiToken;
23
+ const existingToken = existingCliConfig.apiToken;
31
24
  if (existingToken && !options.force) {
32
25
  const { confirm } = await inquirer.prompt([
33
26
  {
@@ -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
- const questions = [];
84
- // Always show the derived name with option to override
85
- // This provides transparency and control
86
- questions.push({
87
- type: 'input',
88
- name: 'name',
89
- message: 'Project name:',
90
- default: derivedName,
91
- validate: (input) => {
92
- const result = validateProjectName(input);
93
- return result === true ? true : result;
94
- },
95
- });
96
- if (!description)
97
- questions.push({
98
- type: 'input',
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: 'version',
114
- message: 'What is the version of your application?',
115
- default: '0.1.0',
102
+ name: 'name',
103
+ message: 'Project name:',
104
+ default: derivedName,
116
105
  validate: (input) => {
117
- if (/^\d+\.\d+\.\d+(-.+)?$/.test(input))
118
- return true;
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
- if (!template)
123
- questions.push({
124
- type: 'list',
125
- name: 'template',
126
- message: 'Which template would you like to use?',
127
- choices: [{ name: 'Vite + React + TypeScript', value: 'vite-react-typescript' }],
128
- });
129
- // Step 6: Prompt user
130
- const answers = await inquirer.prompt(questions);
131
- name = answers.name || name;
132
- version = answers.version || version;
133
- description = answers.description || description;
134
- author = answers.author || author;
135
- template = answers.template || template;
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 };
@@ -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
- console.log(`\n${ansi.cyan}Loading project configuration...${ansi.reset}`);
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
- console.error(`${ansi.red}Error: Application name is required${ansi.reset}`);
71
- process.exit(1);
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
- console.log(` Project: ${ansi.bold}${buildConfig.name}${ansi.reset}`);
79
+ logMessage(` Project: ${ansi.bold}${buildConfig.name}${ansi.reset}`);
74
80
  if (config.version) {
75
- console.log(` Version: ${config.version}`);
81
+ logMessage(` Version: ${config.version}`);
76
82
  }
77
83
  // Step 3: Initialize API client
78
- console.log(`\n${ansi.cyan}Authenticating...${ansi.reset}`);
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
- console.log(` ${ansi.green}Authenticated${ansi.reset}`);
87
+ logMessage(` ${ansi.green}Authenticated${ansi.reset}`);
81
88
  // Step 4: Create archive
82
- console.log(`\n${ansi.cyan}Archiving project...${ansi.reset}`);
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
- console.log(` ${ansi.dim}${msg}${ansi.reset}`);
92
+ logMessage(` ${ansi.dim}${msg}${ansi.reset}`);
85
93
  });
86
94
  // Step 5: Find or create application
87
- console.log(`\n${ansi.cyan}Checking for existing application...${ansi.reset}`);
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
- console.log(` Found existing application: ${ansi.dim}${app.id}${ansi.reset}`);
100
+ logMessage(` Found existing application: ${ansi.dim}${app.id}${ansi.reset}`);
93
101
  const specifiedOptions = getSpecifiedBuildOptions(options);
94
102
  if (specifiedOptions.length > 0) {
95
- console.log(` ${ansi.yellow}Warning: ${specifiedOptions.join(', ')} ignored for existing application${ansi.reset}`);
103
+ logMessage(` ${ansi.yellow}Warning: ${specifiedOptions.join(', ')} ignored for existing application${ansi.reset}`);
96
104
  }
97
105
  }
98
106
  else {
99
- console.log(` Creating new application...`);
107
+ logMessage(` Creating new application...`);
100
108
  const createRequest = {
101
109
  kind: 'uploaded',
102
110
  title: buildConfig.name,
103
- baseImage: (_a = buildConfig.baseImage) !== null && _a !== void 0 ? _a : APP_BUILD_DEFAULTS.baseImage,
111
+ baseImage: (_e = buildConfig.baseImage) !== null && _e !== void 0 ? _e : APP_BUILD_DEFAULTS.baseImage,
104
112
  baseImageRegistryAuth: '',
105
- buildWorkingPath: (_b = buildConfig.workingPath) !== null && _b !== void 0 ? _b : APP_BUILD_DEFAULTS.workingPath,
106
- buildScript: (_c = buildConfig.buildScript) !== null && _c !== void 0 ? _c : APP_BUILD_DEFAULTS.buildScript,
107
- buildOutputPath: (_d = buildConfig.buildOutput) !== null && _d !== void 0 ? _d : APP_BUILD_DEFAULTS.buildOutput,
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
- console.log(` ${ansi.green}Created application:${ansi.reset} ${ansi.dim}${app.id}${ansi.reset}`);
119
+ logMessage(` ${ansi.green}Created application:${ansi.reset} ${ansi.dim}${app.id}${ansi.reset}`);
112
120
  }
113
121
  // Step 6: Upload archive
114
- console.log(`\n${ansi.cyan}Uploading archive...${ansi.reset}`);
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
- console.log(` ${ansi.green}Upload complete${ansi.reset}`);
129
+ logMessage(` ${ansi.green}Upload complete${ansi.reset}`);
121
130
  // Step 7: Poll for build status and stream logs
122
- console.log(`\n${ansi.cyan}Building...${ansi.reset}`);
123
- console.log(`${ansi.white}Build Logs${ansi.reset}`);
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
- console.log(` ${ansi.dim}${line}${ansi.reset}`);
136
+ logMessage(` ${ansi.dim}${line}${ansi.reset}`);
127
137
  },
128
138
  onStateChange: () => { },
129
139
  onComplete: () => { },
130
140
  onError: (error) => {
131
- console.error(`${ansi.red}Polling error: ${error.message}${ansi.reset}`);
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
- console.log(`\n${ansi.green}${ansi.bold}Build successful!${ansi.reset}`);
137
- console.log(`\n Application: ${buildConfig.name}`);
138
- console.log(` Build: #${build.index}`);
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
- const duration = calculateDuration(build.startedAt, build.finishedAt);
141
- console.log(` Duration: ${duration}`);
154
+ duration = calculateDuration(build.startedAt, build.finishedAt);
155
+ logMessage(` Duration: ${duration}`);
142
156
  }
143
- console.log('');
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
- console.log(`\n${ansi.red}${ansi.bold}Build failed${ansi.reset}`);
147
- console.log(`\n State: ${build.state}`);
148
- console.log('');
149
- process.exit(1);
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
- console.error(`\n${ansi.red}Error: ${error.message}${ansi.reset}\n`);
154
- process.exit(1);
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 (!(config === null || config === void 0 ? void 0 : config.apiToken)) {
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
- export type CliConfig = {
2
- apiToken?: string;
3
- apiUrl?: string;
4
- };
5
- export declare function loadCliConfig(): Promise<CliConfig | null>;
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-cli');
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 content = await readFile(getCliConfigPath(), 'utf-8');
14
- return JSON.parse(content);
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
- return null;
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(config, null, 2));
44
+ await writeFile(getCliConfigPath(), JSON.stringify(validated, null, 2));
23
45
  }