hexbus 0.1.0 → 0.1.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/README.md +133 -10
- package/dist/index.d.mts +1216 -36
- package/dist/index.mjs +959 -277
- package/package.json +45 -46
package/dist/index.mjs
CHANGED
|
@@ -5,42 +5,44 @@ import fs from "node:fs/promises";
|
|
|
5
5
|
import * as path$1 from "node:path";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
8
|
+
import { spinner } from "@clack/prompts";
|
|
8
9
|
import { loadConfig } from "c12";
|
|
9
|
-
import
|
|
10
|
+
import { promisify } from "node:util";
|
|
10
11
|
import figlet from "figlet";
|
|
11
12
|
import * as os from "node:os";
|
|
13
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
12
14
|
//#region src/detection.ts
|
|
13
15
|
const LOCK_FILE_MAP = {
|
|
14
|
-
"bun.lockb": "bun",
|
|
15
16
|
"bun.lock": "bun",
|
|
17
|
+
"bun.lockb": "bun",
|
|
18
|
+
"package-lock.json": "npm",
|
|
16
19
|
"pnpm-lock.yaml": "pnpm",
|
|
17
|
-
"yarn.lock": "yarn"
|
|
18
|
-
"package-lock.json": "npm"
|
|
20
|
+
"yarn.lock": "yarn"
|
|
19
21
|
};
|
|
20
22
|
const PACKAGE_MANAGER_CONFIG = {
|
|
21
23
|
bun: {
|
|
22
|
-
installCommand: "bun install",
|
|
23
24
|
addCommand: "bun add",
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
execCommand: "bunx",
|
|
26
|
+
installCommand: "bun install",
|
|
27
|
+
runCommand: "bun run"
|
|
28
|
+
},
|
|
29
|
+
npm: {
|
|
30
|
+
addCommand: "npm install",
|
|
31
|
+
execCommand: "npx",
|
|
32
|
+
installCommand: "npm install",
|
|
33
|
+
runCommand: "npm run"
|
|
26
34
|
},
|
|
27
35
|
pnpm: {
|
|
28
|
-
installCommand: "pnpm install",
|
|
29
36
|
addCommand: "pnpm add",
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
execCommand: "pnpm dlx",
|
|
38
|
+
installCommand: "pnpm install",
|
|
39
|
+
runCommand: "pnpm"
|
|
32
40
|
},
|
|
33
41
|
yarn: {
|
|
34
|
-
installCommand: "yarn",
|
|
35
42
|
addCommand: "yarn add",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
npm: {
|
|
40
|
-
installCommand: "npm install",
|
|
41
|
-
addCommand: "npm install",
|
|
42
|
-
runCommand: "npm run",
|
|
43
|
-
execCommand: "npx"
|
|
43
|
+
execCommand: "yarn dlx",
|
|
44
|
+
installCommand: "yarn",
|
|
45
|
+
runCommand: "yarn"
|
|
44
46
|
}
|
|
45
47
|
};
|
|
46
48
|
async function readPackageJson(projectRoot) {
|
|
@@ -48,6 +50,21 @@ async function readPackageJson(projectRoot) {
|
|
|
48
50
|
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
49
51
|
return JSON.parse(content);
|
|
50
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Detects common frontend frameworks from project dependencies.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* Detection reads the nearest project's `package.json` and looks at
|
|
58
|
+
* dependencies and devDependencies. It recognizes Next.js, Remix, Vite React,
|
|
59
|
+
* Gatsby, generic React, Tailwind CSS, and a configured core fallback.
|
|
60
|
+
*
|
|
61
|
+
* @typeParam TPackage - Product-specific package identifier returned in
|
|
62
|
+
* `FrameworkDetectionResult.pkg`.
|
|
63
|
+
* @param projectRoot - Directory containing the package.json to inspect.
|
|
64
|
+
* @param logger - Optional logger for debug diagnostics.
|
|
65
|
+
* @param packageMap - Product packages to select for detected frameworks.
|
|
66
|
+
* @returns Framework metadata and the selected product package identifier.
|
|
67
|
+
*/
|
|
51
68
|
async function detectFramework(projectRoot, logger, packageMap = {}) {
|
|
52
69
|
try {
|
|
53
70
|
logger?.debug(`Detecting framework in ${projectRoot}`);
|
|
@@ -86,8 +103,8 @@ async function detectFramework(projectRoot, logger, packageMap = {}) {
|
|
|
86
103
|
return {
|
|
87
104
|
framework,
|
|
88
105
|
frameworkVersion,
|
|
89
|
-
pkg,
|
|
90
106
|
hasReact,
|
|
107
|
+
pkg,
|
|
91
108
|
reactVersion: reactVersion ?? null,
|
|
92
109
|
tailwindVersion
|
|
93
110
|
};
|
|
@@ -96,13 +113,25 @@ async function detectFramework(projectRoot, logger, packageMap = {}) {
|
|
|
96
113
|
return {
|
|
97
114
|
framework: null,
|
|
98
115
|
frameworkVersion: null,
|
|
99
|
-
pkg: packageMap.core ?? null,
|
|
100
116
|
hasReact: false,
|
|
117
|
+
pkg: packageMap.core ?? null,
|
|
101
118
|
reactVersion: null,
|
|
102
119
|
tailwindVersion: null
|
|
103
120
|
};
|
|
104
121
|
}
|
|
105
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Finds the nearest project root by walking up to a package.json.
|
|
125
|
+
*
|
|
126
|
+
* @remarks
|
|
127
|
+
* The search is capped at ten directory levels to avoid surprising filesystem
|
|
128
|
+
* traversal. When no root is found, the original current working directory is
|
|
129
|
+
* returned and a warning is logged.
|
|
130
|
+
*
|
|
131
|
+
* @param cwd - Directory where invocation started.
|
|
132
|
+
* @param logger - Optional logger for warning output.
|
|
133
|
+
* @returns The detected project root or `cwd` as a fallback.
|
|
134
|
+
*/
|
|
106
135
|
async function detectProjectRoot(cwd, logger) {
|
|
107
136
|
let projectRoot = cwd;
|
|
108
137
|
let previousDirectory = "";
|
|
@@ -143,30 +172,45 @@ async function promptForPackageManager(logger) {
|
|
|
143
172
|
message: "Which package manager do you use?",
|
|
144
173
|
options: [
|
|
145
174
|
{
|
|
146
|
-
|
|
175
|
+
hint: "Fast all-in-one toolkit",
|
|
147
176
|
label: "bun",
|
|
148
|
-
|
|
177
|
+
value: "bun"
|
|
149
178
|
},
|
|
150
179
|
{
|
|
151
|
-
|
|
180
|
+
hint: "Fast, disk space efficient",
|
|
152
181
|
label: "pnpm",
|
|
153
|
-
|
|
182
|
+
value: "pnpm"
|
|
154
183
|
},
|
|
155
184
|
{
|
|
156
|
-
|
|
185
|
+
hint: "Classic package manager",
|
|
157
186
|
label: "yarn",
|
|
158
|
-
|
|
187
|
+
value: "yarn"
|
|
159
188
|
},
|
|
160
189
|
{
|
|
161
|
-
|
|
190
|
+
hint: "Default Node.js package manager",
|
|
162
191
|
label: "npm",
|
|
163
|
-
|
|
192
|
+
value: "npm"
|
|
164
193
|
}
|
|
165
194
|
]
|
|
166
195
|
});
|
|
167
196
|
if (p.isCancel(result)) throw new Error("Package manager selection cancelled");
|
|
168
197
|
return result;
|
|
169
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Detects the package manager used by a project.
|
|
201
|
+
*
|
|
202
|
+
* @remarks
|
|
203
|
+
* Detection prefers lockfiles, then the `packageManager` field in
|
|
204
|
+
* `package.json`, then an optional interactive prompt, and finally `npm`.
|
|
205
|
+
*
|
|
206
|
+
* @param projectRoot - Directory to inspect for lockfiles and package.json.
|
|
207
|
+
* @param logger - Optional logger for debug output.
|
|
208
|
+
* @param options - Detection options.
|
|
209
|
+
* @returns Command templates for the detected package manager.
|
|
210
|
+
*
|
|
211
|
+
* @throws When interactive prompting is enabled and the user cancels package
|
|
212
|
+
* manager selection.
|
|
213
|
+
*/
|
|
170
214
|
async function detectPackageManager(projectRoot, logger, options) {
|
|
171
215
|
let pm = await detectFromLockFile(projectRoot, logger);
|
|
172
216
|
if (!pm) pm = await detectFromPackageJson(projectRoot, logger);
|
|
@@ -180,55 +224,129 @@ async function detectPackageManager(projectRoot, logger, options) {
|
|
|
180
224
|
...PACKAGE_MANAGER_CONFIG[pm]
|
|
181
225
|
};
|
|
182
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Builds a dependency installation command for the detected package manager.
|
|
229
|
+
*
|
|
230
|
+
* @param pm - Package manager command templates.
|
|
231
|
+
* @param packages - Package names to install.
|
|
232
|
+
* @param options - Install command options.
|
|
233
|
+
* @returns A shell command string suitable for display or execution.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* getInstallCommand(pm, ['typescript'], { dev: true });
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
183
240
|
function getInstallCommand(pm, packages, options) {
|
|
184
241
|
const pkgList = packages.join(" ");
|
|
185
|
-
|
|
186
|
-
|
|
242
|
+
let devFlag = "";
|
|
243
|
+
if (options?.dev) devFlag = pm.name === "npm" ? "--save-dev" : "-D";
|
|
244
|
+
return `${pm.addCommand} ${devFlag} ${pkgList}`.trim().replaceAll(/\s+/g, " ");
|
|
187
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Builds a package-script command for the detected package manager.
|
|
248
|
+
*
|
|
249
|
+
* @param pm - Package manager command templates.
|
|
250
|
+
* @param script - Package script name to run.
|
|
251
|
+
* @returns A shell command string.
|
|
252
|
+
*/
|
|
188
253
|
function getRunCommand(pm, script) {
|
|
189
254
|
return `${pm.runCommand} ${script}`;
|
|
190
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Builds a one-off binary execution command for the detected package manager.
|
|
258
|
+
*
|
|
259
|
+
* @param pm - Package manager command templates.
|
|
260
|
+
* @param binary - Binary name to execute.
|
|
261
|
+
* @param args - Optional arguments appended after the binary name.
|
|
262
|
+
* @returns A shell command string.
|
|
263
|
+
*/
|
|
191
264
|
function getExecCommand(pm, binary, args) {
|
|
192
265
|
const argString = args?.join(" ") || "";
|
|
193
266
|
return `${pm.execCommand} ${binary} ${argString}`.trim();
|
|
194
267
|
}
|
|
195
268
|
//#endregion
|
|
196
269
|
//#region src/errors.ts
|
|
270
|
+
/**
|
|
271
|
+
* Built-in errors used by the Hexbus runtime.
|
|
272
|
+
*/
|
|
197
273
|
const DEFAULT_ERROR_CATALOG = {
|
|
274
|
+
CANCELLED: {
|
|
275
|
+
code: "CANCELLED",
|
|
276
|
+
message: "Operation cancelled"
|
|
277
|
+
},
|
|
278
|
+
COMMAND_NOT_FOUND: {
|
|
279
|
+
code: "COMMAND_NOT_FOUND",
|
|
280
|
+
hint: "Run --help to see available commands",
|
|
281
|
+
message: "Unknown command"
|
|
282
|
+
},
|
|
198
283
|
CONFIG_NOT_FOUND: {
|
|
199
284
|
code: "CONFIG_NOT_FOUND",
|
|
200
|
-
|
|
201
|
-
|
|
285
|
+
hint: "Run the setup command to create a configuration",
|
|
286
|
+
message: "Configuration not found"
|
|
202
287
|
},
|
|
203
288
|
FLAG_VALUE_REQUIRED: {
|
|
204
289
|
code: "FLAG_VALUE_REQUIRED",
|
|
205
290
|
message: "Flag requires a value"
|
|
206
291
|
},
|
|
207
|
-
COMMAND_NOT_FOUND: {
|
|
208
|
-
code: "COMMAND_NOT_FOUND",
|
|
209
|
-
message: "Unknown command",
|
|
210
|
-
hint: "Run --help to see available commands"
|
|
211
|
-
},
|
|
212
|
-
CANCELLED: {
|
|
213
|
-
code: "CANCELLED",
|
|
214
|
-
message: "Operation cancelled"
|
|
215
|
-
},
|
|
216
292
|
UNKNOWN_ERROR: {
|
|
217
293
|
code: "UNKNOWN_ERROR",
|
|
218
294
|
message: "An unexpected error occurred"
|
|
219
295
|
}
|
|
220
296
|
};
|
|
221
297
|
let activeCatalog = { ...DEFAULT_ERROR_CATALOG };
|
|
298
|
+
/**
|
|
299
|
+
* Adds or replaces entries in the active error catalog.
|
|
300
|
+
*
|
|
301
|
+
* @remarks
|
|
302
|
+
* This mutates process-local catalog state. Product CLIs should call it once
|
|
303
|
+
* during startup before command actions create `CliError` instances.
|
|
304
|
+
*
|
|
305
|
+
* @param entries - Error entries keyed by their stable code.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```ts
|
|
309
|
+
* extendErrorCatalog({
|
|
310
|
+
* PROJECT_NOT_READY: {
|
|
311
|
+
* code: 'PROJECT_NOT_READY',
|
|
312
|
+
* message: 'Project is not ready',
|
|
313
|
+
* hint: 'Run init before running this command.',
|
|
314
|
+
* },
|
|
315
|
+
* });
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
222
318
|
function extendErrorCatalog(entries) {
|
|
223
319
|
activeCatalog = {
|
|
224
320
|
...activeCatalog,
|
|
225
321
|
...entries
|
|
226
322
|
};
|
|
227
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Error type that carries a catalog entry and optional structured context.
|
|
326
|
+
*
|
|
327
|
+
* @remarks
|
|
328
|
+
* `CliError` keeps command code focused on stable error codes while shared
|
|
329
|
+
* handlers render the configured message, hint, and documentation link.
|
|
330
|
+
*/
|
|
228
331
|
var CliError = class CliError extends Error {
|
|
332
|
+
/**
|
|
333
|
+
* Error code requested by the caller.
|
|
334
|
+
*/
|
|
229
335
|
code;
|
|
336
|
+
/**
|
|
337
|
+
* Structured diagnostic details attached by the caller.
|
|
338
|
+
*/
|
|
230
339
|
context;
|
|
340
|
+
/**
|
|
341
|
+
* Catalog entry used to render this error.
|
|
342
|
+
*/
|
|
231
343
|
entry;
|
|
344
|
+
/**
|
|
345
|
+
* Creates a catalog-backed CLI error.
|
|
346
|
+
*
|
|
347
|
+
* @param code - Built-in or product-defined error code.
|
|
348
|
+
* @param context - Optional diagnostic details for rendering or telemetry.
|
|
349
|
+
*/
|
|
232
350
|
constructor(code, context) {
|
|
233
351
|
const entry = activeCatalog[code] ?? activeCatalog.UNKNOWN_ERROR ?? DEFAULT_ERROR_CATALOG.UNKNOWN_ERROR;
|
|
234
352
|
super(entry.message);
|
|
@@ -238,13 +356,27 @@ var CliError = class CliError extends Error {
|
|
|
238
356
|
this.entry = entry;
|
|
239
357
|
if (Error.captureStackTrace) Error.captureStackTrace(this, CliError);
|
|
240
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Renders the error message, hint, and docs link through a logger.
|
|
361
|
+
*
|
|
362
|
+
* @param logger - Logger used for user-facing output.
|
|
363
|
+
*/
|
|
241
364
|
display(logger) {
|
|
242
|
-
let message = this.entry
|
|
365
|
+
let { message } = this.entry;
|
|
243
366
|
if (this.context?.details) message += `: ${this.context.details}`;
|
|
244
367
|
logger.error(message);
|
|
245
368
|
if (this.entry.hint) logger.info(`Hint: ${this.entry.hint}`);
|
|
246
369
|
if (this.entry.docs) logger.info(`Docs: ${this.entry.docs}`);
|
|
247
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Normalizes an unknown thrown value into a `CliError`.
|
|
373
|
+
*
|
|
374
|
+
* @param error - Value caught from a `try`/`catch` block.
|
|
375
|
+
* @param fallbackCode - Catalog code to use when `error` is not already a
|
|
376
|
+
* `CliError`.
|
|
377
|
+
* @returns `error` unchanged when it is already a `CliError`, otherwise a
|
|
378
|
+
* wrapped `CliError`.
|
|
379
|
+
*/
|
|
248
380
|
static from(error, fallbackCode = "UNKNOWN_ERROR") {
|
|
249
381
|
if (error instanceof CliError) return error;
|
|
250
382
|
return new CliError(fallbackCode, {
|
|
@@ -253,31 +385,59 @@ var CliError = class CliError extends Error {
|
|
|
253
385
|
});
|
|
254
386
|
}
|
|
255
387
|
};
|
|
388
|
+
/**
|
|
389
|
+
* Narrows an unknown value to a `CliError`.
|
|
390
|
+
*
|
|
391
|
+
* @param error - Value to inspect.
|
|
392
|
+
* @param code - Optional code that must match the error.
|
|
393
|
+
* @returns `true` when the value is a `CliError` and, if provided, matches the
|
|
394
|
+
* requested code.
|
|
395
|
+
*/
|
|
256
396
|
function isCliError(error, code) {
|
|
257
397
|
if (!(error instanceof CliError)) return false;
|
|
258
398
|
if (code) return error.code === code;
|
|
259
399
|
return true;
|
|
260
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Creates shared process-ending error and cancellation handlers.
|
|
403
|
+
*
|
|
404
|
+
* @param logger - Logger used to render messages.
|
|
405
|
+
* @param telemetry - Optional telemetry sink for failed command errors.
|
|
406
|
+
* @returns Error handlers suitable for `CliContext.error`.
|
|
407
|
+
*/
|
|
261
408
|
function createErrorHandlers(logger, telemetry) {
|
|
262
409
|
return {
|
|
410
|
+
handleCancel(message = "Operation cancelled", context) {
|
|
411
|
+
logger.warn(message);
|
|
412
|
+
if (context?.command) logger.info(`Command: ${context.command}`);
|
|
413
|
+
process.exit(0);
|
|
414
|
+
},
|
|
263
415
|
handleError(error, command) {
|
|
264
416
|
const cliError = CliError.from(error);
|
|
265
417
|
try {
|
|
266
418
|
telemetry?.trackError(cliError, command);
|
|
267
|
-
} catch (
|
|
268
|
-
const message =
|
|
419
|
+
} catch (telemetryError) {
|
|
420
|
+
const message = telemetryError instanceof Error ? telemetryError.message : String(telemetryError);
|
|
269
421
|
logger.warn(`Failed to track error telemetry: ${message}`);
|
|
270
422
|
}
|
|
271
423
|
cliError.display(logger);
|
|
272
424
|
process.exit(1);
|
|
273
|
-
},
|
|
274
|
-
handleCancel(message = "Operation cancelled", context) {
|
|
275
|
-
logger.warn(message);
|
|
276
|
-
if (context?.command) logger.info(`Command: ${context.command}`);
|
|
277
|
-
process.exit(0);
|
|
278
425
|
}
|
|
279
426
|
};
|
|
280
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Wraps an async function with CLI-style error rendering and process exit.
|
|
430
|
+
*
|
|
431
|
+
* @remarks
|
|
432
|
+
* This helper is useful near process entrypoints. Library-style code should
|
|
433
|
+
* usually throw `CliError` and let the caller decide when to exit.
|
|
434
|
+
*
|
|
435
|
+
* @typeParam T - Async function type to preserve.
|
|
436
|
+
* @param fn - Async function to run.
|
|
437
|
+
* @param logger - Logger used to render caught errors.
|
|
438
|
+
* @param context - Optional command metadata shown after an error.
|
|
439
|
+
* @returns A function with the same call signature as `fn`.
|
|
440
|
+
*/
|
|
281
441
|
function withErrorHandling(fn, logger, context) {
|
|
282
442
|
return (async (...args) => {
|
|
283
443
|
try {
|
|
@@ -290,19 +450,141 @@ function withErrorHandling(fn, logger, context) {
|
|
|
290
450
|
});
|
|
291
451
|
}
|
|
292
452
|
//#endregion
|
|
453
|
+
//#region src/color.ts
|
|
454
|
+
const replaceClose = (string, close, replace, startIndex) => {
|
|
455
|
+
let result = "";
|
|
456
|
+
let cursor = 0;
|
|
457
|
+
let index = startIndex;
|
|
458
|
+
do {
|
|
459
|
+
result += string.slice(cursor, index) + replace;
|
|
460
|
+
cursor = index + close.length;
|
|
461
|
+
index = string.indexOf(close, cursor);
|
|
462
|
+
} while (index !== -1);
|
|
463
|
+
return result + string.slice(cursor);
|
|
464
|
+
};
|
|
465
|
+
const createFormatter = (open, close, replace = open) => (input) => {
|
|
466
|
+
const string = String(input);
|
|
467
|
+
const index = string.indexOf(close, open.length);
|
|
468
|
+
return index === -1 ? open + string + close : open + replaceClose(string, close, replace, index) + close;
|
|
469
|
+
};
|
|
470
|
+
const createPlainFormatter = () => String;
|
|
471
|
+
/**
|
|
472
|
+
* Detects whether ANSI color output should be enabled.
|
|
473
|
+
*
|
|
474
|
+
* @remarks
|
|
475
|
+
* Detection follows common CLI conventions: `NO_COLOR`, `--no-color`, and
|
|
476
|
+
* `FORCE_COLOR=0` disable colors; `FORCE_COLOR` and `--color` enable colors;
|
|
477
|
+
* Windows defaults to enabled; otherwise TTY, non-dumb terminals, and CI
|
|
478
|
+
* environments are considered color-capable.
|
|
479
|
+
*
|
|
480
|
+
* @param options - Optional process-like inputs for detection.
|
|
481
|
+
* @returns `true` when color formatters should emit ANSI escape codes.
|
|
482
|
+
*/
|
|
483
|
+
function detectColorSupport(options = {}) {
|
|
484
|
+
const argv = options.argv ?? process.argv;
|
|
485
|
+
const env = options.env ?? process.env;
|
|
486
|
+
const platform = options.platform ?? process.platform;
|
|
487
|
+
const stdout = options.stdout ?? process.stdout;
|
|
488
|
+
const forceColor = env.FORCE_COLOR ? env.FORCE_COLOR.toLowerCase() : void 0;
|
|
489
|
+
const hasNoColor = !!env.NO_COLOR;
|
|
490
|
+
const isCI = !!env.CI;
|
|
491
|
+
const isForceColorDisabled = forceColor === "0" || forceColor === "false";
|
|
492
|
+
if (hasNoColor || argv.includes("--no-color") || isForceColorDisabled) return false;
|
|
493
|
+
if (forceColor !== void 0 && forceColor !== "0" && forceColor !== "false" || argv.includes("--color")) return true;
|
|
494
|
+
if (platform === "win32") return true;
|
|
495
|
+
return Boolean(stdout.isTTY) && env.TERM !== "dumb" || isCI;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Process-level color support detected at module load time.
|
|
499
|
+
*/
|
|
500
|
+
const isColorSupported = detectColorSupport();
|
|
501
|
+
/**
|
|
502
|
+
* Creates a complete set of color and style formatters.
|
|
503
|
+
*
|
|
504
|
+
* @param enabled - Whether returned formatters should emit ANSI escape codes.
|
|
505
|
+
* @returns A `Colors` object with either active ANSI formatters or plain string
|
|
506
|
+
* pass-through formatters.
|
|
507
|
+
*/
|
|
508
|
+
function createColors(enabled = isColorSupported) {
|
|
509
|
+
const formatter = enabled ? createFormatter : createPlainFormatter;
|
|
510
|
+
return {
|
|
511
|
+
bgBlack: formatter("\x1B[40m", "\x1B[49m"),
|
|
512
|
+
bgBlackBright: formatter("\x1B[100m", "\x1B[49m"),
|
|
513
|
+
bgBlue: formatter("\x1B[44m", "\x1B[49m"),
|
|
514
|
+
bgBlueBright: formatter("\x1B[104m", "\x1B[49m"),
|
|
515
|
+
bgCyan: formatter("\x1B[46m", "\x1B[49m"),
|
|
516
|
+
bgCyanBright: formatter("\x1B[106m", "\x1B[49m"),
|
|
517
|
+
bgGreen: formatter("\x1B[42m", "\x1B[49m"),
|
|
518
|
+
bgGreenBright: formatter("\x1B[102m", "\x1B[49m"),
|
|
519
|
+
bgMagenta: formatter("\x1B[45m", "\x1B[49m"),
|
|
520
|
+
bgMagentaBright: formatter("\x1B[105m", "\x1B[49m"),
|
|
521
|
+
bgRed: formatter("\x1B[41m", "\x1B[49m"),
|
|
522
|
+
bgRedBright: formatter("\x1B[101m", "\x1B[49m"),
|
|
523
|
+
bgWhite: formatter("\x1B[47m", "\x1B[49m"),
|
|
524
|
+
bgWhiteBright: formatter("\x1B[107m", "\x1B[49m"),
|
|
525
|
+
bgYellow: formatter("\x1B[43m", "\x1B[49m"),
|
|
526
|
+
bgYellowBright: formatter("\x1B[103m", "\x1B[49m"),
|
|
527
|
+
black: formatter("\x1B[30m", "\x1B[39m"),
|
|
528
|
+
blackBright: formatter("\x1B[90m", "\x1B[39m"),
|
|
529
|
+
blue: formatter("\x1B[34m", "\x1B[39m"),
|
|
530
|
+
blueBright: formatter("\x1B[94m", "\x1B[39m"),
|
|
531
|
+
bold: formatter("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
532
|
+
cyan: formatter("\x1B[36m", "\x1B[39m"),
|
|
533
|
+
cyanBright: formatter("\x1B[96m", "\x1B[39m"),
|
|
534
|
+
dim: formatter("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
535
|
+
gray: formatter("\x1B[90m", "\x1B[39m"),
|
|
536
|
+
green: formatter("\x1B[32m", "\x1B[39m"),
|
|
537
|
+
greenBright: formatter("\x1B[92m", "\x1B[39m"),
|
|
538
|
+
hidden: formatter("\x1B[8m", "\x1B[28m"),
|
|
539
|
+
inverse: formatter("\x1B[7m", "\x1B[27m"),
|
|
540
|
+
isColorSupported: enabled,
|
|
541
|
+
italic: formatter("\x1B[3m", "\x1B[23m"),
|
|
542
|
+
magenta: formatter("\x1B[35m", "\x1B[39m"),
|
|
543
|
+
magentaBright: formatter("\x1B[95m", "\x1B[39m"),
|
|
544
|
+
red: formatter("\x1B[31m", "\x1B[39m"),
|
|
545
|
+
redBright: formatter("\x1B[91m", "\x1B[39m"),
|
|
546
|
+
reset: formatter("\x1B[0m", "\x1B[0m"),
|
|
547
|
+
strikethrough: formatter("\x1B[9m", "\x1B[29m"),
|
|
548
|
+
underline: formatter("\x1B[4m", "\x1B[24m"),
|
|
549
|
+
white: formatter("\x1B[37m", "\x1B[39m"),
|
|
550
|
+
whiteBright: formatter("\x1B[97m", "\x1B[39m"),
|
|
551
|
+
yellow: formatter("\x1B[33m", "\x1B[39m"),
|
|
552
|
+
yellowBright: formatter("\x1B[93m", "\x1B[39m")
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Default color formatter set for the current process.
|
|
557
|
+
*
|
|
558
|
+
* @remarks
|
|
559
|
+
* Use `color.createColors(false)` when tests need deterministic plain-text
|
|
560
|
+
* output.
|
|
561
|
+
*/
|
|
562
|
+
const color = Object.assign(createColors(), { createColors });
|
|
563
|
+
/**
|
|
564
|
+
* Named color and style formatter exports from the default `color` object.
|
|
565
|
+
*/
|
|
566
|
+
const { reset, bold, dim, italic, underline, inverse, hidden, strikethrough, black, red, green, yellow, blue, magenta, cyan, white, gray, bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite, blackBright, redBright, greenBright, yellowBright, blueBright, magentaBright, cyanBright, whiteBright, bgBlackBright, bgRedBright, bgGreenBright, bgYellowBright, bgBlueBright, bgMagentaBright, bgCyanBright, bgWhiteBright } = color;
|
|
567
|
+
//#endregion
|
|
293
568
|
//#region src/logger.ts
|
|
569
|
+
/**
|
|
570
|
+
* Supported log levels in ascending verbosity.
|
|
571
|
+
*/
|
|
294
572
|
const LOG_LEVELS = [
|
|
295
573
|
"error",
|
|
296
574
|
"warn",
|
|
297
575
|
"info",
|
|
298
576
|
"debug"
|
|
299
577
|
];
|
|
578
|
+
/**
|
|
579
|
+
* Alias for `LOG_LEVELS` kept for callers that prefer validation-oriented
|
|
580
|
+
* naming.
|
|
581
|
+
*/
|
|
300
582
|
const validLogLevels = LOG_LEVELS;
|
|
301
583
|
const LOG_LEVEL_PRIORITY = {
|
|
584
|
+
debug: 3,
|
|
302
585
|
error: 0,
|
|
303
|
-
warn: 1,
|
|
304
586
|
info: 2,
|
|
305
|
-
|
|
587
|
+
warn: 1
|
|
306
588
|
};
|
|
307
589
|
function safeStringify(arg) {
|
|
308
590
|
try {
|
|
@@ -315,6 +597,14 @@ function formatArgs(args) {
|
|
|
315
597
|
if (args.length === 0) return "";
|
|
316
598
|
return `\n${args.map((arg) => ` - ${safeStringify(arg)}`).join("\n")}`;
|
|
317
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* Formats a log message with a level badge and optional structured arguments.
|
|
602
|
+
*
|
|
603
|
+
* @param logLevel - Log level or custom badge label.
|
|
604
|
+
* @param message - Primary message to render.
|
|
605
|
+
* @param args - Additional values rendered as indented JSON-like bullets.
|
|
606
|
+
* @returns A formatted message string.
|
|
607
|
+
*/
|
|
318
608
|
function formatLogMessage(logLevel, message, args = []) {
|
|
319
609
|
const messageStr = typeof message === "string" ? message : String(message);
|
|
320
610
|
const formattedArgs = formatArgs(args);
|
|
@@ -328,6 +618,13 @@ function formatLogMessage(logLevel, message, args = []) {
|
|
|
328
618
|
default: return `[${logLevel.toUpperCase()}] ${messageStr}${formattedArgs}`;
|
|
329
619
|
}
|
|
330
620
|
}
|
|
621
|
+
/**
|
|
622
|
+
* Emits a formatted message through the prompt logger.
|
|
623
|
+
*
|
|
624
|
+
* @param logLevel - Log level or custom badge label.
|
|
625
|
+
* @param message - Primary message to render.
|
|
626
|
+
* @param args - Additional values rendered below the message.
|
|
627
|
+
*/
|
|
331
628
|
function logMessage(logLevel, message, ...args) {
|
|
332
629
|
const formattedMessage = formatLogMessage(logLevel, message, args);
|
|
333
630
|
switch (logLevel) {
|
|
@@ -348,11 +645,35 @@ function logMessage(logLevel, message, ...args) {
|
|
|
348
645
|
default: p.log.message(formattedMessage);
|
|
349
646
|
}
|
|
350
647
|
}
|
|
648
|
+
/**
|
|
649
|
+
* Formats a bounded progress step indicator.
|
|
650
|
+
*
|
|
651
|
+
* @remarks
|
|
652
|
+
* `current` is clamped between `0` and `total`, and `total` is clamped to at
|
|
653
|
+
* least `0` so malformed progress values still render predictably.
|
|
654
|
+
*
|
|
655
|
+
* @param current - Current step number.
|
|
656
|
+
* @param total - Total number of steps.
|
|
657
|
+
* @param label - Step label shown after the progress bar.
|
|
658
|
+
* @returns A formatted progress row.
|
|
659
|
+
*/
|
|
351
660
|
function formatStep(current, total, label) {
|
|
352
661
|
const safeTotal = Math.max(0, total);
|
|
353
662
|
const safeCurrent = Math.min(Math.max(0, current), safeTotal);
|
|
354
663
|
return `[${color.green("█".repeat(safeCurrent))}${color.dim("░".repeat(safeTotal - safeCurrent))}] Step ${safeCurrent}/${safeTotal}: ${label}`;
|
|
355
664
|
}
|
|
665
|
+
/**
|
|
666
|
+
* Creates a `CliLogger` backed by Clack prompt output.
|
|
667
|
+
*
|
|
668
|
+
* @remarks
|
|
669
|
+
* Messages below the configured verbosity are ignored for `debug`, `info`,
|
|
670
|
+
* `warn`, and `error`. Other output helpers such as `message`, `note`,
|
|
671
|
+
* `success`, `failed`, and `outro` always render because they represent
|
|
672
|
+
* explicit user interaction states rather than diagnostic verbosity.
|
|
673
|
+
*
|
|
674
|
+
* @param level - Minimum log level to emit.
|
|
675
|
+
* @returns A logger suitable for `CliContext.logger`.
|
|
676
|
+
*/
|
|
356
677
|
function createCliLogger(level = "info") {
|
|
357
678
|
const currentLevelPriority = LOG_LEVEL_PRIORITY[level];
|
|
358
679
|
const shouldLog = (targetLevel) => LOG_LEVEL_PRIORITY[targetLevel] <= currentLevelPriority;
|
|
@@ -360,91 +681,112 @@ function createCliLogger(level = "info") {
|
|
|
360
681
|
debug(message, ...args) {
|
|
361
682
|
if (shouldLog("debug")) logMessage("debug", message, ...args);
|
|
362
683
|
},
|
|
363
|
-
info(message, ...args) {
|
|
364
|
-
if (shouldLog("info")) logMessage("info", message, ...args);
|
|
365
|
-
},
|
|
366
|
-
warn(message, ...args) {
|
|
367
|
-
if (shouldLog("warn")) logMessage("warn", message, ...args);
|
|
368
|
-
},
|
|
369
684
|
error(message, ...args) {
|
|
370
685
|
if (shouldLog("error")) logMessage("error", message, ...args);
|
|
371
686
|
},
|
|
687
|
+
failed(message, exitCode = 1) {
|
|
688
|
+
logMessage("failed", message);
|
|
689
|
+
process.exit(exitCode);
|
|
690
|
+
},
|
|
691
|
+
info(message, ...args) {
|
|
692
|
+
if (shouldLog("info")) logMessage("info", message, ...args);
|
|
693
|
+
},
|
|
372
694
|
message(message) {
|
|
373
695
|
p.log.message(message);
|
|
374
696
|
},
|
|
375
697
|
note(content, title) {
|
|
376
698
|
p.note(content, title, { format: (line) => line });
|
|
377
699
|
},
|
|
378
|
-
success(message) {
|
|
379
|
-
logMessage("success", message);
|
|
380
|
-
},
|
|
381
|
-
failed(message, exitCode = 1) {
|
|
382
|
-
logMessage("failed", message);
|
|
383
|
-
process.exit(exitCode);
|
|
384
|
-
},
|
|
385
700
|
outro(message) {
|
|
386
701
|
p.outro(message);
|
|
387
702
|
},
|
|
388
703
|
step(current, total, label) {
|
|
389
704
|
p.log.step(formatStep(current, total, label));
|
|
705
|
+
},
|
|
706
|
+
success(message) {
|
|
707
|
+
logMessage("success", message);
|
|
708
|
+
},
|
|
709
|
+
warn(message, ...args) {
|
|
710
|
+
if (shouldLog("warn")) logMessage("warn", message, ...args);
|
|
390
711
|
}
|
|
391
712
|
};
|
|
392
713
|
}
|
|
393
714
|
//#endregion
|
|
394
715
|
//#region src/parser.ts
|
|
716
|
+
/**
|
|
717
|
+
* Built-in flags parsed for every Hexbus CLI invocation.
|
|
718
|
+
*
|
|
719
|
+
* @remarks
|
|
720
|
+
* Primary flag names are derived from the first long flag in each entry. For
|
|
721
|
+
* example, `--no-telemetry` is exposed as `parsedFlags['no-telemetry']`.
|
|
722
|
+
*/
|
|
395
723
|
const globalFlags = [
|
|
396
724
|
{
|
|
397
|
-
names: ["--help", "-h"],
|
|
398
725
|
description: "Show this help message",
|
|
399
|
-
|
|
400
|
-
|
|
726
|
+
expectsValue: false,
|
|
727
|
+
names: ["--help", "-h"],
|
|
728
|
+
type: "special"
|
|
401
729
|
},
|
|
402
730
|
{
|
|
403
|
-
names: ["--version", "-v"],
|
|
404
731
|
description: "Show the CLI version",
|
|
405
|
-
|
|
406
|
-
|
|
732
|
+
expectsValue: false,
|
|
733
|
+
names: ["--version", "-v"],
|
|
734
|
+
type: "special"
|
|
407
735
|
},
|
|
408
736
|
{
|
|
409
|
-
|
|
737
|
+
defaultValue: "info",
|
|
410
738
|
description: "Set log level (error, warn, info, debug)",
|
|
411
|
-
type: "string",
|
|
412
739
|
expectsValue: true,
|
|
413
|
-
|
|
740
|
+
names: ["--logger"],
|
|
741
|
+
type: "string"
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
defaultValue: false,
|
|
745
|
+
description: "Force color output",
|
|
746
|
+
expectsValue: false,
|
|
747
|
+
names: ["--color"],
|
|
748
|
+
type: "boolean"
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
defaultValue: false,
|
|
752
|
+
description: "Disable color output",
|
|
753
|
+
expectsValue: false,
|
|
754
|
+
names: ["--no-color"],
|
|
755
|
+
type: "boolean"
|
|
414
756
|
},
|
|
415
757
|
{
|
|
416
|
-
names: ["--config"],
|
|
417
758
|
description: "Specify path to configuration file",
|
|
418
|
-
|
|
419
|
-
|
|
759
|
+
expectsValue: true,
|
|
760
|
+
names: ["--config"],
|
|
761
|
+
type: "string"
|
|
420
762
|
},
|
|
421
763
|
{
|
|
422
|
-
|
|
764
|
+
defaultValue: false,
|
|
423
765
|
description: "Skip confirmation prompts",
|
|
424
|
-
type: "boolean",
|
|
425
766
|
expectsValue: false,
|
|
426
|
-
|
|
767
|
+
names: ["-y", "--yes"],
|
|
768
|
+
type: "boolean"
|
|
427
769
|
},
|
|
428
770
|
{
|
|
429
|
-
|
|
771
|
+
defaultValue: false,
|
|
430
772
|
description: "Disable telemetry data collection",
|
|
431
|
-
type: "boolean",
|
|
432
773
|
expectsValue: false,
|
|
433
|
-
|
|
774
|
+
names: ["--no-telemetry"],
|
|
775
|
+
type: "boolean"
|
|
434
776
|
},
|
|
435
777
|
{
|
|
436
|
-
|
|
778
|
+
defaultValue: false,
|
|
437
779
|
description: "Enable debug mode for telemetry",
|
|
438
|
-
type: "boolean",
|
|
439
780
|
expectsValue: false,
|
|
440
|
-
|
|
781
|
+
names: ["--telemetry-debug"],
|
|
782
|
+
type: "boolean"
|
|
441
783
|
},
|
|
442
784
|
{
|
|
443
|
-
|
|
785
|
+
defaultValue: false,
|
|
444
786
|
description: "Force operation even if files exist",
|
|
445
|
-
type: "boolean",
|
|
446
787
|
expectsValue: false,
|
|
447
|
-
|
|
788
|
+
names: ["--force"],
|
|
789
|
+
type: "boolean"
|
|
448
790
|
}
|
|
449
791
|
];
|
|
450
792
|
function getPrimaryFlagName(flag) {
|
|
@@ -452,11 +794,32 @@ function getPrimaryFlagName(flag) {
|
|
|
452
794
|
const fallback = flag.names.reduce((longest, name) => name.length > longest.length ? name : longest, "");
|
|
453
795
|
return (longName ?? fallback).replace(/^--?/, "");
|
|
454
796
|
}
|
|
797
|
+
/**
|
|
798
|
+
* Parses raw command-line arguments into command name, command args, and
|
|
799
|
+
* global flags.
|
|
800
|
+
*
|
|
801
|
+
* @remarks
|
|
802
|
+
* Only flags declared in `globalFlags` are parsed. Unknown flags and
|
|
803
|
+
* positional arguments are preserved as command arguments unless the first
|
|
804
|
+
* positional argument matches a registered top-level command.
|
|
805
|
+
*
|
|
806
|
+
* @param rawArgs - Arguments after the executable and script path.
|
|
807
|
+
* @param commands - Top-level commands used to identify the command name.
|
|
808
|
+
* @returns Normalized parsed arguments for context creation or custom routing.
|
|
809
|
+
*
|
|
810
|
+
* @example
|
|
811
|
+
* ```ts
|
|
812
|
+
* const parsed = parseCliArgs(['init', '--logger', 'debug'], commands);
|
|
813
|
+
* // parsed.commandName === 'init'
|
|
814
|
+
* // parsed.parsedFlags.logger === 'debug'
|
|
815
|
+
* ```
|
|
816
|
+
*/
|
|
455
817
|
function parseCliArgs(rawArgs, commands) {
|
|
456
818
|
const parsedFlags = {};
|
|
457
819
|
const potentialCommandArgs = [];
|
|
458
820
|
let commandName;
|
|
459
821
|
const commandArgs = [];
|
|
822
|
+
const knownFlagSet = new Set(globalFlags.flatMap((flag) => flag.names));
|
|
460
823
|
for (const flag of globalFlags) {
|
|
461
824
|
const primaryName = getPrimaryFlagName(flag);
|
|
462
825
|
if (!primaryName) continue;
|
|
@@ -475,7 +838,7 @@ function parseCliArgs(rawArgs, commands) {
|
|
|
475
838
|
if (flag.type === "boolean") parsedFlags[primaryName] = true;
|
|
476
839
|
else if (flag.expectsValue) {
|
|
477
840
|
const nextArg = rawArgs[i + 1];
|
|
478
|
-
if (nextArg && !
|
|
841
|
+
if (nextArg && !knownFlagSet.has(nextArg)) {
|
|
479
842
|
parsedFlags[primaryName] = nextArg;
|
|
480
843
|
i++;
|
|
481
844
|
} else p.log.warn(formatLogMessage("warn", `Flag ${arg} expects a value, but none was provided`));
|
|
@@ -484,72 +847,142 @@ function parseCliArgs(rawArgs, commands) {
|
|
|
484
847
|
}
|
|
485
848
|
if (!isFlag) potentialCommandArgs.push(arg);
|
|
486
849
|
}
|
|
487
|
-
const firstPositional = potentialCommandArgs
|
|
850
|
+
const [firstPositional] = potentialCommandArgs;
|
|
488
851
|
if (typeof firstPositional === "string" && commands.some((cmd) => cmd.name === firstPositional)) {
|
|
489
852
|
commandName = firstPositional;
|
|
490
853
|
commandArgs.push(...potentialCommandArgs.slice(1));
|
|
491
854
|
} else commandArgs.push(...potentialCommandArgs);
|
|
492
855
|
return {
|
|
493
|
-
commandName,
|
|
494
856
|
commandArgs,
|
|
857
|
+
commandName,
|
|
495
858
|
parsedFlags
|
|
496
859
|
};
|
|
497
860
|
}
|
|
861
|
+
/**
|
|
862
|
+
* Formats a single flag for display in help output.
|
|
863
|
+
*
|
|
864
|
+
* @param flag - Flag definition to render.
|
|
865
|
+
* @returns A help row containing names, value hint, and description.
|
|
866
|
+
*/
|
|
498
867
|
function formatFlagHelp(flag) {
|
|
499
868
|
return ` ${flag.names.join(", ")}${flag.expectsValue ? " <value>" : ""}\t${flag.description}`;
|
|
500
869
|
}
|
|
870
|
+
/**
|
|
871
|
+
* Formats all built-in global flags for help output.
|
|
872
|
+
*
|
|
873
|
+
* @returns Newline-delimited help rows for `globalFlags`.
|
|
874
|
+
*/
|
|
501
875
|
function generateFlagsHelp() {
|
|
502
876
|
return globalFlags.map(formatFlagHelp).join("\n");
|
|
503
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Checks whether a parsed boolean flag is enabled.
|
|
880
|
+
*
|
|
881
|
+
* @param flags - Parsed flag map from `parseCliArgs` or `CliContext`.
|
|
882
|
+
* @param name - Primary flag name without leading dashes.
|
|
883
|
+
* @returns `true` only when the flag value is exactly `true`.
|
|
884
|
+
*
|
|
885
|
+
* @example
|
|
886
|
+
* ```ts
|
|
887
|
+
* if (hasFlag(context.flags, 'force')) {
|
|
888
|
+
* // overwrite existing files
|
|
889
|
+
* }
|
|
890
|
+
* ```
|
|
891
|
+
*/
|
|
504
892
|
function hasFlag(flags, name) {
|
|
505
893
|
return flags[name] === true;
|
|
506
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* Reads a string-valued flag from a parsed flag map.
|
|
897
|
+
*
|
|
898
|
+
* @param flags - Parsed flag map from `parseCliArgs` or `CliContext`.
|
|
899
|
+
* @param name - Primary flag name without leading dashes.
|
|
900
|
+
* @returns The flag value when it is a string, otherwise `undefined`.
|
|
901
|
+
*/
|
|
507
902
|
function getFlagValue(flags, name) {
|
|
508
903
|
const value = flags[name];
|
|
509
904
|
if (typeof value === "string") return value;
|
|
510
905
|
}
|
|
906
|
+
/**
|
|
907
|
+
* Splits command arguments into an optional nested subcommand and remaining
|
|
908
|
+
* args.
|
|
909
|
+
*
|
|
910
|
+
* @param args - Positional command arguments to inspect.
|
|
911
|
+
* @param subcommands - Available subcommands for the current command.
|
|
912
|
+
* @returns The matched subcommand, if any, plus arguments after the subcommand
|
|
913
|
+
* name.
|
|
914
|
+
*
|
|
915
|
+
* @example
|
|
916
|
+
* ```ts
|
|
917
|
+
* const { subcommand, remainingArgs } = parseSubcommand(
|
|
918
|
+
* context.commandArgs,
|
|
919
|
+
* command.subcommands ?? []
|
|
920
|
+
* );
|
|
921
|
+
* ```
|
|
922
|
+
*/
|
|
511
923
|
function parseSubcommand(args, subcommands) {
|
|
512
|
-
const subcommandName = args
|
|
924
|
+
const [subcommandName] = args;
|
|
513
925
|
const subcommand = subcommands.find((cmd) => cmd.name === subcommandName);
|
|
514
926
|
if (subcommand) return {
|
|
515
|
-
|
|
516
|
-
|
|
927
|
+
remainingArgs: args.slice(1),
|
|
928
|
+
subcommand
|
|
517
929
|
};
|
|
518
930
|
return {
|
|
519
|
-
|
|
520
|
-
|
|
931
|
+
remainingArgs: args,
|
|
932
|
+
subcommand: void 0
|
|
521
933
|
};
|
|
522
934
|
}
|
|
523
935
|
//#endregion
|
|
524
936
|
//#region src/telemetry.ts
|
|
937
|
+
/**
|
|
938
|
+
* Standard telemetry event names emitted by Hexbus runtime helpers.
|
|
939
|
+
*/
|
|
525
940
|
const TelemetryEventName = {
|
|
526
|
-
CLI_INVOKED: "cli_invoked",
|
|
527
|
-
CLI_ENVIRONMENT_DETECTED: "cli_environment_detected",
|
|
528
941
|
CLI_COMPLETED: "cli_completed",
|
|
942
|
+
CLI_ENVIRONMENT_DETECTED: "cli_environment_detected",
|
|
943
|
+
CLI_INVOKED: "cli_invoked",
|
|
944
|
+
COMMAND_FAILED: "command_failed",
|
|
529
945
|
COMMAND_INVOKED: "command_invoked",
|
|
530
946
|
COMMAND_SUCCEEDED: "command_succeeded",
|
|
531
|
-
COMMAND_FAILED: "command_failed",
|
|
532
947
|
COMMAND_UNKNOWN: "command_unknown",
|
|
533
948
|
ERROR_OCCURRED: "error_occurred",
|
|
534
949
|
HELP_DISPLAYED: "help_displayed",
|
|
535
|
-
|
|
950
|
+
INTERACTIVE_MENU_EXITED: "interactive_menu_exited",
|
|
536
951
|
INTERACTIVE_MENU_OPENED: "interactive_menu_opened",
|
|
537
|
-
|
|
952
|
+
VERSION_DISPLAYED: "version_displayed"
|
|
538
953
|
};
|
|
539
954
|
function isEnvDisabled(prefix) {
|
|
540
955
|
const value = process.env[`${prefix}_TELEMETRY_DISABLED`];
|
|
541
956
|
return value === "1" || value === "true";
|
|
542
957
|
}
|
|
958
|
+
/**
|
|
959
|
+
* Creates a no-op telemetry client.
|
|
960
|
+
*
|
|
961
|
+
* @returns A telemetry implementation whose methods do nothing and whose
|
|
962
|
+
* `isDisabled()` method returns `true`.
|
|
963
|
+
*/
|
|
543
964
|
function createDisabledTelemetry() {
|
|
544
965
|
return {
|
|
545
|
-
trackEvent: () => {},
|
|
546
|
-
trackCommand: () => {},
|
|
547
|
-
trackError: () => {},
|
|
548
966
|
flush: async () => {},
|
|
967
|
+
isDisabled: () => true,
|
|
549
968
|
shutdown: async () => {},
|
|
550
|
-
|
|
969
|
+
trackCommand: () => {},
|
|
970
|
+
trackError: () => {},
|
|
971
|
+
trackEvent: () => {}
|
|
551
972
|
};
|
|
552
973
|
}
|
|
974
|
+
/**
|
|
975
|
+
* Creates the built-in telemetry client.
|
|
976
|
+
*
|
|
977
|
+
* @remarks
|
|
978
|
+
* Events are queued in memory. `flush()` posts the queue to `endpoint` when one
|
|
979
|
+
* is configured, then clears the queue. Failed flushes are reported through
|
|
980
|
+
* the optional logger and do not throw, keeping telemetry best effort.
|
|
981
|
+
*
|
|
982
|
+
* @param options - Telemetry behavior and event defaults.
|
|
983
|
+
* @returns An enabled or disabled telemetry client depending on options and
|
|
984
|
+
* environment opt-out variables.
|
|
985
|
+
*/
|
|
553
986
|
function createTelemetry(options = {}) {
|
|
554
987
|
const envVarPrefix = options.envVarPrefix ?? "APP";
|
|
555
988
|
const disabled = options.disabled === true || isEnvDisabled(envVarPrefix);
|
|
@@ -573,45 +1006,54 @@ function createTelemetry(options = {}) {
|
|
|
573
1006
|
events.length = 0;
|
|
574
1007
|
return;
|
|
575
1008
|
}
|
|
576
|
-
const batch = events.splice(0
|
|
1009
|
+
const batch = events.splice(0);
|
|
577
1010
|
try {
|
|
578
1011
|
await fetch(options.endpoint, {
|
|
579
|
-
method: "POST",
|
|
580
|
-
headers: { "content-type": "application/json" },
|
|
581
1012
|
body: JSON.stringify({ events: batch }),
|
|
582
|
-
|
|
1013
|
+
headers: { "content-type": "application/json" },
|
|
1014
|
+
keepalive: true,
|
|
1015
|
+
method: "POST"
|
|
583
1016
|
});
|
|
584
1017
|
} catch (error) {
|
|
585
1018
|
options.logger?.warn(`Failed to send telemetry: ${error instanceof Error ? error.message : String(error)}`);
|
|
586
1019
|
}
|
|
587
1020
|
};
|
|
588
1021
|
return {
|
|
589
|
-
|
|
1022
|
+
flush,
|
|
1023
|
+
isDisabled() {
|
|
1024
|
+
return false;
|
|
1025
|
+
},
|
|
1026
|
+
async shutdown() {
|
|
1027
|
+
await flush();
|
|
1028
|
+
},
|
|
590
1029
|
trackCommand(command, args = [], flags = {}) {
|
|
1030
|
+
const enabledFlags = Object.entries(flags).filter(([, value]) => value !== false && value !== void 0).map(([key]) => key);
|
|
1031
|
+
enabledFlags.sort();
|
|
591
1032
|
trackEvent(TelemetryEventName.COMMAND_INVOKED, {
|
|
592
|
-
command,
|
|
593
1033
|
argsCount: args.length,
|
|
594
|
-
|
|
1034
|
+
command,
|
|
1035
|
+
enabledFlags
|
|
595
1036
|
});
|
|
596
1037
|
},
|
|
597
1038
|
trackError(error, command) {
|
|
598
1039
|
trackEvent(TelemetryEventName.ERROR_OCCURRED, {
|
|
599
1040
|
command,
|
|
600
|
-
|
|
601
|
-
|
|
1041
|
+
errorMessage: error.message,
|
|
1042
|
+
errorName: error.name
|
|
602
1043
|
});
|
|
603
1044
|
},
|
|
604
|
-
|
|
605
|
-
async shutdown() {
|
|
606
|
-
await flush();
|
|
607
|
-
},
|
|
608
|
-
isDisabled() {
|
|
609
|
-
return false;
|
|
610
|
-
}
|
|
1045
|
+
trackEvent
|
|
611
1046
|
};
|
|
612
1047
|
}
|
|
613
1048
|
//#endregion
|
|
614
1049
|
//#region src/context.ts
|
|
1050
|
+
/**
|
|
1051
|
+
* Resolves the active logger level from parsed CLI flags.
|
|
1052
|
+
*
|
|
1053
|
+
* @param parsedFlags - Parsed global flags from the current invocation.
|
|
1054
|
+
* @returns A valid log level, falling back to `info` for missing or invalid
|
|
1055
|
+
* values.
|
|
1056
|
+
*/
|
|
615
1057
|
function getLogLevel(parsedFlags) {
|
|
616
1058
|
const levelArg = parsedFlags.logger;
|
|
617
1059
|
if (typeof levelArg === "string") {
|
|
@@ -620,17 +1062,34 @@ function getLogLevel(parsedFlags) {
|
|
|
620
1062
|
}
|
|
621
1063
|
return "info";
|
|
622
1064
|
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Creates file-system helpers scoped to a project root.
|
|
1067
|
+
*
|
|
1068
|
+
* @param cwd - Project root used for package metadata lookup.
|
|
1069
|
+
* @returns File-system utilities for context consumers.
|
|
1070
|
+
*/
|
|
623
1071
|
function createFileSystem(cwd) {
|
|
624
1072
|
return {
|
|
1073
|
+
async exists(filePath) {
|
|
1074
|
+
try {
|
|
1075
|
+
await fs.access(filePath);
|
|
1076
|
+
return true;
|
|
1077
|
+
} catch {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
625
1081
|
getPackageInfo() {
|
|
626
1082
|
const packageJsonPath = path.join(cwd, "package.json");
|
|
627
1083
|
try {
|
|
628
1084
|
const content = fsSync.readFileSync(packageJsonPath, "utf-8");
|
|
629
1085
|
const packageInfo = JSON.parse(content);
|
|
1086
|
+
const packageFields = packageInfo && typeof packageInfo === "object" && !Array.isArray(packageInfo) ? packageInfo : {};
|
|
1087
|
+
const name = typeof packageFields.name === "string" ? packageFields.name : "unknown";
|
|
1088
|
+
const version = typeof packageFields.version === "string" ? packageFields.version : "unknown";
|
|
630
1089
|
return {
|
|
631
|
-
...
|
|
632
|
-
name:
|
|
633
|
-
version:
|
|
1090
|
+
...packageFields,
|
|
1091
|
+
name: name || "unknown",
|
|
1092
|
+
version: version || "unknown"
|
|
634
1093
|
};
|
|
635
1094
|
} catch {
|
|
636
1095
|
return {
|
|
@@ -639,25 +1098,44 @@ function createFileSystem(cwd) {
|
|
|
639
1098
|
};
|
|
640
1099
|
}
|
|
641
1100
|
},
|
|
642
|
-
async
|
|
643
|
-
|
|
644
|
-
await fs.access(filePath);
|
|
645
|
-
return true;
|
|
646
|
-
} catch {
|
|
647
|
-
return false;
|
|
648
|
-
}
|
|
1101
|
+
async mkdir(dirPath) {
|
|
1102
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
649
1103
|
},
|
|
650
1104
|
read(filePath) {
|
|
651
1105
|
return fs.readFile(filePath, "utf-8");
|
|
652
1106
|
},
|
|
653
1107
|
write(filePath, content) {
|
|
654
1108
|
return fs.writeFile(filePath, content, "utf-8");
|
|
655
|
-
},
|
|
656
|
-
async mkdir(dirPath) {
|
|
657
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
658
1109
|
}
|
|
659
1110
|
};
|
|
660
1111
|
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Creates the resolved context passed to command actions.
|
|
1114
|
+
*
|
|
1115
|
+
* @remarks
|
|
1116
|
+
* Context creation performs the standard CLI bootstrap sequence: parse global
|
|
1117
|
+
* flags, create the logger, detect the project root, detect framework and
|
|
1118
|
+
* package manager metadata, set up telemetry, and attach helpers for config
|
|
1119
|
+
* loading, file-system access, confirmation prompts, and process-ending error
|
|
1120
|
+
* handling.
|
|
1121
|
+
*
|
|
1122
|
+
* @typeParam TPackage - Product-specific package identifier returned from
|
|
1123
|
+
* framework detection.
|
|
1124
|
+
* @param options - Context creation options for the current invocation.
|
|
1125
|
+
* @returns A fully initialized `CliContext`.
|
|
1126
|
+
*
|
|
1127
|
+
* @example
|
|
1128
|
+
* ```ts
|
|
1129
|
+
* const context = await createCliContext({
|
|
1130
|
+
* rawArgs: process.argv.slice(2),
|
|
1131
|
+
* appName: 'acme',
|
|
1132
|
+
* commands,
|
|
1133
|
+
* });
|
|
1134
|
+
*
|
|
1135
|
+
* await commands.find((command) => command.name === context.commandName)
|
|
1136
|
+
* ?.action(context);
|
|
1137
|
+
* ```
|
|
1138
|
+
*/
|
|
661
1139
|
async function createCliContext(options) {
|
|
662
1140
|
const cwd = options.cwd ?? process.cwd();
|
|
663
1141
|
const appName = options.appName ?? "cli";
|
|
@@ -668,37 +1146,35 @@ async function createCliContext(options) {
|
|
|
668
1146
|
const framework = await detectFramework(projectRoot, logger, options.packageMap);
|
|
669
1147
|
const packageManager = await detectPackageManager(projectRoot, logger, { interactive: options.interactivePackageManagerDetection });
|
|
670
1148
|
const telemetry = createTelemetry({
|
|
671
|
-
disabled: options.telemetry?.disabled === true || parsedFlags["no-telemetry"] === true,
|
|
672
|
-
debug: options.telemetry?.debug === true || parsedFlags["telemetry-debug"] === true,
|
|
673
|
-
endpoint: options.telemetry?.endpoint,
|
|
674
1149
|
appName,
|
|
675
|
-
|
|
1150
|
+
debug: options.telemetry?.debug === true || parsedFlags["telemetry-debug"] === true,
|
|
676
1151
|
defaultProperties: {
|
|
677
|
-
entryCommand: commandName ?? "interactive",
|
|
678
|
-
commandArgsCount: commandArgs.length,
|
|
679
1152
|
cliVersion: fsUtils.getPackageInfo().version,
|
|
1153
|
+
commandArgsCount: commandArgs.length,
|
|
1154
|
+
entryCommand: commandName ?? "interactive",
|
|
680
1155
|
framework: framework.framework ?? "unknown",
|
|
681
1156
|
frameworkVersion: framework.frameworkVersion ?? "unknown",
|
|
682
1157
|
packageManager: packageManager.name,
|
|
683
1158
|
...options.telemetry?.defaultProperties
|
|
684
1159
|
},
|
|
1160
|
+
disabled: options.telemetry?.disabled === true || parsedFlags["no-telemetry"] === true,
|
|
1161
|
+
endpoint: options.telemetry?.endpoint,
|
|
1162
|
+
envVarPrefix: options.telemetry?.envVarPrefix ?? appName.toUpperCase(),
|
|
685
1163
|
logger
|
|
686
1164
|
});
|
|
687
1165
|
const errorHandlers = createErrorHandlers(logger, telemetry);
|
|
688
1166
|
const context = {
|
|
689
|
-
logger,
|
|
690
|
-
flags: parsedFlags,
|
|
691
|
-
commandName,
|
|
692
1167
|
commandArgs,
|
|
693
|
-
|
|
694
|
-
error: errorHandlers,
|
|
1168
|
+
commandName,
|
|
695
1169
|
config: {
|
|
1170
|
+
getPathAliases() {
|
|
1171
|
+
return null;
|
|
1172
|
+
},
|
|
696
1173
|
async loadConfig() {
|
|
697
|
-
const configPath = typeof parsedFlags.config === "string" ? parsedFlags.config : void 0;
|
|
698
1174
|
const { config } = await loadConfig({
|
|
699
|
-
|
|
1175
|
+
configFile: typeof parsedFlags.config === "string" ? parsedFlags.config : void 0,
|
|
700
1176
|
cwd: projectRoot,
|
|
701
|
-
|
|
1177
|
+
name: options.configName ?? appName
|
|
702
1178
|
});
|
|
703
1179
|
return config ?? null;
|
|
704
1180
|
},
|
|
@@ -706,89 +1182,117 @@ async function createCliContext(options) {
|
|
|
706
1182
|
const config = await this.loadConfig();
|
|
707
1183
|
if (!config) throw new CliError("CONFIG_NOT_FOUND");
|
|
708
1184
|
return config;
|
|
709
|
-
},
|
|
710
|
-
getPathAliases() {
|
|
711
|
-
return null;
|
|
712
1185
|
}
|
|
713
1186
|
},
|
|
714
|
-
fs: fsUtils,
|
|
715
|
-
telemetry,
|
|
716
1187
|
async confirm(message, initialValue = true) {
|
|
717
1188
|
if (parsedFlags.y === true || parsedFlags.yes === true) return true;
|
|
718
1189
|
const result = await p.confirm({
|
|
719
|
-
|
|
720
|
-
|
|
1190
|
+
initialValue,
|
|
1191
|
+
message
|
|
721
1192
|
});
|
|
722
1193
|
if (p.isCancel(result)) errorHandlers.handleCancel("Confirmation cancelled");
|
|
723
1194
|
return result;
|
|
724
1195
|
},
|
|
725
|
-
|
|
1196
|
+
cwd,
|
|
1197
|
+
error: errorHandlers,
|
|
1198
|
+
flags: parsedFlags,
|
|
726
1199
|
framework,
|
|
727
|
-
|
|
1200
|
+
fs: fsUtils,
|
|
1201
|
+
logger,
|
|
1202
|
+
packageManager,
|
|
1203
|
+
projectRoot,
|
|
1204
|
+
telemetry
|
|
728
1205
|
};
|
|
729
1206
|
telemetry.trackEvent(TelemetryEventName.CLI_ENVIRONMENT_DETECTED, {
|
|
730
1207
|
command: commandName ?? "interactive",
|
|
731
|
-
projectRootChanged: projectRoot !== cwd,
|
|
732
1208
|
framework: framework.framework ?? "unknown",
|
|
733
1209
|
frameworkVersion: framework.frameworkVersion ?? "unknown",
|
|
734
|
-
packageManager: packageManager.name,
|
|
735
1210
|
hasReact: framework.hasReact,
|
|
1211
|
+
packageManager: packageManager.name,
|
|
1212
|
+
projectRootChanged: projectRoot !== cwd,
|
|
736
1213
|
reactVersion: framework.reactVersion ?? "unknown",
|
|
737
1214
|
tailwindVersion: framework.tailwindVersion ?? "unknown"
|
|
738
1215
|
});
|
|
739
1216
|
return context;
|
|
740
1217
|
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Creates a deterministic context for unit tests.
|
|
1220
|
+
*
|
|
1221
|
+
* @remarks
|
|
1222
|
+
* The test context disables telemetry, uses an error-only logger, avoids real
|
|
1223
|
+
* framework or package-manager detection, and provides no-op file-system
|
|
1224
|
+
* helpers. Pass overrides to replace only the services a test needs.
|
|
1225
|
+
*
|
|
1226
|
+
* @param overrides - Partial context fields to merge into the default test
|
|
1227
|
+
* context.
|
|
1228
|
+
* @returns A `CliContext` suitable for command and helper tests.
|
|
1229
|
+
*
|
|
1230
|
+
* @example
|
|
1231
|
+
* ```ts
|
|
1232
|
+
* const context = createTestContext({
|
|
1233
|
+
* flags: { force: true },
|
|
1234
|
+
* commandName: 'init',
|
|
1235
|
+
* });
|
|
1236
|
+
* ```
|
|
1237
|
+
*/
|
|
741
1238
|
function createTestContext(overrides = {}) {
|
|
742
1239
|
const logger = createCliLogger("error");
|
|
743
1240
|
const telemetry = createDisabledTelemetry();
|
|
744
1241
|
const error = createErrorHandlers(logger, telemetry);
|
|
745
1242
|
return {
|
|
746
|
-
logger,
|
|
747
|
-
flags: {},
|
|
748
|
-
commandName: void 0,
|
|
749
1243
|
commandArgs: [],
|
|
750
|
-
|
|
751
|
-
error,
|
|
1244
|
+
commandName: void 0,
|
|
752
1245
|
config: {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
},
|
|
757
|
-
getPathAliases: () => null
|
|
758
|
-
},
|
|
759
|
-
fs: {
|
|
760
|
-
getPackageInfo: () => ({
|
|
761
|
-
name: "test",
|
|
762
|
-
version: "0.0.0"
|
|
763
|
-
}),
|
|
764
|
-
exists: async () => false,
|
|
765
|
-
read: async () => "",
|
|
766
|
-
write: async () => {},
|
|
767
|
-
mkdir: async () => {}
|
|
1246
|
+
getPathAliases: () => null,
|
|
1247
|
+
loadConfig: () => Promise.resolve(null),
|
|
1248
|
+
requireConfig: () => Promise.reject(new CliError("CONFIG_NOT_FOUND"))
|
|
768
1249
|
},
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1250
|
+
confirm: () => Promise.resolve(true),
|
|
1251
|
+
cwd: process.cwd(),
|
|
1252
|
+
error,
|
|
1253
|
+
flags: {},
|
|
772
1254
|
framework: {
|
|
773
1255
|
framework: null,
|
|
774
1256
|
frameworkVersion: null,
|
|
775
|
-
pkg: null,
|
|
776
1257
|
hasReact: false,
|
|
1258
|
+
pkg: null,
|
|
777
1259
|
reactVersion: null,
|
|
778
1260
|
tailwindVersion: null
|
|
779
1261
|
},
|
|
1262
|
+
fs: {
|
|
1263
|
+
exists: () => Promise.resolve(false),
|
|
1264
|
+
getPackageInfo: () => ({
|
|
1265
|
+
name: "test",
|
|
1266
|
+
version: "0.0.0"
|
|
1267
|
+
}),
|
|
1268
|
+
mkdir: () => Promise.resolve(),
|
|
1269
|
+
read: () => Promise.resolve(""),
|
|
1270
|
+
write: () => Promise.resolve()
|
|
1271
|
+
},
|
|
1272
|
+
logger,
|
|
780
1273
|
packageManager: {
|
|
781
|
-
name: "npm",
|
|
782
|
-
installCommand: "npm install",
|
|
783
1274
|
addCommand: "npm install",
|
|
784
|
-
|
|
785
|
-
|
|
1275
|
+
execCommand: "npx",
|
|
1276
|
+
installCommand: "npm install",
|
|
1277
|
+
name: "npm",
|
|
1278
|
+
runCommand: "npm run"
|
|
786
1279
|
},
|
|
1280
|
+
projectRoot: process.cwd(),
|
|
1281
|
+
telemetry,
|
|
787
1282
|
...overrides
|
|
788
1283
|
};
|
|
789
1284
|
}
|
|
790
1285
|
//#endregion
|
|
791
1286
|
//#region src/help.ts
|
|
1287
|
+
/**
|
|
1288
|
+
* Renders a help menu for commands and global flags.
|
|
1289
|
+
*
|
|
1290
|
+
* @param context - Context subset providing the logger used for output.
|
|
1291
|
+
* @param options - Application metadata for the help menu.
|
|
1292
|
+
* @param commands - Commands to list; commands with `hidden: true` are
|
|
1293
|
+
* omitted.
|
|
1294
|
+
* @param flags - Global flags to list.
|
|
1295
|
+
*/
|
|
792
1296
|
function showHelpMenu(context, options, commands, flags) {
|
|
793
1297
|
const commandRows = commands.filter((command) => !command.hidden).map((command) => ` ${command.name.padEnd(16)} ${command.description}`).join("\n");
|
|
794
1298
|
const flagRows = flags.map((flag) => {
|
|
@@ -800,17 +1304,23 @@ function showHelpMenu(context, options, commands, flags) {
|
|
|
800
1304
|
}
|
|
801
1305
|
//#endregion
|
|
802
1306
|
//#region src/intro.ts
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
resolve(data);
|
|
811
|
-
});
|
|
812
|
-
});
|
|
1307
|
+
const renderFigletAsync = promisify(figlet);
|
|
1308
|
+
async function renderFiglet(text) {
|
|
1309
|
+
try {
|
|
1310
|
+
return await renderFigletAsync(text) ?? text;
|
|
1311
|
+
} catch {
|
|
1312
|
+
return text;
|
|
1313
|
+
}
|
|
813
1314
|
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Renders a figlet banner and short intro note.
|
|
1317
|
+
*
|
|
1318
|
+
* @remarks
|
|
1319
|
+
* If figlet rendering fails, the plain banner text is displayed instead.
|
|
1320
|
+
*
|
|
1321
|
+
* @param context - Context subset providing the logger used for output.
|
|
1322
|
+
* @param options - Intro banner metadata.
|
|
1323
|
+
*/
|
|
814
1324
|
async function displayIntro(context, options) {
|
|
815
1325
|
const banner = await renderFiglet(options.figletText ?? options.appName);
|
|
816
1326
|
const versionLabel = options.version ? ` v${options.version}` : "";
|
|
@@ -819,49 +1329,104 @@ async function displayIntro(context, options) {
|
|
|
819
1329
|
}
|
|
820
1330
|
//#endregion
|
|
821
1331
|
//#region src/spinner.ts
|
|
1332
|
+
/**
|
|
1333
|
+
* Creates a Clack-backed spinner.
|
|
1334
|
+
*
|
|
1335
|
+
* @param initialMessage - Message used when `start()` is called without an
|
|
1336
|
+
* explicit message.
|
|
1337
|
+
* @returns A spinner controller.
|
|
1338
|
+
*/
|
|
822
1339
|
function createSpinner(initialMessage) {
|
|
823
|
-
const
|
|
1340
|
+
const spinnerInstance = spinner();
|
|
824
1341
|
return {
|
|
1342
|
+
message(message) {
|
|
1343
|
+
spinnerInstance.message(message);
|
|
1344
|
+
},
|
|
825
1345
|
start(message) {
|
|
826
|
-
|
|
1346
|
+
spinnerInstance.start(message ?? initialMessage ?? "Processing...");
|
|
827
1347
|
},
|
|
828
1348
|
stop(message) {
|
|
829
|
-
|
|
830
|
-
},
|
|
831
|
-
message(message) {
|
|
832
|
-
spinner.message(message);
|
|
1349
|
+
spinnerInstance.stop(message ?? "Done");
|
|
833
1350
|
}
|
|
834
1351
|
};
|
|
835
1352
|
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Runs an async task while displaying a spinner.
|
|
1355
|
+
*
|
|
1356
|
+
* @typeParam T - Value returned by the task.
|
|
1357
|
+
* @param message - Message shown when the spinner starts.
|
|
1358
|
+
* @param task - Async work to run.
|
|
1359
|
+
* @param options - Optional success and error messages shown when the task
|
|
1360
|
+
* settles.
|
|
1361
|
+
* @returns The value returned by `task`.
|
|
1362
|
+
*
|
|
1363
|
+
* @throws Re-throws any error from `task` after stopping the spinner.
|
|
1364
|
+
*/
|
|
836
1365
|
async function withSpinner(message, task, options) {
|
|
837
|
-
const
|
|
838
|
-
|
|
1366
|
+
const spinnerInstance = createSpinner(message);
|
|
1367
|
+
spinnerInstance.start();
|
|
839
1368
|
try {
|
|
840
1369
|
const result = await task();
|
|
841
|
-
|
|
1370
|
+
spinnerInstance.stop(options?.successMessage ?? "Done");
|
|
842
1371
|
return result;
|
|
843
1372
|
} catch (error) {
|
|
844
|
-
|
|
1373
|
+
spinnerInstance.stop(options?.errorMessage ?? "Failed");
|
|
845
1374
|
throw error;
|
|
846
1375
|
}
|
|
847
1376
|
}
|
|
848
1377
|
//#endregion
|
|
849
|
-
//#region src/version-check.ts
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1378
|
+
//#region src/version-check/compare.ts
|
|
1379
|
+
function parseVersionParts(version) {
|
|
1380
|
+
const [coreVersion = "", prerelease] = version.replace(/^[^\d]*/, "").split("-", 2);
|
|
1381
|
+
return {
|
|
1382
|
+
parts: coreVersion.split(".").map((part) => {
|
|
1383
|
+
const parsed = Number.parseInt(part.replace(/\D.*$/, ""), 10);
|
|
1384
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
1385
|
+
}),
|
|
1386
|
+
prerelease
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
function comparePrereleaseIdentifiers(left, right) {
|
|
1390
|
+
const leftIsNumeric = /^\d+$/.test(left);
|
|
1391
|
+
const rightIsNumeric = /^\d+$/.test(right);
|
|
1392
|
+
if (leftIsNumeric && rightIsNumeric) return Math.sign(Number.parseInt(left, 10) - Number.parseInt(right, 10));
|
|
1393
|
+
if (leftIsNumeric) return -1;
|
|
1394
|
+
if (rightIsNumeric) return 1;
|
|
1395
|
+
return Math.sign(left.localeCompare(right));
|
|
1396
|
+
}
|
|
1397
|
+
function comparePrerelease(left, right) {
|
|
1398
|
+
if (!left && !right) return 0;
|
|
1399
|
+
if (!left) return 1;
|
|
1400
|
+
if (!right) return -1;
|
|
1401
|
+
const leftParts = left.split(".");
|
|
1402
|
+
const rightParts = right.split(".");
|
|
1403
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
1404
|
+
for (let index = 0; index < length; index++) {
|
|
1405
|
+
const leftValue = leftParts[index];
|
|
1406
|
+
const rightValue = rightParts[index];
|
|
1407
|
+
if (leftValue === void 0) return -1;
|
|
1408
|
+
if (rightValue === void 0) return 1;
|
|
1409
|
+
const result = comparePrereleaseIdentifiers(leftValue, rightValue);
|
|
1410
|
+
if (result !== 0) return result;
|
|
860
1411
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1412
|
+
return 0;
|
|
1413
|
+
}
|
|
1414
|
+
function compareVersions(left, right) {
|
|
1415
|
+
const leftVersion = parseVersionParts(left);
|
|
1416
|
+
const rightVersion = parseVersionParts(right);
|
|
1417
|
+
const leftParts = leftVersion.parts;
|
|
1418
|
+
const rightParts = rightVersion.parts;
|
|
1419
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
1420
|
+
for (let index = 0; index < length; index++) {
|
|
1421
|
+
const leftValue = leftParts[index] ?? 0;
|
|
1422
|
+
const rightValue = rightParts[index] ?? 0;
|
|
1423
|
+
if (leftValue > rightValue) return 1;
|
|
1424
|
+
if (leftValue < rightValue) return -1;
|
|
1425
|
+
}
|
|
1426
|
+
return comparePrerelease(leftVersion.prerelease, rightVersion.prerelease);
|
|
864
1427
|
}
|
|
1428
|
+
//#endregion
|
|
1429
|
+
//#region src/version-check/install-source.ts
|
|
865
1430
|
function normalizePath(filePath) {
|
|
866
1431
|
return filePath.replaceAll("\\", "/");
|
|
867
1432
|
}
|
|
@@ -884,12 +1449,28 @@ function isPathUnder(candidate, parent) {
|
|
|
884
1449
|
function envValue(name) {
|
|
885
1450
|
return process.env[name];
|
|
886
1451
|
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Infers how the current CLI binary was installed.
|
|
1454
|
+
*
|
|
1455
|
+
* @remarks
|
|
1456
|
+
* Detection is heuristic and based on binary path, realpath, and package
|
|
1457
|
+
* manager environment variables. Unknown or transient install modes return
|
|
1458
|
+
* `unknown` instead of throwing.
|
|
1459
|
+
*
|
|
1460
|
+
* @param binPath - Binary path to inspect.
|
|
1461
|
+
* @returns The inferred installation source.
|
|
1462
|
+
*/
|
|
887
1463
|
function detectInstallSource(binPath = process.argv[1] ?? "") {
|
|
888
1464
|
const resolvedPath = normalizePath(safeRealpath(binPath || ""));
|
|
889
1465
|
const npmPrefix = envValue("npm_config_prefix");
|
|
1466
|
+
const execPath = normalizePath(process.execPath);
|
|
1467
|
+
const argvPath = normalizePath(process.argv[0] ?? "");
|
|
1468
|
+
const execName = path$1.basename(execPath);
|
|
1469
|
+
const argvName = path$1.basename(argvPath);
|
|
1470
|
+
const isBunInvocation = execName === "bun" || execName === "bunx" || argvName === "bun" || argvName === "bunx";
|
|
890
1471
|
if (resolvedPath.includes("/opt/homebrew/") || resolvedPath.includes("/usr/local/Cellar/") || resolvedPath.includes("/home/linuxbrew/") || resolvedPath.includes("/Homebrew/Cellar/")) return "brew";
|
|
891
1472
|
if (resolvedPath.includes("/.npm/_npx/") || resolvedPath.includes("/_npx/") || process.env.npm_command === "exec") return "npx";
|
|
892
|
-
if (resolvedPath.includes("/.bun/install/cache/") || Boolean(envValue("BUN_INSTALL"))) return "bunx";
|
|
1473
|
+
if (resolvedPath.includes("/.bun/install/cache/") || Boolean(envValue("BUN_INSTALL")) && (resolvedPath.includes("/.bun/install/run/") || isBunInvocation)) return "bunx";
|
|
893
1474
|
if (resolvedPath.includes("/.pnpm-store/") || resolvedPath.includes("/dlx-")) return "pnpm-dlx";
|
|
894
1475
|
if (resolvedPath.includes("/.yarn/berry/cache/") || resolvedPath.includes("/yarn/dlx-")) return "yarn-dlx";
|
|
895
1476
|
if (resolvedPath.includes("/node_modules/.bin/") || resolvedPath.endsWith("/node_modules/.bin")) return "local";
|
|
@@ -906,6 +1487,15 @@ function detectInstallSource(binPath = process.argv[1] ?? "") {
|
|
|
906
1487
|
})) return "npm-global";
|
|
907
1488
|
return "unknown";
|
|
908
1489
|
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Builds an update command for an installation source.
|
|
1492
|
+
*
|
|
1493
|
+
* @param source - Installation source returned by `detectInstallSource`.
|
|
1494
|
+
* @param packageName - Package name to update.
|
|
1495
|
+
* @param brewFormula - Homebrew formula name when `source` is `brew`.
|
|
1496
|
+
* @returns A command string when the source has an actionable update path,
|
|
1497
|
+
* otherwise `null`.
|
|
1498
|
+
*/
|
|
909
1499
|
function getUpdateCommand(source, packageName, brewFormula = packageName) {
|
|
910
1500
|
switch (source) {
|
|
911
1501
|
case "npm-global": return `npm install -g ${packageName}@latest`;
|
|
@@ -914,8 +1504,48 @@ function getUpdateCommand(source, packageName, brewFormula = packageName) {
|
|
|
914
1504
|
default: return null;
|
|
915
1505
|
}
|
|
916
1506
|
}
|
|
1507
|
+
//#endregion
|
|
1508
|
+
//#region src/version-check/registry.ts
|
|
1509
|
+
const DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
1510
|
+
const DEFAULT_TIMEOUT_MS = 1500;
|
|
1511
|
+
const DEFAULT_CACHE_TTL_MS = 1440 * 60 * 1e3;
|
|
1512
|
+
const MAX_CACHE_NAME_LENGTH = 80;
|
|
1513
|
+
const WINDOWS_RESERVED_NAMES = new Set([
|
|
1514
|
+
"con",
|
|
1515
|
+
"prn",
|
|
1516
|
+
"aux",
|
|
1517
|
+
"nul",
|
|
1518
|
+
"com1",
|
|
1519
|
+
"com2",
|
|
1520
|
+
"com3",
|
|
1521
|
+
"com4",
|
|
1522
|
+
"com5",
|
|
1523
|
+
"com6",
|
|
1524
|
+
"com7",
|
|
1525
|
+
"com8",
|
|
1526
|
+
"com9",
|
|
1527
|
+
"lpt1",
|
|
1528
|
+
"lpt2",
|
|
1529
|
+
"lpt3",
|
|
1530
|
+
"lpt4",
|
|
1531
|
+
"lpt5",
|
|
1532
|
+
"lpt6",
|
|
1533
|
+
"lpt7",
|
|
1534
|
+
"lpt8",
|
|
1535
|
+
"lpt9"
|
|
1536
|
+
]);
|
|
1537
|
+
function getCacheNameHash(packageName) {
|
|
1538
|
+
return createHash("sha256").update(packageName).digest("hex").slice(0, 8);
|
|
1539
|
+
}
|
|
917
1540
|
function sanitizeCacheName(packageName) {
|
|
918
|
-
|
|
1541
|
+
const normalized = packageName.normalize("NFKD").toLowerCase().replaceAll(/[^a-z0-9._-]+/g, "_").replaceAll(/_+/g, "_").replaceAll(/^[._-]+|[._-]+$/g, "");
|
|
1542
|
+
const fallbackName = normalized || "package";
|
|
1543
|
+
const baseName = fallbackName.split(".")[0] ?? fallbackName;
|
|
1544
|
+
const isReservedName = WINDOWS_RESERVED_NAMES.has(baseName);
|
|
1545
|
+
if (!(normalized !== packageName || normalized.length === 0 || isReservedName || normalized.length > MAX_CACHE_NAME_LENGTH)) return normalized;
|
|
1546
|
+
const hash = getCacheNameHash(packageName);
|
|
1547
|
+
const maxStemLength = MAX_CACHE_NAME_LENGTH - hash.length - 1;
|
|
1548
|
+
return `${(isReservedName ? `package-${fallbackName}` : fallbackName).slice(0, maxStemLength).replaceAll(/[._-]+$/g, "") || "package"}-${hash}`;
|
|
919
1549
|
}
|
|
920
1550
|
function getCacheDir(options) {
|
|
921
1551
|
return options.cacheDir ?? path$1.join(os.tmpdir(), "hexbus-version-cache");
|
|
@@ -928,8 +1558,8 @@ function readCachedVersion(options) {
|
|
|
928
1558
|
const content = fsSync$1.readFileSync(getCachePath(options), "utf-8");
|
|
929
1559
|
const parsed = JSON.parse(content);
|
|
930
1560
|
if (typeof parsed.version === "string" && typeof parsed.fetchedAt === "number") return {
|
|
931
|
-
|
|
932
|
-
|
|
1561
|
+
fetchedAt: parsed.fetchedAt,
|
|
1562
|
+
version: parsed.version
|
|
933
1563
|
};
|
|
934
1564
|
} catch {}
|
|
935
1565
|
return null;
|
|
@@ -942,52 +1572,19 @@ function isCacheFresh(cache, options) {
|
|
|
942
1572
|
async function writeCachedVersion(options, version) {
|
|
943
1573
|
const cachePath = getCachePath(options);
|
|
944
1574
|
const cacheDir = path$1.dirname(cachePath);
|
|
945
|
-
const tempPath = `${cachePath}.${process.pid}.tmp`;
|
|
1575
|
+
const tempPath = `${cachePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
946
1576
|
const payload = {
|
|
947
|
-
|
|
948
|
-
|
|
1577
|
+
fetchedAt: options.now?.() ?? Date.now(),
|
|
1578
|
+
version
|
|
949
1579
|
};
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
return Number.isNaN(parsed) ? 0 : parsed;
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
function compareVersions(left, right) {
|
|
961
|
-
const leftParts = parseVersionParts(left);
|
|
962
|
-
const rightParts = parseVersionParts(right);
|
|
963
|
-
const length = Math.max(leftParts.length, rightParts.length);
|
|
964
|
-
for (let index = 0; index < length; index++) {
|
|
965
|
-
const leftValue = leftParts[index] ?? 0;
|
|
966
|
-
const rightValue = rightParts[index] ?? 0;
|
|
967
|
-
if (leftValue > rightValue) return 1;
|
|
968
|
-
if (leftValue < rightValue) return -1;
|
|
1580
|
+
try {
|
|
1581
|
+
await fs$1.mkdir(cacheDir, { recursive: true });
|
|
1582
|
+
await fs$1.writeFile(tempPath, `${JSON.stringify(payload)}\n`, "utf-8");
|
|
1583
|
+
await fs$1.rename(tempPath, cachePath);
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
await fs$1.unlink(tempPath).catch(() => {});
|
|
1586
|
+
throw error;
|
|
969
1587
|
}
|
|
970
|
-
return 0;
|
|
971
|
-
}
|
|
972
|
-
function createResult(options, latestVersion) {
|
|
973
|
-
const source = detectInstallSource(options.binPath);
|
|
974
|
-
const updateCommand = getUpdateCommand(source, options.packageName, options.brewFormula);
|
|
975
|
-
const isOutdated = typeof latestVersion === "string" && compareVersions(options.currentVersion, latestVersion) < 0;
|
|
976
|
-
const result = {
|
|
977
|
-
currentVersion: options.currentVersion,
|
|
978
|
-
latestVersion,
|
|
979
|
-
isOutdated,
|
|
980
|
-
source,
|
|
981
|
-
updateCommand
|
|
982
|
-
};
|
|
983
|
-
const hint = formatUpdateHint({
|
|
984
|
-
...result,
|
|
985
|
-
hint: null
|
|
986
|
-
});
|
|
987
|
-
return {
|
|
988
|
-
...result,
|
|
989
|
-
hint
|
|
990
|
-
};
|
|
991
1588
|
}
|
|
992
1589
|
async function fetchLatestVersion(options) {
|
|
993
1590
|
const controller = new AbortController();
|
|
@@ -1010,11 +1607,24 @@ async function refreshCache(options) {
|
|
|
1010
1607
|
if (latestVersion) await writeCachedVersion(options, latestVersion);
|
|
1011
1608
|
return latestVersion;
|
|
1012
1609
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1610
|
+
//#endregion
|
|
1611
|
+
//#region src/version-check/check.ts
|
|
1612
|
+
/**
|
|
1613
|
+
* Checks whether raw arguments request version output.
|
|
1614
|
+
*
|
|
1615
|
+
* @param rawArgs - Arguments after executable and script path.
|
|
1616
|
+
* @returns `true` when `-v` or `--version` is present.
|
|
1617
|
+
*/
|
|
1618
|
+
function isVersionRequest(rawArgs) {
|
|
1619
|
+
return rawArgs.includes("-v") || rawArgs.includes("--version");
|
|
1017
1620
|
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Formats a user-facing update hint from an update-check result.
|
|
1623
|
+
*
|
|
1624
|
+
* @param result - Update-check result to render.
|
|
1625
|
+
* @returns A formatted hint when the result is outdated and has an update
|
|
1626
|
+
* command, otherwise `null`.
|
|
1627
|
+
*/
|
|
1018
1628
|
function formatUpdateHint(result) {
|
|
1019
1629
|
if (!result.isOutdated || result.updateCommand === null || result.latestVersion === null) return null;
|
|
1020
1630
|
if (result.source === "brew") return [
|
|
@@ -1028,23 +1638,95 @@ function formatUpdateHint(result) {
|
|
|
1028
1638
|
` ${color.cyan(result.updateCommand)}`
|
|
1029
1639
|
].join("\n");
|
|
1030
1640
|
}
|
|
1641
|
+
function createUpdateCheckResult(options, latestVersion) {
|
|
1642
|
+
const source = detectInstallSource(options.binPath);
|
|
1643
|
+
const updateCommand = getUpdateCommand(source, options.packageName, options.brewFormula);
|
|
1644
|
+
const isOutdated = typeof latestVersion === "string" && compareVersions(options.currentVersion, latestVersion) < 0;
|
|
1645
|
+
const result = {
|
|
1646
|
+
currentVersion: options.currentVersion,
|
|
1647
|
+
isOutdated,
|
|
1648
|
+
latestVersion,
|
|
1649
|
+
source,
|
|
1650
|
+
updateCommand
|
|
1651
|
+
};
|
|
1652
|
+
const hint = formatUpdateHint({
|
|
1653
|
+
...result,
|
|
1654
|
+
hint: null
|
|
1655
|
+
});
|
|
1656
|
+
return {
|
|
1657
|
+
...result,
|
|
1658
|
+
hint
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Checks whether a newer package version is available.
|
|
1663
|
+
*
|
|
1664
|
+
* @remarks
|
|
1665
|
+
* The function first reads the local cache. On a cache miss it refreshes the
|
|
1666
|
+
* cache from the configured registry. Refresh failures produce a result with
|
|
1667
|
+
* `latestVersion: null` rather than throwing.
|
|
1668
|
+
*
|
|
1669
|
+
* @param options - Update-check configuration.
|
|
1670
|
+
* @returns Update metadata and a formatted hint when an update is available.
|
|
1671
|
+
*/
|
|
1672
|
+
async function checkForUpdate(options) {
|
|
1673
|
+
const cached = readCachedVersion(options);
|
|
1674
|
+
if (cached) return createUpdateCheckResult(options, cached.version);
|
|
1675
|
+
try {
|
|
1676
|
+
return createUpdateCheckResult(options, await refreshCache(options));
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
options.logger?.debug?.(`Update check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1679
|
+
return createUpdateCheckResult(options, null);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
//#endregion
|
|
1683
|
+
//#region src/version-check/display.ts
|
|
1684
|
+
const defaultLogger = {
|
|
1685
|
+
message(message) {
|
|
1686
|
+
process.stdout.write(`${message}\n`);
|
|
1687
|
+
},
|
|
1688
|
+
note(content, title) {
|
|
1689
|
+
const prefix = title ? `${title}\n` : "";
|
|
1690
|
+
process.stdout.write(`${prefix}${content}\n`);
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
/**
|
|
1694
|
+
* Prints CLI version information and any available update hint.
|
|
1695
|
+
*
|
|
1696
|
+
* @param options - Version display and update-check options.
|
|
1697
|
+
*/
|
|
1031
1698
|
async function printVersionInfo(options) {
|
|
1032
1699
|
const logger = options.logger ?? defaultLogger;
|
|
1033
1700
|
logger.message(`${options.appName} v${options.currentVersion}`);
|
|
1034
1701
|
const result = await checkForUpdate(options);
|
|
1035
1702
|
if (result.hint) logger.note(result.hint, "Update available");
|
|
1036
1703
|
}
|
|
1704
|
+
async function refreshCacheInBackground(options, logger) {
|
|
1705
|
+
try {
|
|
1706
|
+
await refreshCache(options);
|
|
1707
|
+
} catch (error) {
|
|
1708
|
+
logger.debug?.(`Update check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Starts a non-blocking update check.
|
|
1713
|
+
*
|
|
1714
|
+
* @remarks
|
|
1715
|
+
* Cached hints are displayed synchronously when available. If the cache is
|
|
1716
|
+
* stale or missing, a refresh is started in the background and failures are
|
|
1717
|
+
* only logged at debug level.
|
|
1718
|
+
*
|
|
1719
|
+
* @param options - Version display and update-check options.
|
|
1720
|
+
*/
|
|
1037
1721
|
function startBackgroundUpdateCheck(options) {
|
|
1038
1722
|
const logger = options.logger ?? defaultLogger;
|
|
1039
1723
|
const cached = readCachedVersion(options);
|
|
1040
1724
|
if (cached) {
|
|
1041
|
-
const result =
|
|
1725
|
+
const result = createUpdateCheckResult(options, cached.version);
|
|
1042
1726
|
if (result.hint) logger.note(result.hint, "Update available");
|
|
1043
1727
|
}
|
|
1044
1728
|
if (cached && isCacheFresh(cached, options)) return;
|
|
1045
|
-
|
|
1046
|
-
logger.debug?.(`Update check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1047
|
-
});
|
|
1729
|
+
refreshCacheInBackground(options, logger);
|
|
1048
1730
|
}
|
|
1049
1731
|
//#endregion
|
|
1050
1732
|
export { CliError, DEFAULT_ERROR_CATALOG, LOG_LEVELS, TelemetryEventName, checkForUpdate, color, createCliContext, createCliLogger, createDisabledTelemetry, createErrorHandlers, createSpinner, createTelemetry, createTestContext, detectFramework, detectInstallSource, detectPackageManager, detectProjectRoot, displayIntro, extendErrorCatalog, formatFlagHelp, formatLogMessage, formatStep, formatUpdateHint, generateFlagsHelp, getExecCommand, getFlagValue, getInstallCommand, getRunCommand, getUpdateCommand, globalFlags, hasFlag, isCliError, isVersionRequest, logMessage, parseCliArgs, parseSubcommand, printVersionInfo, showHelpMenu, startBackgroundUpdateCheck, validLogLevels, withErrorHandling, withSpinner };
|