@telemetryos/cli 1.17.0 → 1.17.1

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 (38) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/commands/auth.d.ts +2 -0
  3. package/dist/commands/auth.js +60 -0
  4. package/dist/commands/claude-code.d.ts +2 -0
  5. package/dist/commands/claude-code.js +29 -0
  6. package/dist/commands/init.d.ts +2 -0
  7. package/dist/commands/init.js +176 -0
  8. package/dist/commands/publish.d.ts +22 -0
  9. package/dist/commands/publish.js +238 -0
  10. package/dist/commands/root.d.ts +2 -0
  11. package/dist/commands/root.js +5 -0
  12. package/dist/commands/serve.d.ts +2 -0
  13. package/dist/commands/serve.js +7 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +13 -0
  16. package/dist/services/api-client.d.ts +18 -0
  17. package/dist/services/api-client.js +70 -0
  18. package/dist/services/archiver.d.ts +4 -0
  19. package/dist/services/archiver.js +65 -0
  20. package/dist/services/build-poller.d.ts +10 -0
  21. package/dist/services/build-poller.js +63 -0
  22. package/dist/services/cli-config.d.ts +10 -0
  23. package/dist/services/cli-config.js +45 -0
  24. package/dist/services/create-project.d.ts +13 -0
  25. package/dist/services/create-project.js +188 -0
  26. package/dist/services/project-config.d.ts +27 -0
  27. package/dist/services/project-config.js +54 -0
  28. package/dist/services/run-server.d.ts +5 -0
  29. package/dist/services/run-server.js +327 -0
  30. package/dist/types/applications.d.ts +44 -0
  31. package/dist/types/applications.js +1 -0
  32. package/dist/utils/ansi.d.ts +11 -0
  33. package/dist/utils/ansi.js +11 -0
  34. package/dist/utils/path-utils.d.ts +55 -0
  35. package/dist/utils/path-utils.js +99 -0
  36. package/dist/utils/template.d.ts +2 -0
  37. package/dist/utils/template.js +30 -0
  38. package/package.json +2 -2
@@ -0,0 +1,327 @@
1
+ import { spawn } from 'child_process';
2
+ import { access, readFile } from 'fs/promises';
3
+ import { fileURLToPath } from 'url';
4
+ import http from 'http';
5
+ import path from 'path';
6
+ import { createInterface } from 'readline/promises';
7
+ import serveHandler from 'serve-handler';
8
+ import pkg from '../../package.json' with { type: 'json' };
9
+ import { loadProjectConfig } from './project-config.js';
10
+ import { ansi, ansiRegex } from '../utils/ansi.js';
11
+ // import { handlePublishCommand } from '../commands/publish.js'
12
+ const IMAGE_MIME_TYPES = {
13
+ '.jpg': 'image/jpeg',
14
+ '.jpeg': 'image/jpeg',
15
+ '.png': 'image/png',
16
+ '.gif': 'image/gif',
17
+ '.svg': 'image/svg+xml',
18
+ '.webp': 'image/webp',
19
+ };
20
+ export async function runServer(projectPath, flags) {
21
+ printSplashScreen();
22
+ projectPath = path.resolve(process.cwd(), projectPath);
23
+ let projectConfig;
24
+ try {
25
+ projectConfig = await loadProjectConfig(projectPath);
26
+ }
27
+ catch (error) {
28
+ console.error(error.message);
29
+ process.exit(1);
30
+ }
31
+ await serveDevelopmentApplicationHostUI(projectPath, flags.port, projectConfig);
32
+ await serveTelemetryApplication(projectPath, projectConfig);
33
+ // Print ready message after Vite is confirmed ready
34
+ printServerInfo(flags.port);
35
+ }
36
+ async function serveDevelopmentApplicationHostUI(projectPath, port, projectConfig) {
37
+ const hostUiPath = await import.meta.resolve('@telemetryos/development-application-host-ui/dist');
38
+ const serveConfig = { public: fileURLToPath(hostUiPath) };
39
+ const server = http.createServer();
40
+ server.on('request', async (req, res) => {
41
+ var _a, _b;
42
+ const url = new URL(req.url, `http://${req.headers.origin}`);
43
+ if (url.pathname === '/__tos-config__') {
44
+ res.setHeader('Content-Type', 'application/json');
45
+ res.end(JSON.stringify(projectConfig));
46
+ return;
47
+ }
48
+ if (url.pathname === '/__tos-logo__') {
49
+ await serveImageFile(res, projectPath, projectConfig.logoPath, 'logo');
50
+ return;
51
+ }
52
+ if (url.pathname === '/__tos-thumbnail__') {
53
+ await serveImageFile(res, projectPath, projectConfig.thumbnailPath, 'thumbnail');
54
+ return;
55
+ }
56
+ if (url.pathname === '/__dev_proxy__' && req.method === 'POST' && res) {
57
+ let body = '';
58
+ for await (const chunk of req) {
59
+ body += chunk;
60
+ }
61
+ try {
62
+ const { url, method, headers, body: requestBody } = JSON.parse(body);
63
+ const response = await fetch(url, {
64
+ method,
65
+ headers,
66
+ body: requestBody !== null && requestBody !== void 0 ? requestBody : undefined,
67
+ });
68
+ const contentType = response.headers.get('content-type') || '';
69
+ const isJson = contentType.includes('application/json');
70
+ const isText = contentType.includes('text/') ||
71
+ contentType.includes('application/javascript') ||
72
+ contentType.includes('application/xml');
73
+ let responseBody;
74
+ let bodyType;
75
+ if (isJson) {
76
+ const text = await response.text();
77
+ responseBody = text ? JSON.parse(text) : null;
78
+ bodyType = 'json';
79
+ }
80
+ else if (isText) {
81
+ responseBody = await response.text();
82
+ bodyType = 'text';
83
+ }
84
+ else {
85
+ // Binary data - convert to base64 for JSON transport
86
+ const arrayBuffer = await response.arrayBuffer();
87
+ responseBody = Buffer.from(arrayBuffer).toString('base64');
88
+ bodyType = 'binary';
89
+ }
90
+ const responseHeaders = {};
91
+ response.headers.forEach((value, key) => {
92
+ responseHeaders[key] = value;
93
+ });
94
+ res.writeHead(200, { 'Content-Type': 'application/json' });
95
+ res.end(JSON.stringify({
96
+ success: true,
97
+ status: response.status,
98
+ statusText: response.statusText,
99
+ headers: responseHeaders,
100
+ body: responseBody,
101
+ bodyType,
102
+ ok: response.ok,
103
+ url: response.url,
104
+ }));
105
+ }
106
+ catch (error) {
107
+ res.writeHead(200, { 'Content-Type': 'application/json' });
108
+ res.end(JSON.stringify({
109
+ success: false,
110
+ errorMessage: `Proxy fetch failed: ${String(error)}`,
111
+ errorCause: error.cause ? String((_b = (_a = error.cause) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : error.cause) : undefined,
112
+ }));
113
+ }
114
+ return;
115
+ }
116
+ // TODO: Publish endpoint disabled until auth is working
117
+ // if (url.pathname === '/__publish__') {
118
+ // if (req.method !== 'GET') {
119
+ // res.statusCode = 405
120
+ // res.end('Method not allowed')
121
+ // return
122
+ // }
123
+ //
124
+ // // Set SSE headers
125
+ // res.setHeader('Content-Type', 'text/event-stream')
126
+ // res.setHeader('Cache-Control', 'no-cache')
127
+ // res.setHeader('Connection', 'keep-alive')
128
+ //
129
+ // const sendEvent = (event: string, data: any) => {
130
+ // res.write(`event: ${event}\n`)
131
+ // res.write(`data: ${JSON.stringify(data)}\n\n`)
132
+ // }
133
+ //
134
+ // try {
135
+ // sendEvent('state', { state: 'starting' })
136
+ //
137
+ // await handlePublishCommand(
138
+ // projectPath,
139
+ // {},
140
+ // {
141
+ // onLog: (line: string) => {
142
+ // sendEvent('log', { message: line })
143
+ // },
144
+ // onStateChange: (state: string) => {
145
+ // sendEvent('state', { state })
146
+ // },
147
+ // onComplete: (data: { success: boolean; buildIndex?: number; duration?: string }) => {
148
+ // sendEvent('complete', data)
149
+ // res.end()
150
+ // },
151
+ // onError: (error: Error) => {
152
+ // const isAuthError =
153
+ // error.message.includes('authenticate') ||
154
+ // error.message.includes('Not authenticated')
155
+ // sendEvent('error', {
156
+ // error: error.message,
157
+ // code: isAuthError ? 'AUTH_REQUIRED' : 'PUBLISH_FAILED',
158
+ // message: isAuthError
159
+ // ? "Run 'tos auth' in the terminal to authenticate"
160
+ // : error.message,
161
+ // })
162
+ // res.end()
163
+ // },
164
+ // },
165
+ // )
166
+ // } catch (error) {
167
+ // sendEvent('error', {
168
+ // error: (error as Error).message,
169
+ // code: 'UNEXPECTED_ERROR',
170
+ // message: (error as Error).message,
171
+ // })
172
+ // res.end()
173
+ // }
174
+ // return
175
+ // }
176
+ serveHandler(req, res, serveConfig).catch((err) => {
177
+ console.error('Error handling request:', err);
178
+ res.statusCode = 500;
179
+ res.end('Internal Server Error');
180
+ });
181
+ });
182
+ server.listen(port);
183
+ }
184
+ async function serveImageFile(res, projectPath, filePath, label) {
185
+ if (!filePath) {
186
+ res.statusCode = 404;
187
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
188
+ res.end(`No ${label} configured`);
189
+ return;
190
+ }
191
+ const projectRoot = path.resolve(projectPath);
192
+ const validatePath = (fullPath) => fullPath.startsWith(projectRoot + path.sep) || fullPath === projectRoot;
193
+ // Try public/ first (Vite convention), then project root (backward compat)
194
+ const publicPath = path.resolve(projectPath, 'public', filePath);
195
+ const rootPath = path.resolve(projectPath, filePath);
196
+ let resolvedPath;
197
+ if (validatePath(publicPath)) {
198
+ try {
199
+ await access(publicPath);
200
+ resolvedPath = publicPath;
201
+ }
202
+ catch { }
203
+ }
204
+ if (!resolvedPath && validatePath(rootPath)) {
205
+ resolvedPath = rootPath;
206
+ }
207
+ if (!resolvedPath) {
208
+ res.statusCode = 403;
209
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
210
+ res.end('Forbidden: path escapes project root');
211
+ return;
212
+ }
213
+ try {
214
+ const imageData = await readFile(resolvedPath);
215
+ const ext = path.extname(filePath).toLowerCase();
216
+ const contentType = IMAGE_MIME_TYPES[ext] || 'application/octet-stream';
217
+ res.setHeader('Content-Type', contentType);
218
+ res.end(imageData);
219
+ }
220
+ catch (error) {
221
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
222
+ if ((error === null || error === void 0 ? void 0 : error.code) === 'ENOENT') {
223
+ res.statusCode = 404;
224
+ res.end(`${label.charAt(0).toUpperCase() + label.slice(1)} file not found`);
225
+ }
226
+ else {
227
+ res.statusCode = 500;
228
+ res.end(`Error reading ${label}: ${(error === null || error === void 0 ? void 0 : error.message) || 'unknown error'}`);
229
+ }
230
+ }
231
+ }
232
+ async function serveTelemetryApplication(rootPath, projectConfig) {
233
+ return new Promise((resolve, reject) => {
234
+ var _a;
235
+ if (!((_a = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.devServer) === null || _a === void 0 ? void 0 : _a.runCommand)) {
236
+ console.log('No value in config at devServer.runCommand');
237
+ resolve();
238
+ return;
239
+ }
240
+ const runCommand = projectConfig.devServer.runCommand;
241
+ const binPath = path.join(rootPath, 'node_modules', '.bin');
242
+ const childProcess = spawn(runCommand, {
243
+ shell: true,
244
+ env: {
245
+ ...process.env,
246
+ FORCE_COLOR: '1',
247
+ PATH: `${binPath}${path.delimiter}${process.env.PATH}`,
248
+ },
249
+ stdio: ['ignore', 'pipe', 'pipe'],
250
+ cwd: rootPath,
251
+ });
252
+ const stdoutReadline = createInterface({
253
+ input: childProcess.stdout,
254
+ crlfDelay: Infinity,
255
+ });
256
+ const stderrReadline = createInterface({
257
+ input: childProcess.stderr,
258
+ crlfDelay: Infinity,
259
+ });
260
+ let cleaned = false;
261
+ const cleanup = () => {
262
+ if (cleaned)
263
+ return;
264
+ cleaned = true;
265
+ process.removeListener('exit', onParentExit);
266
+ clearTimeout(timeoutHandle);
267
+ childProcess.kill();
268
+ stdoutReadline.close();
269
+ stderrReadline.close();
270
+ };
271
+ const onParentExit = () => {
272
+ console.log('Shutting down development server...');
273
+ cleanup();
274
+ };
275
+ process.on('exit', onParentExit);
276
+ const timeoutHandle = setTimeout(() => {
277
+ cleanup();
278
+ reject(new Error('Vite dev server did not start within 30 seconds'));
279
+ }, 30000);
280
+ let viteReady = false;
281
+ stdoutReadline.on('line', (line) => {
282
+ console.log(`[application]: ${line}`);
283
+ // Detect Vite ready signal
284
+ if (!viteReady) {
285
+ const cleanLine = line.replace(ansiRegex, '');
286
+ if (cleanLine.includes('VITE') && cleanLine.includes('ready in')) {
287
+ viteReady = true;
288
+ clearTimeout(timeoutHandle);
289
+ resolve();
290
+ }
291
+ }
292
+ });
293
+ stderrReadline.on('line', (line) => {
294
+ console.error(`[application]: ${line}`);
295
+ });
296
+ childProcess.on('error', (error) => {
297
+ if (!viteReady) {
298
+ cleanup();
299
+ reject(error);
300
+ }
301
+ });
302
+ childProcess.on('close', (code) => {
303
+ if (!viteReady) {
304
+ cleanup();
305
+ reject(new Error(`Dev server process exited with code ${code} before becoming ready`));
306
+ }
307
+ });
308
+ });
309
+ }
310
+ function printSplashScreen() {
311
+ console.log(`
312
+
313
+ ${ansi.white} █ █ █ ${ansi.yellow}▄▀▀▀▄ ▄▀▀▀▄
314
+ ${ansi.white} █ █ █ ${ansi.yellow}█ █ █
315
+ ${ansi.white}▀█▀ ▄▀▀▄ █ ▄▀▀▄ █▀▄▀▄ ▄▀▀▄ ▀█▀ █▄▀ █ █ ${ansi.yellow}█ █ ▀▀▀▄
316
+ ${ansi.white} █ █▀▀▀ █ █▀▀▀ █ █ █ █▀▀▀ █ █ █ █ ${ansi.yellow}█ █ █
317
+ ${ansi.white} ▀▄ ▀▄▄▀ █ ▀▄▄▀ █ █ █ ▀▄▄▀ ▀▄ █ █ ${ansi.yellow}▀▄▄▄▀ ▀▄▄▄▀
318
+ ${ansi.white} ▄▀ ${ansi.reset}
319
+ v${pkg.version}`);
320
+ }
321
+ function printServerInfo(port) {
322
+ console.log(`
323
+ ╔═══════════════════════════════════════════════════════════╗
324
+ ║ ${ansi.bold}Development environment running at: ${ansi.cyan}http://localhost:${port}${ansi.reset} ║
325
+ ╚═══════════════════════════════════════════════════════════╝
326
+ `);
327
+ }
@@ -0,0 +1,44 @@
1
+ export type ApplicationKind = 'git' | 'github' | 'uploaded' | null;
2
+ export type Application = {
3
+ id: string;
4
+ title: string;
5
+ description: string;
6
+ kind: ApplicationKind;
7
+ baseImage: string;
8
+ buildWorkingPath?: string;
9
+ buildScript?: string;
10
+ buildOutputPath?: string;
11
+ buildEnvironmentVariables?: Record<string, string>;
12
+ versions?: ApplicationVersion[];
13
+ createdAt?: string;
14
+ updatedAt?: string;
15
+ };
16
+ export type ApplicationVersion = {
17
+ name?: string;
18
+ version?: string;
19
+ applicationId: string;
20
+ buildId: string;
21
+ applicationSpecifier: string;
22
+ publishedAt: string;
23
+ };
24
+ export type ApplicationBuild = {
25
+ id: string;
26
+ applicationId: string;
27
+ index: number;
28
+ state: 'pending' | 'building' | 'success' | 'failure' | 'failed' | 'cancelled';
29
+ logs: string[];
30
+ scheduledAt?: string;
31
+ startedAt?: string;
32
+ finishedAt?: string;
33
+ };
34
+ export type CreateApplicationRequest = {
35
+ kind: 'uploaded';
36
+ title: string;
37
+ description?: string;
38
+ baseImage: string;
39
+ baseImageRegistryAuth: string;
40
+ buildWorkingPath: string;
41
+ buildScript: string;
42
+ buildOutputPath: string;
43
+ buildEnvironmentVariables?: Record<string, string>;
44
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ export declare const ansi: {
2
+ readonly white: "\u001B[37m";
3
+ readonly yellow: "\u001B[33m";
4
+ readonly green: "\u001B[32m";
5
+ readonly cyan: "\u001B[36m";
6
+ readonly red: "\u001B[31m";
7
+ readonly bold: "\u001B[1m";
8
+ readonly dim: "\u001B[2m";
9
+ readonly reset: "\u001B[0m";
10
+ };
11
+ export declare const ansiRegex: RegExp;
@@ -0,0 +1,11 @@
1
+ export const ansi = {
2
+ white: '\u001b[37m',
3
+ yellow: '\u001b[33m',
4
+ green: '\u001b[32m',
5
+ cyan: '\u001b[36m',
6
+ red: '\u001b[31m',
7
+ bold: '\u001b[1m',
8
+ dim: '\u001b[2m',
9
+ reset: '\u001b[0m',
10
+ };
11
+ export const ansiRegex = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g');
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Converts a string to kebab-case
3
+ *
4
+ * @param str - The string to convert
5
+ * @returns The kebab-cased string
6
+ *
7
+ * @example
8
+ * toKebabCase('MyApp') // 'my-app'
9
+ * toKebabCase('my_app') // 'my-app'
10
+ * toKebabCase('My App!') // 'my-app'
11
+ * toKebabCase('my--app') // 'my-app'
12
+ */
13
+ export declare function toKebabCase(str: string): string;
14
+ /**
15
+ * Derives a project name from a given path
16
+ *
17
+ * @param projectPath - The path to derive the name from
18
+ * @param currentWorkingDirectory - The current working directory (defaults to process.cwd())
19
+ * @returns The derived project name in kebab-case
20
+ *
21
+ * @example
22
+ * deriveProjectName('my-project') // 'my-project'
23
+ * deriveProjectName('apps/MyApp') // 'my-app'
24
+ * deriveProjectName('./', '/Users/test/MyProject') // 'my-project'
25
+ * deriveProjectName('../parent') // 'parent'
26
+ * deriveProjectName('/absolute/path/to/app') // 'app'
27
+ */
28
+ export declare function deriveProjectName(projectPath: string, currentWorkingDirectory?: string): string;
29
+ /**
30
+ * Resolves a project path and derives the name
31
+ *
32
+ * @param projectPath - The path to resolve
33
+ * @param currentWorkingDirectory - The current working directory (defaults to process.cwd())
34
+ * @returns An object containing the resolved path and derived name
35
+ *
36
+ * @example
37
+ * resolveProjectPathAndName('apps/MyApp')
38
+ * // { resolvedPath: '/Users/user/cwd/apps/MyApp', derivedName: 'my-app' }
39
+ */
40
+ export declare function resolveProjectPathAndName(projectPath: string, currentWorkingDirectory?: string): {
41
+ resolvedPath: string;
42
+ derivedName: string;
43
+ };
44
+ /**
45
+ * Validates a project name according to npm package name requirements
46
+ *
47
+ * @param name - The project name to validate
48
+ * @returns true if valid, or an error message string if invalid
49
+ *
50
+ * @example
51
+ * validateProjectName('my-app') // true
52
+ * validateProjectName('') // 'Project name cannot be empty'
53
+ * validateProjectName('MyApp') // 'Project name must contain only lowercase letters, numbers, and hyphens'
54
+ */
55
+ export declare function validateProjectName(name: string): true | string;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export declare const templatesDir: string;
2
+ export declare function copyDir(source: string, destination: string, replacements: Record<string, string>, progressFn: (createdFilePath: string) => void): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ export const templatesDir = path.join(import.meta.dirname, '../../templates');
4
+ const ignoredTemplateFiles = ['.DS_Store', 'thumbs.db', 'node_modules', '.git', 'dist'];
5
+ const dotfileNames = ['_gitignore', '_claude'];
6
+ export async function copyDir(source, destination, replacements, progressFn) {
7
+ const dirListing = await fs.readdir(source);
8
+ for (const dirEntry of dirListing) {
9
+ if (ignoredTemplateFiles.includes(dirEntry))
10
+ continue;
11
+ const sourcePath = path.join(source, dirEntry);
12
+ const destinationPath = path.join(destination, dotfileNames.includes(dirEntry) ? `.${dirEntry.slice(1)}` : dirEntry);
13
+ const stats = await fs.stat(sourcePath);
14
+ if (stats.isDirectory()) {
15
+ await fs.mkdir(destinationPath, { recursive: true });
16
+ await copyDir(sourcePath, destinationPath, replacements, progressFn);
17
+ }
18
+ else if (stats.isFile()) {
19
+ await copyFile(sourcePath, destinationPath, replacements, progressFn);
20
+ }
21
+ }
22
+ }
23
+ async function copyFile(source, destination, replacements, progressFn) {
24
+ let contents = await fs.readFile(source, 'utf-8');
25
+ for (const [key, value] of Object.entries(replacements)) {
26
+ contents = contents.replace(new RegExp(`{{${key}}}`, 'g'), value);
27
+ }
28
+ await fs.writeFile(destination, contents, 'utf-8');
29
+ progressFn(destination);
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetryos/cli",
3
- "version": "1.17.0",
3
+ "version": "1.17.1",
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.17.0",
28
+ "@telemetryos/development-application-host-ui": "^1.17.1",
29
29
  "@types/serve-handler": "^6.1.4",
30
30
  "commander": "^14.0.0",
31
31
  "ignore": "^6.0.2",