@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.
- package/CHANGELOG.md +8 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +60 -0
- package/dist/commands/claude-code.d.ts +2 -0
- package/dist/commands/claude-code.js +29 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +176 -0
- package/dist/commands/publish.d.ts +22 -0
- package/dist/commands/publish.js +238 -0
- package/dist/commands/root.d.ts +2 -0
- package/dist/commands/root.js +5 -0
- package/dist/commands/serve.d.ts +2 -0
- package/dist/commands/serve.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/services/api-client.d.ts +18 -0
- package/dist/services/api-client.js +70 -0
- package/dist/services/archiver.d.ts +4 -0
- package/dist/services/archiver.js +65 -0
- package/dist/services/build-poller.d.ts +10 -0
- package/dist/services/build-poller.js +63 -0
- package/dist/services/cli-config.d.ts +10 -0
- package/dist/services/cli-config.js +45 -0
- package/dist/services/create-project.d.ts +13 -0
- package/dist/services/create-project.js +188 -0
- package/dist/services/project-config.d.ts +27 -0
- package/dist/services/project-config.js +54 -0
- package/dist/services/run-server.d.ts +5 -0
- package/dist/services/run-server.js +327 -0
- package/dist/types/applications.d.ts +44 -0
- package/dist/types/applications.js +1 -0
- package/dist/utils/ansi.d.ts +11 -0
- package/dist/utils/ansi.js +11 -0
- package/dist/utils/path-utils.d.ts +55 -0
- package/dist/utils/path-utils.js +99 -0
- package/dist/utils/template.d.ts +2 -0
- package/dist/utils/template.js +30 -0
- 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,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.
|
|
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.
|
|
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",
|