hexbus 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -10
- package/dist/index.d.mts +1242 -38
- package/dist/index.mjs +990 -283
- 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,12 +794,51 @@ 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
|
}
|
|
455
|
-
function
|
|
797
|
+
function mergeFlags(flags) {
|
|
798
|
+
const merged = /* @__PURE__ */ new Map();
|
|
799
|
+
for (const flag of globalFlags) {
|
|
800
|
+
const primaryName = getPrimaryFlagName(flag);
|
|
801
|
+
if (primaryName) merged.set(primaryName, flag);
|
|
802
|
+
}
|
|
803
|
+
for (const flag of flags) {
|
|
804
|
+
const primaryName = getPrimaryFlagName(flag);
|
|
805
|
+
if (primaryName) merged.set(primaryName, flag);
|
|
806
|
+
}
|
|
807
|
+
return [...merged.values()];
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Parses raw command-line arguments into command name, command args, and
|
|
811
|
+
* global and caller-provided flags.
|
|
812
|
+
*
|
|
813
|
+
* @remarks
|
|
814
|
+
* Flags declared in `globalFlags` and `flags` are parsed. Unknown flags and
|
|
815
|
+
* other positional arguments are preserved as command arguments. If the first
|
|
816
|
+
* positional argument matches a registered top-level command in `commands`, it
|
|
817
|
+
* is returned as `commandName` and removed from `commandArgs`.
|
|
818
|
+
*
|
|
819
|
+
* @param rawArgs - Arguments after the executable and script path.
|
|
820
|
+
* @param commands - Top-level commands used to identify the command name.
|
|
821
|
+
* @param flags - Additional global flag definitions to parse alongside
|
|
822
|
+
* `globalFlags`. Caller-provided flags override built-in definitions that use
|
|
823
|
+
* the same primary flag name.
|
|
824
|
+
* @returns Normalized parsed output containing the matched command name,
|
|
825
|
+
* remaining command arguments, and parsed flag values keyed by primary name.
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* ```ts
|
|
829
|
+
* const parsed = parseCliArgs(['init', '--logger', 'debug'], commands);
|
|
830
|
+
* // parsed.commandName === 'init'
|
|
831
|
+
* // parsed.parsedFlags.logger === 'debug'
|
|
832
|
+
* ```
|
|
833
|
+
*/
|
|
834
|
+
function parseCliArgs(rawArgs, commands, flags = []) {
|
|
835
|
+
const mergedFlags = mergeFlags(flags);
|
|
456
836
|
const parsedFlags = {};
|
|
457
837
|
const potentialCommandArgs = [];
|
|
458
838
|
let commandName;
|
|
459
839
|
const commandArgs = [];
|
|
460
|
-
|
|
840
|
+
const knownFlagSet = new Set(mergedFlags.flatMap((flag) => flag.names));
|
|
841
|
+
for (const flag of mergedFlags) {
|
|
461
842
|
const primaryName = getPrimaryFlagName(flag);
|
|
462
843
|
if (!primaryName) continue;
|
|
463
844
|
if (flag.type === "boolean") parsedFlags[primaryName] = flag.defaultValue ?? false;
|
|
@@ -467,7 +848,7 @@ function parseCliArgs(rawArgs, commands) {
|
|
|
467
848
|
const arg = rawArgs[i];
|
|
468
849
|
if (typeof arg !== "string") continue;
|
|
469
850
|
let isFlag = false;
|
|
470
|
-
for (const flag of
|
|
851
|
+
for (const flag of mergedFlags) {
|
|
471
852
|
if (!flag.names.includes(arg)) continue;
|
|
472
853
|
const primaryName = getPrimaryFlagName(flag);
|
|
473
854
|
if (!primaryName) continue;
|
|
@@ -475,7 +856,7 @@ function parseCliArgs(rawArgs, commands) {
|
|
|
475
856
|
if (flag.type === "boolean") parsedFlags[primaryName] = true;
|
|
476
857
|
else if (flag.expectsValue) {
|
|
477
858
|
const nextArg = rawArgs[i + 1];
|
|
478
|
-
if (nextArg && !
|
|
859
|
+
if (nextArg && !knownFlagSet.has(nextArg)) {
|
|
479
860
|
parsedFlags[primaryName] = nextArg;
|
|
480
861
|
i++;
|
|
481
862
|
} else p.log.warn(formatLogMessage("warn", `Flag ${arg} expects a value, but none was provided`));
|
|
@@ -484,72 +865,146 @@ function parseCliArgs(rawArgs, commands) {
|
|
|
484
865
|
}
|
|
485
866
|
if (!isFlag) potentialCommandArgs.push(arg);
|
|
486
867
|
}
|
|
487
|
-
const firstPositional = potentialCommandArgs
|
|
868
|
+
const [firstPositional] = potentialCommandArgs;
|
|
488
869
|
if (typeof firstPositional === "string" && commands.some((cmd) => cmd.name === firstPositional)) {
|
|
489
870
|
commandName = firstPositional;
|
|
490
871
|
commandArgs.push(...potentialCommandArgs.slice(1));
|
|
491
872
|
} else commandArgs.push(...potentialCommandArgs);
|
|
492
873
|
return {
|
|
493
|
-
commandName,
|
|
494
874
|
commandArgs,
|
|
875
|
+
commandName,
|
|
495
876
|
parsedFlags
|
|
496
877
|
};
|
|
497
878
|
}
|
|
879
|
+
/**
|
|
880
|
+
* Formats a single flag for display in help output.
|
|
881
|
+
*
|
|
882
|
+
* @param flag - Flag definition to render.
|
|
883
|
+
* @returns A help row containing names, value hint, and description.
|
|
884
|
+
*/
|
|
498
885
|
function formatFlagHelp(flag) {
|
|
499
886
|
return ` ${flag.names.join(", ")}${flag.expectsValue ? " <value>" : ""}\t${flag.description}`;
|
|
500
887
|
}
|
|
501
|
-
|
|
502
|
-
|
|
888
|
+
/**
|
|
889
|
+
* Formats a provided set of flags for help output.
|
|
890
|
+
*
|
|
891
|
+
* @param flags - Flag definitions to render with `formatFlagHelp`, defaulting
|
|
892
|
+
* to `globalFlags`.
|
|
893
|
+
*
|
|
894
|
+
* @returns Newline-delimited help rows for the provided flag definitions.
|
|
895
|
+
*/
|
|
896
|
+
function generateFlagsHelp(flags = globalFlags) {
|
|
897
|
+
return flags.map(formatFlagHelp).join("\n");
|
|
503
898
|
}
|
|
899
|
+
/**
|
|
900
|
+
* Checks whether a parsed boolean flag is enabled.
|
|
901
|
+
*
|
|
902
|
+
* @param flags - Parsed flag map from `parseCliArgs` or `CliContext`.
|
|
903
|
+
* @param name - Primary flag name without leading dashes.
|
|
904
|
+
* @returns `true` only when the flag value is exactly `true`.
|
|
905
|
+
*
|
|
906
|
+
* @example
|
|
907
|
+
* ```ts
|
|
908
|
+
* if (hasFlag(context.flags, 'force')) {
|
|
909
|
+
* // overwrite existing files
|
|
910
|
+
* }
|
|
911
|
+
* ```
|
|
912
|
+
*/
|
|
504
913
|
function hasFlag(flags, name) {
|
|
505
914
|
return flags[name] === true;
|
|
506
915
|
}
|
|
916
|
+
/**
|
|
917
|
+
* Reads a string-valued flag from a parsed flag map.
|
|
918
|
+
*
|
|
919
|
+
* @param flags - Parsed flag map from `parseCliArgs` or `CliContext`.
|
|
920
|
+
* @param name - Primary flag name without leading dashes.
|
|
921
|
+
* @returns The flag value when it is a string, otherwise `undefined`.
|
|
922
|
+
*/
|
|
507
923
|
function getFlagValue(flags, name) {
|
|
508
924
|
const value = flags[name];
|
|
509
925
|
if (typeof value === "string") return value;
|
|
510
926
|
}
|
|
927
|
+
/**
|
|
928
|
+
* Splits command arguments into an optional nested subcommand and remaining
|
|
929
|
+
* args.
|
|
930
|
+
*
|
|
931
|
+
* @param args - Positional command arguments to inspect.
|
|
932
|
+
* @param subcommands - Available subcommands for the current command.
|
|
933
|
+
* @returns The matched subcommand, if any, plus arguments after the subcommand
|
|
934
|
+
* name.
|
|
935
|
+
*
|
|
936
|
+
* @example
|
|
937
|
+
* ```ts
|
|
938
|
+
* const { subcommand, remainingArgs } = parseSubcommand(
|
|
939
|
+
* context.commandArgs,
|
|
940
|
+
* command.subcommands ?? []
|
|
941
|
+
* );
|
|
942
|
+
* ```
|
|
943
|
+
*/
|
|
511
944
|
function parseSubcommand(args, subcommands) {
|
|
512
|
-
const subcommandName = args
|
|
945
|
+
const [subcommandName] = args;
|
|
513
946
|
const subcommand = subcommands.find((cmd) => cmd.name === subcommandName);
|
|
514
947
|
if (subcommand) return {
|
|
515
|
-
|
|
516
|
-
|
|
948
|
+
remainingArgs: args.slice(1),
|
|
949
|
+
subcommand
|
|
517
950
|
};
|
|
518
951
|
return {
|
|
519
|
-
|
|
520
|
-
|
|
952
|
+
remainingArgs: args,
|
|
953
|
+
subcommand: void 0
|
|
521
954
|
};
|
|
522
955
|
}
|
|
523
956
|
//#endregion
|
|
524
957
|
//#region src/telemetry.ts
|
|
958
|
+
/**
|
|
959
|
+
* Standard telemetry event names emitted by Hexbus runtime helpers.
|
|
960
|
+
*/
|
|
525
961
|
const TelemetryEventName = {
|
|
526
|
-
CLI_INVOKED: "cli_invoked",
|
|
527
|
-
CLI_ENVIRONMENT_DETECTED: "cli_environment_detected",
|
|
528
962
|
CLI_COMPLETED: "cli_completed",
|
|
963
|
+
CLI_ENVIRONMENT_DETECTED: "cli_environment_detected",
|
|
964
|
+
CLI_INVOKED: "cli_invoked",
|
|
965
|
+
COMMAND_FAILED: "command_failed",
|
|
529
966
|
COMMAND_INVOKED: "command_invoked",
|
|
530
967
|
COMMAND_SUCCEEDED: "command_succeeded",
|
|
531
|
-
COMMAND_FAILED: "command_failed",
|
|
532
968
|
COMMAND_UNKNOWN: "command_unknown",
|
|
533
969
|
ERROR_OCCURRED: "error_occurred",
|
|
534
970
|
HELP_DISPLAYED: "help_displayed",
|
|
535
|
-
|
|
971
|
+
INTERACTIVE_MENU_EXITED: "interactive_menu_exited",
|
|
536
972
|
INTERACTIVE_MENU_OPENED: "interactive_menu_opened",
|
|
537
|
-
|
|
973
|
+
VERSION_DISPLAYED: "version_displayed"
|
|
538
974
|
};
|
|
539
975
|
function isEnvDisabled(prefix) {
|
|
540
976
|
const value = process.env[`${prefix}_TELEMETRY_DISABLED`];
|
|
541
977
|
return value === "1" || value === "true";
|
|
542
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Creates a no-op telemetry client.
|
|
981
|
+
*
|
|
982
|
+
* @returns A telemetry implementation whose methods do nothing and whose
|
|
983
|
+
* `isDisabled()` method returns `true`.
|
|
984
|
+
*/
|
|
543
985
|
function createDisabledTelemetry() {
|
|
544
986
|
return {
|
|
545
|
-
trackEvent: () => {},
|
|
546
|
-
trackCommand: () => {},
|
|
547
|
-
trackError: () => {},
|
|
548
987
|
flush: async () => {},
|
|
988
|
+
flushBackground: () => {},
|
|
989
|
+
isDisabled: () => true,
|
|
549
990
|
shutdown: async () => {},
|
|
550
|
-
|
|
991
|
+
trackCommand: () => {},
|
|
992
|
+
trackError: () => {},
|
|
993
|
+
trackEvent: () => {}
|
|
551
994
|
};
|
|
552
995
|
}
|
|
996
|
+
/**
|
|
997
|
+
* Creates the built-in telemetry client.
|
|
998
|
+
*
|
|
999
|
+
* @remarks
|
|
1000
|
+
* Events are queued in memory. `flush()` posts the queue to `endpoint` when one
|
|
1001
|
+
* is configured, then clears the queue. Failed flushes are reported through
|
|
1002
|
+
* the optional logger and do not throw, keeping telemetry best effort.
|
|
1003
|
+
*
|
|
1004
|
+
* @param options - Telemetry behavior and event defaults.
|
|
1005
|
+
* @returns An enabled or disabled telemetry client depending on options and
|
|
1006
|
+
* environment opt-out variables.
|
|
1007
|
+
*/
|
|
553
1008
|
function createTelemetry(options = {}) {
|
|
554
1009
|
const envVarPrefix = options.envVarPrefix ?? "APP";
|
|
555
1010
|
const disabled = options.disabled === true || isEnvDisabled(envVarPrefix);
|
|
@@ -573,45 +1028,57 @@ function createTelemetry(options = {}) {
|
|
|
573
1028
|
events.length = 0;
|
|
574
1029
|
return;
|
|
575
1030
|
}
|
|
576
|
-
const batch = events.splice(0
|
|
1031
|
+
const batch = events.splice(0);
|
|
577
1032
|
try {
|
|
578
1033
|
await fetch(options.endpoint, {
|
|
579
|
-
method: "POST",
|
|
580
|
-
headers: { "content-type": "application/json" },
|
|
581
1034
|
body: JSON.stringify({ events: batch }),
|
|
582
|
-
|
|
1035
|
+
headers: { "content-type": "application/json" },
|
|
1036
|
+
keepalive: true,
|
|
1037
|
+
method: "POST"
|
|
583
1038
|
});
|
|
584
1039
|
} catch (error) {
|
|
585
1040
|
options.logger?.warn(`Failed to send telemetry: ${error instanceof Error ? error.message : String(error)}`);
|
|
586
1041
|
}
|
|
587
1042
|
};
|
|
588
1043
|
return {
|
|
589
|
-
|
|
1044
|
+
flush,
|
|
1045
|
+
flushBackground() {
|
|
1046
|
+
flush();
|
|
1047
|
+
},
|
|
1048
|
+
isDisabled() {
|
|
1049
|
+
return false;
|
|
1050
|
+
},
|
|
1051
|
+
async shutdown() {
|
|
1052
|
+
await flush();
|
|
1053
|
+
},
|
|
590
1054
|
trackCommand(command, args = [], flags = {}) {
|
|
1055
|
+
const enabledFlags = Object.entries(flags).filter(([, value]) => value !== false && value !== void 0).map(([key]) => key);
|
|
1056
|
+
enabledFlags.sort();
|
|
591
1057
|
trackEvent(TelemetryEventName.COMMAND_INVOKED, {
|
|
592
|
-
command,
|
|
593
1058
|
argsCount: args.length,
|
|
594
|
-
|
|
1059
|
+
command,
|
|
1060
|
+
enabledFlags
|
|
595
1061
|
});
|
|
596
1062
|
},
|
|
597
1063
|
trackError(error, command) {
|
|
598
1064
|
trackEvent(TelemetryEventName.ERROR_OCCURRED, {
|
|
599
1065
|
command,
|
|
600
|
-
|
|
601
|
-
|
|
1066
|
+
errorMessage: error.message,
|
|
1067
|
+
errorName: error.name
|
|
602
1068
|
});
|
|
603
1069
|
},
|
|
604
|
-
|
|
605
|
-
async shutdown() {
|
|
606
|
-
await flush();
|
|
607
|
-
},
|
|
608
|
-
isDisabled() {
|
|
609
|
-
return false;
|
|
610
|
-
}
|
|
1070
|
+
trackEvent
|
|
611
1071
|
};
|
|
612
1072
|
}
|
|
613
1073
|
//#endregion
|
|
614
1074
|
//#region src/context.ts
|
|
1075
|
+
/**
|
|
1076
|
+
* Resolves the active logger level from parsed CLI flags.
|
|
1077
|
+
*
|
|
1078
|
+
* @param parsedFlags - Parsed global flags from the current invocation.
|
|
1079
|
+
* @returns A valid log level, falling back to `info` for missing or invalid
|
|
1080
|
+
* values.
|
|
1081
|
+
*/
|
|
615
1082
|
function getLogLevel(parsedFlags) {
|
|
616
1083
|
const levelArg = parsedFlags.logger;
|
|
617
1084
|
if (typeof levelArg === "string") {
|
|
@@ -620,17 +1087,34 @@ function getLogLevel(parsedFlags) {
|
|
|
620
1087
|
}
|
|
621
1088
|
return "info";
|
|
622
1089
|
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Creates file-system helpers scoped to a project root.
|
|
1092
|
+
*
|
|
1093
|
+
* @param cwd - Project root used for package metadata lookup.
|
|
1094
|
+
* @returns File-system utilities for context consumers.
|
|
1095
|
+
*/
|
|
623
1096
|
function createFileSystem(cwd) {
|
|
624
1097
|
return {
|
|
1098
|
+
async exists(filePath) {
|
|
1099
|
+
try {
|
|
1100
|
+
await fs.access(filePath);
|
|
1101
|
+
return true;
|
|
1102
|
+
} catch {
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
},
|
|
625
1106
|
getPackageInfo() {
|
|
626
1107
|
const packageJsonPath = path.join(cwd, "package.json");
|
|
627
1108
|
try {
|
|
628
1109
|
const content = fsSync.readFileSync(packageJsonPath, "utf-8");
|
|
629
1110
|
const packageInfo = JSON.parse(content);
|
|
1111
|
+
const packageFields = packageInfo && typeof packageInfo === "object" && !Array.isArray(packageInfo) ? packageInfo : {};
|
|
1112
|
+
const name = typeof packageFields.name === "string" ? packageFields.name : "unknown";
|
|
1113
|
+
const version = typeof packageFields.version === "string" ? packageFields.version : "unknown";
|
|
630
1114
|
return {
|
|
631
|
-
...
|
|
632
|
-
name:
|
|
633
|
-
version:
|
|
1115
|
+
...packageFields,
|
|
1116
|
+
name: name || "unknown",
|
|
1117
|
+
version: version || "unknown"
|
|
634
1118
|
};
|
|
635
1119
|
} catch {
|
|
636
1120
|
return {
|
|
@@ -639,66 +1123,83 @@ function createFileSystem(cwd) {
|
|
|
639
1123
|
};
|
|
640
1124
|
}
|
|
641
1125
|
},
|
|
642
|
-
async
|
|
643
|
-
|
|
644
|
-
await fs.access(filePath);
|
|
645
|
-
return true;
|
|
646
|
-
} catch {
|
|
647
|
-
return false;
|
|
648
|
-
}
|
|
1126
|
+
async mkdir(dirPath) {
|
|
1127
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
649
1128
|
},
|
|
650
1129
|
read(filePath) {
|
|
651
1130
|
return fs.readFile(filePath, "utf-8");
|
|
652
1131
|
},
|
|
653
1132
|
write(filePath, content) {
|
|
654
1133
|
return fs.writeFile(filePath, content, "utf-8");
|
|
655
|
-
},
|
|
656
|
-
async mkdir(dirPath) {
|
|
657
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
658
1134
|
}
|
|
659
1135
|
};
|
|
660
1136
|
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Creates the resolved context passed to command actions.
|
|
1139
|
+
*
|
|
1140
|
+
* @remarks
|
|
1141
|
+
* Context creation performs the standard CLI bootstrap sequence: parse global
|
|
1142
|
+
* flags, create the logger, detect the project root, detect framework and
|
|
1143
|
+
* package manager metadata, set up telemetry, and attach helpers for config
|
|
1144
|
+
* loading, file-system access, confirmation prompts, and process-ending error
|
|
1145
|
+
* handling.
|
|
1146
|
+
*
|
|
1147
|
+
* @typeParam TPackage - Product-specific package identifier returned from
|
|
1148
|
+
* framework detection.
|
|
1149
|
+
* @param options - Context creation options for the current invocation.
|
|
1150
|
+
* @returns A fully initialized `CliContext`.
|
|
1151
|
+
*
|
|
1152
|
+
* @example
|
|
1153
|
+
* ```ts
|
|
1154
|
+
* const context = await createCliContext({
|
|
1155
|
+
* rawArgs: process.argv.slice(2),
|
|
1156
|
+
* appName: 'acme',
|
|
1157
|
+
* commands,
|
|
1158
|
+
* });
|
|
1159
|
+
*
|
|
1160
|
+
* await commands.find((command) => command.name === context.commandName)
|
|
1161
|
+
* ?.action(context);
|
|
1162
|
+
* ```
|
|
1163
|
+
*/
|
|
661
1164
|
async function createCliContext(options) {
|
|
662
1165
|
const cwd = options.cwd ?? process.cwd();
|
|
663
1166
|
const appName = options.appName ?? "cli";
|
|
664
|
-
const { commandName, commandArgs, parsedFlags } = parseCliArgs(options.rawArgs, options.commands);
|
|
1167
|
+
const { commandName, commandArgs, parsedFlags } = parseCliArgs(options.rawArgs, options.commands, options.globalFlags);
|
|
665
1168
|
const logger = createCliLogger(getLogLevel(parsedFlags));
|
|
666
1169
|
const projectRoot = await detectProjectRoot(cwd, logger);
|
|
667
1170
|
const fsUtils = createFileSystem(projectRoot);
|
|
668
1171
|
const framework = await detectFramework(projectRoot, logger, options.packageMap);
|
|
669
1172
|
const packageManager = await detectPackageManager(projectRoot, logger, { interactive: options.interactivePackageManagerDetection });
|
|
670
1173
|
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
1174
|
appName,
|
|
675
|
-
|
|
1175
|
+
debug: options.telemetry?.debug === true || parsedFlags["telemetry-debug"] === true,
|
|
676
1176
|
defaultProperties: {
|
|
677
|
-
entryCommand: commandName ?? "interactive",
|
|
678
|
-
commandArgsCount: commandArgs.length,
|
|
679
1177
|
cliVersion: fsUtils.getPackageInfo().version,
|
|
1178
|
+
commandArgsCount: commandArgs.length,
|
|
1179
|
+
entryCommand: commandName ?? "interactive",
|
|
680
1180
|
framework: framework.framework ?? "unknown",
|
|
681
1181
|
frameworkVersion: framework.frameworkVersion ?? "unknown",
|
|
682
1182
|
packageManager: packageManager.name,
|
|
683
1183
|
...options.telemetry?.defaultProperties
|
|
684
1184
|
},
|
|
1185
|
+
disabled: options.telemetry?.disabled === true || parsedFlags["no-telemetry"] === true,
|
|
1186
|
+
endpoint: options.telemetry?.endpoint,
|
|
1187
|
+
envVarPrefix: options.telemetry?.envVarPrefix ?? appName.toUpperCase(),
|
|
685
1188
|
logger
|
|
686
1189
|
});
|
|
687
1190
|
const errorHandlers = createErrorHandlers(logger, telemetry);
|
|
688
1191
|
const context = {
|
|
689
|
-
logger,
|
|
690
|
-
flags: parsedFlags,
|
|
691
|
-
commandName,
|
|
692
1192
|
commandArgs,
|
|
693
|
-
|
|
694
|
-
error: errorHandlers,
|
|
1193
|
+
commandName,
|
|
695
1194
|
config: {
|
|
1195
|
+
getPathAliases() {
|
|
1196
|
+
return null;
|
|
1197
|
+
},
|
|
696
1198
|
async loadConfig() {
|
|
697
|
-
const configPath = typeof parsedFlags.config === "string" ? parsedFlags.config : void 0;
|
|
698
1199
|
const { config } = await loadConfig({
|
|
699
|
-
|
|
1200
|
+
configFile: typeof parsedFlags.config === "string" ? parsedFlags.config : void 0,
|
|
700
1201
|
cwd: projectRoot,
|
|
701
|
-
|
|
1202
|
+
name: options.configName ?? appName
|
|
702
1203
|
});
|
|
703
1204
|
return config ?? null;
|
|
704
1205
|
},
|
|
@@ -706,89 +1207,117 @@ async function createCliContext(options) {
|
|
|
706
1207
|
const config = await this.loadConfig();
|
|
707
1208
|
if (!config) throw new CliError("CONFIG_NOT_FOUND");
|
|
708
1209
|
return config;
|
|
709
|
-
},
|
|
710
|
-
getPathAliases() {
|
|
711
|
-
return null;
|
|
712
1210
|
}
|
|
713
1211
|
},
|
|
714
|
-
fs: fsUtils,
|
|
715
|
-
telemetry,
|
|
716
1212
|
async confirm(message, initialValue = true) {
|
|
717
1213
|
if (parsedFlags.y === true || parsedFlags.yes === true) return true;
|
|
718
1214
|
const result = await p.confirm({
|
|
719
|
-
|
|
720
|
-
|
|
1215
|
+
initialValue,
|
|
1216
|
+
message
|
|
721
1217
|
});
|
|
722
1218
|
if (p.isCancel(result)) errorHandlers.handleCancel("Confirmation cancelled");
|
|
723
1219
|
return result;
|
|
724
1220
|
},
|
|
725
|
-
|
|
1221
|
+
cwd,
|
|
1222
|
+
error: errorHandlers,
|
|
1223
|
+
flags: parsedFlags,
|
|
726
1224
|
framework,
|
|
727
|
-
|
|
1225
|
+
fs: fsUtils,
|
|
1226
|
+
logger,
|
|
1227
|
+
packageManager,
|
|
1228
|
+
projectRoot,
|
|
1229
|
+
telemetry
|
|
728
1230
|
};
|
|
729
1231
|
telemetry.trackEvent(TelemetryEventName.CLI_ENVIRONMENT_DETECTED, {
|
|
730
1232
|
command: commandName ?? "interactive",
|
|
731
|
-
projectRootChanged: projectRoot !== cwd,
|
|
732
1233
|
framework: framework.framework ?? "unknown",
|
|
733
1234
|
frameworkVersion: framework.frameworkVersion ?? "unknown",
|
|
734
|
-
packageManager: packageManager.name,
|
|
735
1235
|
hasReact: framework.hasReact,
|
|
1236
|
+
packageManager: packageManager.name,
|
|
1237
|
+
projectRootChanged: projectRoot !== cwd,
|
|
736
1238
|
reactVersion: framework.reactVersion ?? "unknown",
|
|
737
1239
|
tailwindVersion: framework.tailwindVersion ?? "unknown"
|
|
738
1240
|
});
|
|
739
1241
|
return context;
|
|
740
1242
|
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Creates a deterministic context for unit tests.
|
|
1245
|
+
*
|
|
1246
|
+
* @remarks
|
|
1247
|
+
* The test context disables telemetry, uses an error-only logger, avoids real
|
|
1248
|
+
* framework or package-manager detection, and provides no-op file-system
|
|
1249
|
+
* helpers. Pass overrides to replace only the services a test needs.
|
|
1250
|
+
*
|
|
1251
|
+
* @param overrides - Partial context fields to merge into the default test
|
|
1252
|
+
* context.
|
|
1253
|
+
* @returns A `CliContext` suitable for command and helper tests.
|
|
1254
|
+
*
|
|
1255
|
+
* @example
|
|
1256
|
+
* ```ts
|
|
1257
|
+
* const context = createTestContext({
|
|
1258
|
+
* flags: { force: true },
|
|
1259
|
+
* commandName: 'init',
|
|
1260
|
+
* });
|
|
1261
|
+
* ```
|
|
1262
|
+
*/
|
|
741
1263
|
function createTestContext(overrides = {}) {
|
|
742
1264
|
const logger = createCliLogger("error");
|
|
743
1265
|
const telemetry = createDisabledTelemetry();
|
|
744
1266
|
const error = createErrorHandlers(logger, telemetry);
|
|
745
1267
|
return {
|
|
746
|
-
logger,
|
|
747
|
-
flags: {},
|
|
748
|
-
commandName: void 0,
|
|
749
1268
|
commandArgs: [],
|
|
750
|
-
|
|
751
|
-
error,
|
|
1269
|
+
commandName: void 0,
|
|
752
1270
|
config: {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
},
|
|
757
|
-
getPathAliases: () => null
|
|
1271
|
+
getPathAliases: () => null,
|
|
1272
|
+
loadConfig: () => Promise.resolve(null),
|
|
1273
|
+
requireConfig: () => Promise.reject(new CliError("CONFIG_NOT_FOUND"))
|
|
758
1274
|
},
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}),
|
|
764
|
-
exists: async () => false,
|
|
765
|
-
read: async () => "",
|
|
766
|
-
write: async () => {},
|
|
767
|
-
mkdir: async () => {}
|
|
768
|
-
},
|
|
769
|
-
telemetry,
|
|
770
|
-
confirm: async () => true,
|
|
771
|
-
projectRoot: process.cwd(),
|
|
1275
|
+
confirm: () => Promise.resolve(true),
|
|
1276
|
+
cwd: process.cwd(),
|
|
1277
|
+
error,
|
|
1278
|
+
flags: {},
|
|
772
1279
|
framework: {
|
|
773
1280
|
framework: null,
|
|
774
1281
|
frameworkVersion: null,
|
|
775
|
-
pkg: null,
|
|
776
1282
|
hasReact: false,
|
|
1283
|
+
pkg: null,
|
|
777
1284
|
reactVersion: null,
|
|
778
1285
|
tailwindVersion: null
|
|
779
1286
|
},
|
|
1287
|
+
fs: {
|
|
1288
|
+
exists: () => Promise.resolve(false),
|
|
1289
|
+
getPackageInfo: () => ({
|
|
1290
|
+
name: "test",
|
|
1291
|
+
version: "0.0.0"
|
|
1292
|
+
}),
|
|
1293
|
+
mkdir: () => Promise.resolve(),
|
|
1294
|
+
read: () => Promise.resolve(""),
|
|
1295
|
+
write: () => Promise.resolve()
|
|
1296
|
+
},
|
|
1297
|
+
logger,
|
|
780
1298
|
packageManager: {
|
|
781
|
-
name: "npm",
|
|
782
|
-
installCommand: "npm install",
|
|
783
1299
|
addCommand: "npm install",
|
|
784
|
-
|
|
785
|
-
|
|
1300
|
+
execCommand: "npx",
|
|
1301
|
+
installCommand: "npm install",
|
|
1302
|
+
name: "npm",
|
|
1303
|
+
runCommand: "npm run"
|
|
786
1304
|
},
|
|
1305
|
+
projectRoot: process.cwd(),
|
|
1306
|
+
telemetry,
|
|
787
1307
|
...overrides
|
|
788
1308
|
};
|
|
789
1309
|
}
|
|
790
1310
|
//#endregion
|
|
791
1311
|
//#region src/help.ts
|
|
1312
|
+
/**
|
|
1313
|
+
* Renders a help menu for commands and global flags.
|
|
1314
|
+
*
|
|
1315
|
+
* @param context - Context subset providing the logger used for output.
|
|
1316
|
+
* @param options - Application metadata for the help menu.
|
|
1317
|
+
* @param commands - Commands to list; commands with `hidden: true` are
|
|
1318
|
+
* omitted.
|
|
1319
|
+
* @param flags - Global flags to list.
|
|
1320
|
+
*/
|
|
792
1321
|
function showHelpMenu(context, options, commands, flags) {
|
|
793
1322
|
const commandRows = commands.filter((command) => !command.hidden).map((command) => ` ${command.name.padEnd(16)} ${command.description}`).join("\n");
|
|
794
1323
|
const flagRows = flags.map((flag) => {
|
|
@@ -800,17 +1329,23 @@ function showHelpMenu(context, options, commands, flags) {
|
|
|
800
1329
|
}
|
|
801
1330
|
//#endregion
|
|
802
1331
|
//#region src/intro.ts
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
resolve(data);
|
|
811
|
-
});
|
|
812
|
-
});
|
|
1332
|
+
const renderFigletAsync = promisify(figlet);
|
|
1333
|
+
async function renderFiglet(text) {
|
|
1334
|
+
try {
|
|
1335
|
+
return await renderFigletAsync(text) ?? text;
|
|
1336
|
+
} catch {
|
|
1337
|
+
return text;
|
|
1338
|
+
}
|
|
813
1339
|
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Renders a figlet banner and short intro note.
|
|
1342
|
+
*
|
|
1343
|
+
* @remarks
|
|
1344
|
+
* If figlet rendering fails, the plain banner text is displayed instead.
|
|
1345
|
+
*
|
|
1346
|
+
* @param context - Context subset providing the logger used for output.
|
|
1347
|
+
* @param options - Intro banner metadata.
|
|
1348
|
+
*/
|
|
814
1349
|
async function displayIntro(context, options) {
|
|
815
1350
|
const banner = await renderFiglet(options.figletText ?? options.appName);
|
|
816
1351
|
const versionLabel = options.version ? ` v${options.version}` : "";
|
|
@@ -819,49 +1354,104 @@ async function displayIntro(context, options) {
|
|
|
819
1354
|
}
|
|
820
1355
|
//#endregion
|
|
821
1356
|
//#region src/spinner.ts
|
|
1357
|
+
/**
|
|
1358
|
+
* Creates a Clack-backed spinner.
|
|
1359
|
+
*
|
|
1360
|
+
* @param initialMessage - Message used when `start()` is called without an
|
|
1361
|
+
* explicit message.
|
|
1362
|
+
* @returns A spinner controller.
|
|
1363
|
+
*/
|
|
822
1364
|
function createSpinner(initialMessage) {
|
|
823
|
-
const
|
|
1365
|
+
const spinnerInstance = spinner();
|
|
824
1366
|
return {
|
|
1367
|
+
message(message) {
|
|
1368
|
+
spinnerInstance.message(message);
|
|
1369
|
+
},
|
|
825
1370
|
start(message) {
|
|
826
|
-
|
|
1371
|
+
spinnerInstance.start(message ?? initialMessage ?? "Processing...");
|
|
827
1372
|
},
|
|
828
1373
|
stop(message) {
|
|
829
|
-
|
|
830
|
-
},
|
|
831
|
-
message(message) {
|
|
832
|
-
spinner.message(message);
|
|
1374
|
+
spinnerInstance.stop(message ?? "Done");
|
|
833
1375
|
}
|
|
834
1376
|
};
|
|
835
1377
|
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Runs an async task while displaying a spinner.
|
|
1380
|
+
*
|
|
1381
|
+
* @typeParam T - Value returned by the task.
|
|
1382
|
+
* @param message - Message shown when the spinner starts.
|
|
1383
|
+
* @param task - Async work to run.
|
|
1384
|
+
* @param options - Optional success and error messages shown when the task
|
|
1385
|
+
* settles.
|
|
1386
|
+
* @returns The value returned by `task`.
|
|
1387
|
+
*
|
|
1388
|
+
* @throws Re-throws any error from `task` after stopping the spinner.
|
|
1389
|
+
*/
|
|
836
1390
|
async function withSpinner(message, task, options) {
|
|
837
|
-
const
|
|
838
|
-
|
|
1391
|
+
const spinnerInstance = createSpinner(message);
|
|
1392
|
+
spinnerInstance.start();
|
|
839
1393
|
try {
|
|
840
1394
|
const result = await task();
|
|
841
|
-
|
|
1395
|
+
spinnerInstance.stop(options?.successMessage ?? "Done");
|
|
842
1396
|
return result;
|
|
843
1397
|
} catch (error) {
|
|
844
|
-
|
|
1398
|
+
spinnerInstance.stop(options?.errorMessage ?? "Failed");
|
|
845
1399
|
throw error;
|
|
846
1400
|
}
|
|
847
1401
|
}
|
|
848
1402
|
//#endregion
|
|
849
|
-
//#region src/version-check.ts
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1403
|
+
//#region src/version-check/compare.ts
|
|
1404
|
+
function parseVersionParts(version) {
|
|
1405
|
+
const [coreVersion = "", prerelease] = version.replace(/^[^\d]*/, "").split("-", 2);
|
|
1406
|
+
return {
|
|
1407
|
+
parts: coreVersion.split(".").map((part) => {
|
|
1408
|
+
const parsed = Number.parseInt(part.replace(/\D.*$/, ""), 10);
|
|
1409
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
1410
|
+
}),
|
|
1411
|
+
prerelease
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
function comparePrereleaseIdentifiers(left, right) {
|
|
1415
|
+
const leftIsNumeric = /^\d+$/.test(left);
|
|
1416
|
+
const rightIsNumeric = /^\d+$/.test(right);
|
|
1417
|
+
if (leftIsNumeric && rightIsNumeric) return Math.sign(Number.parseInt(left, 10) - Number.parseInt(right, 10));
|
|
1418
|
+
if (leftIsNumeric) return -1;
|
|
1419
|
+
if (rightIsNumeric) return 1;
|
|
1420
|
+
return Math.sign(left.localeCompare(right));
|
|
1421
|
+
}
|
|
1422
|
+
function comparePrerelease(left, right) {
|
|
1423
|
+
if (!left && !right) return 0;
|
|
1424
|
+
if (!left) return 1;
|
|
1425
|
+
if (!right) return -1;
|
|
1426
|
+
const leftParts = left.split(".");
|
|
1427
|
+
const rightParts = right.split(".");
|
|
1428
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
1429
|
+
for (let index = 0; index < length; index++) {
|
|
1430
|
+
const leftValue = leftParts[index];
|
|
1431
|
+
const rightValue = rightParts[index];
|
|
1432
|
+
if (leftValue === void 0) return -1;
|
|
1433
|
+
if (rightValue === void 0) return 1;
|
|
1434
|
+
const result = comparePrereleaseIdentifiers(leftValue, rightValue);
|
|
1435
|
+
if (result !== 0) return result;
|
|
860
1436
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1437
|
+
return 0;
|
|
1438
|
+
}
|
|
1439
|
+
function compareVersions(left, right) {
|
|
1440
|
+
const leftVersion = parseVersionParts(left);
|
|
1441
|
+
const rightVersion = parseVersionParts(right);
|
|
1442
|
+
const leftParts = leftVersion.parts;
|
|
1443
|
+
const rightParts = rightVersion.parts;
|
|
1444
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
1445
|
+
for (let index = 0; index < length; index++) {
|
|
1446
|
+
const leftValue = leftParts[index] ?? 0;
|
|
1447
|
+
const rightValue = rightParts[index] ?? 0;
|
|
1448
|
+
if (leftValue > rightValue) return 1;
|
|
1449
|
+
if (leftValue < rightValue) return -1;
|
|
1450
|
+
}
|
|
1451
|
+
return comparePrerelease(leftVersion.prerelease, rightVersion.prerelease);
|
|
864
1452
|
}
|
|
1453
|
+
//#endregion
|
|
1454
|
+
//#region src/version-check/install-source.ts
|
|
865
1455
|
function normalizePath(filePath) {
|
|
866
1456
|
return filePath.replaceAll("\\", "/");
|
|
867
1457
|
}
|
|
@@ -884,12 +1474,28 @@ function isPathUnder(candidate, parent) {
|
|
|
884
1474
|
function envValue(name) {
|
|
885
1475
|
return process.env[name];
|
|
886
1476
|
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Infers how the current CLI binary was installed.
|
|
1479
|
+
*
|
|
1480
|
+
* @remarks
|
|
1481
|
+
* Detection is heuristic and based on binary path, realpath, and package
|
|
1482
|
+
* manager environment variables. Unknown or transient install modes return
|
|
1483
|
+
* `unknown` instead of throwing.
|
|
1484
|
+
*
|
|
1485
|
+
* @param binPath - Binary path to inspect.
|
|
1486
|
+
* @returns The inferred installation source.
|
|
1487
|
+
*/
|
|
887
1488
|
function detectInstallSource(binPath = process.argv[1] ?? "") {
|
|
888
1489
|
const resolvedPath = normalizePath(safeRealpath(binPath || ""));
|
|
889
1490
|
const npmPrefix = envValue("npm_config_prefix");
|
|
1491
|
+
const execPath = normalizePath(process.execPath);
|
|
1492
|
+
const argvPath = normalizePath(process.argv[0] ?? "");
|
|
1493
|
+
const execName = path$1.basename(execPath);
|
|
1494
|
+
const argvName = path$1.basename(argvPath);
|
|
1495
|
+
const isBunInvocation = execName === "bun" || execName === "bunx" || argvName === "bun" || argvName === "bunx";
|
|
890
1496
|
if (resolvedPath.includes("/opt/homebrew/") || resolvedPath.includes("/usr/local/Cellar/") || resolvedPath.includes("/home/linuxbrew/") || resolvedPath.includes("/Homebrew/Cellar/")) return "brew";
|
|
891
1497
|
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";
|
|
1498
|
+
if (resolvedPath.includes("/.bun/install/cache/") || Boolean(envValue("BUN_INSTALL")) && (resolvedPath.includes("/.bun/install/run/") || isBunInvocation)) return "bunx";
|
|
893
1499
|
if (resolvedPath.includes("/.pnpm-store/") || resolvedPath.includes("/dlx-")) return "pnpm-dlx";
|
|
894
1500
|
if (resolvedPath.includes("/.yarn/berry/cache/") || resolvedPath.includes("/yarn/dlx-")) return "yarn-dlx";
|
|
895
1501
|
if (resolvedPath.includes("/node_modules/.bin/") || resolvedPath.endsWith("/node_modules/.bin")) return "local";
|
|
@@ -906,6 +1512,15 @@ function detectInstallSource(binPath = process.argv[1] ?? "") {
|
|
|
906
1512
|
})) return "npm-global";
|
|
907
1513
|
return "unknown";
|
|
908
1514
|
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Builds an update command for an installation source.
|
|
1517
|
+
*
|
|
1518
|
+
* @param source - Installation source returned by `detectInstallSource`.
|
|
1519
|
+
* @param packageName - Package name to update.
|
|
1520
|
+
* @param brewFormula - Homebrew formula name when `source` is `brew`.
|
|
1521
|
+
* @returns A command string when the source has an actionable update path,
|
|
1522
|
+
* otherwise `null`.
|
|
1523
|
+
*/
|
|
909
1524
|
function getUpdateCommand(source, packageName, brewFormula = packageName) {
|
|
910
1525
|
switch (source) {
|
|
911
1526
|
case "npm-global": return `npm install -g ${packageName}@latest`;
|
|
@@ -914,8 +1529,48 @@ function getUpdateCommand(source, packageName, brewFormula = packageName) {
|
|
|
914
1529
|
default: return null;
|
|
915
1530
|
}
|
|
916
1531
|
}
|
|
1532
|
+
//#endregion
|
|
1533
|
+
//#region src/version-check/registry.ts
|
|
1534
|
+
const DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
1535
|
+
const DEFAULT_TIMEOUT_MS = 1500;
|
|
1536
|
+
const DEFAULT_CACHE_TTL_MS = 1440 * 60 * 1e3;
|
|
1537
|
+
const MAX_CACHE_NAME_LENGTH = 80;
|
|
1538
|
+
const WINDOWS_RESERVED_NAMES = new Set([
|
|
1539
|
+
"con",
|
|
1540
|
+
"prn",
|
|
1541
|
+
"aux",
|
|
1542
|
+
"nul",
|
|
1543
|
+
"com1",
|
|
1544
|
+
"com2",
|
|
1545
|
+
"com3",
|
|
1546
|
+
"com4",
|
|
1547
|
+
"com5",
|
|
1548
|
+
"com6",
|
|
1549
|
+
"com7",
|
|
1550
|
+
"com8",
|
|
1551
|
+
"com9",
|
|
1552
|
+
"lpt1",
|
|
1553
|
+
"lpt2",
|
|
1554
|
+
"lpt3",
|
|
1555
|
+
"lpt4",
|
|
1556
|
+
"lpt5",
|
|
1557
|
+
"lpt6",
|
|
1558
|
+
"lpt7",
|
|
1559
|
+
"lpt8",
|
|
1560
|
+
"lpt9"
|
|
1561
|
+
]);
|
|
1562
|
+
function getCacheNameHash(packageName) {
|
|
1563
|
+
return createHash("sha256").update(packageName).digest("hex").slice(0, 8);
|
|
1564
|
+
}
|
|
917
1565
|
function sanitizeCacheName(packageName) {
|
|
918
|
-
|
|
1566
|
+
const normalized = packageName.normalize("NFKD").toLowerCase().replaceAll(/[^a-z0-9._-]+/g, "_").replaceAll(/_+/g, "_").replaceAll(/^[._-]+|[._-]+$/g, "");
|
|
1567
|
+
const fallbackName = normalized || "package";
|
|
1568
|
+
const baseName = fallbackName.split(".")[0] ?? fallbackName;
|
|
1569
|
+
const isReservedName = WINDOWS_RESERVED_NAMES.has(baseName);
|
|
1570
|
+
if (!(normalized !== packageName || normalized.length === 0 || isReservedName || normalized.length > MAX_CACHE_NAME_LENGTH)) return normalized;
|
|
1571
|
+
const hash = getCacheNameHash(packageName);
|
|
1572
|
+
const maxStemLength = MAX_CACHE_NAME_LENGTH - hash.length - 1;
|
|
1573
|
+
return `${(isReservedName ? `package-${fallbackName}` : fallbackName).slice(0, maxStemLength).replaceAll(/[._-]+$/g, "") || "package"}-${hash}`;
|
|
919
1574
|
}
|
|
920
1575
|
function getCacheDir(options) {
|
|
921
1576
|
return options.cacheDir ?? path$1.join(os.tmpdir(), "hexbus-version-cache");
|
|
@@ -928,8 +1583,8 @@ function readCachedVersion(options) {
|
|
|
928
1583
|
const content = fsSync$1.readFileSync(getCachePath(options), "utf-8");
|
|
929
1584
|
const parsed = JSON.parse(content);
|
|
930
1585
|
if (typeof parsed.version === "string" && typeof parsed.fetchedAt === "number") return {
|
|
931
|
-
|
|
932
|
-
|
|
1586
|
+
fetchedAt: parsed.fetchedAt,
|
|
1587
|
+
version: parsed.version
|
|
933
1588
|
};
|
|
934
1589
|
} catch {}
|
|
935
1590
|
return null;
|
|
@@ -942,52 +1597,19 @@ function isCacheFresh(cache, options) {
|
|
|
942
1597
|
async function writeCachedVersion(options, version) {
|
|
943
1598
|
const cachePath = getCachePath(options);
|
|
944
1599
|
const cacheDir = path$1.dirname(cachePath);
|
|
945
|
-
const tempPath = `${cachePath}.${process.pid}.tmp`;
|
|
1600
|
+
const tempPath = `${cachePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
946
1601
|
const payload = {
|
|
947
|
-
|
|
948
|
-
|
|
1602
|
+
fetchedAt: options.now?.() ?? Date.now(),
|
|
1603
|
+
version
|
|
949
1604
|
};
|
|
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;
|
|
1605
|
+
try {
|
|
1606
|
+
await fs$1.mkdir(cacheDir, { recursive: true });
|
|
1607
|
+
await fs$1.writeFile(tempPath, `${JSON.stringify(payload)}\n`, "utf-8");
|
|
1608
|
+
await fs$1.rename(tempPath, cachePath);
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
await fs$1.unlink(tempPath).catch(() => {});
|
|
1611
|
+
throw error;
|
|
969
1612
|
}
|
|
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
1613
|
}
|
|
992
1614
|
async function fetchLatestVersion(options) {
|
|
993
1615
|
const controller = new AbortController();
|
|
@@ -1010,11 +1632,24 @@ async function refreshCache(options) {
|
|
|
1010
1632
|
if (latestVersion) await writeCachedVersion(options, latestVersion);
|
|
1011
1633
|
return latestVersion;
|
|
1012
1634
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1635
|
+
//#endregion
|
|
1636
|
+
//#region src/version-check/check.ts
|
|
1637
|
+
/**
|
|
1638
|
+
* Checks whether raw arguments request version output.
|
|
1639
|
+
*
|
|
1640
|
+
* @param rawArgs - Arguments after executable and script path.
|
|
1641
|
+
* @returns `true` when `-v` or `--version` is present.
|
|
1642
|
+
*/
|
|
1643
|
+
function isVersionRequest(rawArgs) {
|
|
1644
|
+
return rawArgs.includes("-v") || rawArgs.includes("--version");
|
|
1017
1645
|
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Formats a user-facing update hint from an update-check result.
|
|
1648
|
+
*
|
|
1649
|
+
* @param result - Update-check result to render.
|
|
1650
|
+
* @returns A formatted hint when the result is outdated and has an update
|
|
1651
|
+
* command, otherwise `null`.
|
|
1652
|
+
*/
|
|
1018
1653
|
function formatUpdateHint(result) {
|
|
1019
1654
|
if (!result.isOutdated || result.updateCommand === null || result.latestVersion === null) return null;
|
|
1020
1655
|
if (result.source === "brew") return [
|
|
@@ -1028,23 +1663,95 @@ function formatUpdateHint(result) {
|
|
|
1028
1663
|
` ${color.cyan(result.updateCommand)}`
|
|
1029
1664
|
].join("\n");
|
|
1030
1665
|
}
|
|
1666
|
+
function createUpdateCheckResult(options, latestVersion) {
|
|
1667
|
+
const source = detectInstallSource(options.binPath);
|
|
1668
|
+
const updateCommand = getUpdateCommand(source, options.packageName, options.brewFormula);
|
|
1669
|
+
const isOutdated = typeof latestVersion === "string" && compareVersions(options.currentVersion, latestVersion) < 0;
|
|
1670
|
+
const result = {
|
|
1671
|
+
currentVersion: options.currentVersion,
|
|
1672
|
+
isOutdated,
|
|
1673
|
+
latestVersion,
|
|
1674
|
+
source,
|
|
1675
|
+
updateCommand
|
|
1676
|
+
};
|
|
1677
|
+
const hint = formatUpdateHint({
|
|
1678
|
+
...result,
|
|
1679
|
+
hint: null
|
|
1680
|
+
});
|
|
1681
|
+
return {
|
|
1682
|
+
...result,
|
|
1683
|
+
hint
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Checks whether a newer package version is available.
|
|
1688
|
+
*
|
|
1689
|
+
* @remarks
|
|
1690
|
+
* The function first reads the local cache. On a cache miss it refreshes the
|
|
1691
|
+
* cache from the configured registry. Refresh failures produce a result with
|
|
1692
|
+
* `latestVersion: null` rather than throwing.
|
|
1693
|
+
*
|
|
1694
|
+
* @param options - Update-check configuration.
|
|
1695
|
+
* @returns Update metadata and a formatted hint when an update is available.
|
|
1696
|
+
*/
|
|
1697
|
+
async function checkForUpdate(options) {
|
|
1698
|
+
const cached = readCachedVersion(options);
|
|
1699
|
+
if (cached) return createUpdateCheckResult(options, cached.version);
|
|
1700
|
+
try {
|
|
1701
|
+
return createUpdateCheckResult(options, await refreshCache(options));
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
options.logger?.debug?.(`Update check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1704
|
+
return createUpdateCheckResult(options, null);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
//#endregion
|
|
1708
|
+
//#region src/version-check/display.ts
|
|
1709
|
+
const defaultLogger = {
|
|
1710
|
+
message(message) {
|
|
1711
|
+
process.stdout.write(`${message}\n`);
|
|
1712
|
+
},
|
|
1713
|
+
note(content, title) {
|
|
1714
|
+
const prefix = title ? `${title}\n` : "";
|
|
1715
|
+
process.stdout.write(`${prefix}${content}\n`);
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
/**
|
|
1719
|
+
* Prints CLI version information and any available update hint.
|
|
1720
|
+
*
|
|
1721
|
+
* @param options - Version display and update-check options.
|
|
1722
|
+
*/
|
|
1031
1723
|
async function printVersionInfo(options) {
|
|
1032
1724
|
const logger = options.logger ?? defaultLogger;
|
|
1033
1725
|
logger.message(`${options.appName} v${options.currentVersion}`);
|
|
1034
1726
|
const result = await checkForUpdate(options);
|
|
1035
1727
|
if (result.hint) logger.note(result.hint, "Update available");
|
|
1036
1728
|
}
|
|
1729
|
+
async function refreshCacheInBackground(options, logger) {
|
|
1730
|
+
try {
|
|
1731
|
+
await refreshCache(options);
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
logger.debug?.(`Update check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Starts a non-blocking update check.
|
|
1738
|
+
*
|
|
1739
|
+
* @remarks
|
|
1740
|
+
* Cached hints are displayed synchronously when available. If the cache is
|
|
1741
|
+
* stale or missing, a refresh is started in the background and failures are
|
|
1742
|
+
* only logged at debug level.
|
|
1743
|
+
*
|
|
1744
|
+
* @param options - Version display and update-check options.
|
|
1745
|
+
*/
|
|
1037
1746
|
function startBackgroundUpdateCheck(options) {
|
|
1038
1747
|
const logger = options.logger ?? defaultLogger;
|
|
1039
1748
|
const cached = readCachedVersion(options);
|
|
1040
1749
|
if (cached) {
|
|
1041
|
-
const result =
|
|
1750
|
+
const result = createUpdateCheckResult(options, cached.version);
|
|
1042
1751
|
if (result.hint) logger.note(result.hint, "Update available");
|
|
1043
1752
|
}
|
|
1044
1753
|
if (cached && isCacheFresh(cached, options)) return;
|
|
1045
|
-
|
|
1046
|
-
logger.debug?.(`Update check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1047
|
-
});
|
|
1754
|
+
refreshCacheInBackground(options, logger);
|
|
1048
1755
|
}
|
|
1049
1756
|
//#endregion
|
|
1050
1757
|
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 };
|