hexbus 0.1.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 +17 -0
- package/dist/index.d.mts +312 -0
- package/dist/index.mjs +1050 -0
- package/package.json +48 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
import * as fsSync$1 from "node:fs";
|
|
2
|
+
import fsSync from "node:fs";
|
|
3
|
+
import * as fs$1 from "node:fs/promises";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import * as path$1 from "node:path";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
import { loadConfig } from "c12";
|
|
9
|
+
import color from "picocolors";
|
|
10
|
+
import figlet from "figlet";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
//#region src/detection.ts
|
|
13
|
+
const LOCK_FILE_MAP = {
|
|
14
|
+
"bun.lockb": "bun",
|
|
15
|
+
"bun.lock": "bun",
|
|
16
|
+
"pnpm-lock.yaml": "pnpm",
|
|
17
|
+
"yarn.lock": "yarn",
|
|
18
|
+
"package-lock.json": "npm"
|
|
19
|
+
};
|
|
20
|
+
const PACKAGE_MANAGER_CONFIG = {
|
|
21
|
+
bun: {
|
|
22
|
+
installCommand: "bun install",
|
|
23
|
+
addCommand: "bun add",
|
|
24
|
+
runCommand: "bun run",
|
|
25
|
+
execCommand: "bunx"
|
|
26
|
+
},
|
|
27
|
+
pnpm: {
|
|
28
|
+
installCommand: "pnpm install",
|
|
29
|
+
addCommand: "pnpm add",
|
|
30
|
+
runCommand: "pnpm",
|
|
31
|
+
execCommand: "pnpm dlx"
|
|
32
|
+
},
|
|
33
|
+
yarn: {
|
|
34
|
+
installCommand: "yarn",
|
|
35
|
+
addCommand: "yarn add",
|
|
36
|
+
runCommand: "yarn",
|
|
37
|
+
execCommand: "yarn dlx"
|
|
38
|
+
},
|
|
39
|
+
npm: {
|
|
40
|
+
installCommand: "npm install",
|
|
41
|
+
addCommand: "npm install",
|
|
42
|
+
runCommand: "npm run",
|
|
43
|
+
execCommand: "npx"
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
async function readPackageJson(projectRoot) {
|
|
47
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
48
|
+
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
49
|
+
return JSON.parse(content);
|
|
50
|
+
}
|
|
51
|
+
async function detectFramework(projectRoot, logger, packageMap = {}) {
|
|
52
|
+
try {
|
|
53
|
+
logger?.debug(`Detecting framework in ${projectRoot}`);
|
|
54
|
+
const packageJson = await readPackageJson(projectRoot);
|
|
55
|
+
const deps = {
|
|
56
|
+
...packageJson.dependencies,
|
|
57
|
+
...packageJson.devDependencies
|
|
58
|
+
};
|
|
59
|
+
const hasReact = "react" in deps;
|
|
60
|
+
const reactVersion = hasReact ? deps.react : null;
|
|
61
|
+
const tailwindVersion = deps.tailwindcss ?? null;
|
|
62
|
+
let framework = null;
|
|
63
|
+
let frameworkVersion = null;
|
|
64
|
+
let pkg = hasReact ? packageMap.react ?? null : null;
|
|
65
|
+
if ("next" in deps) {
|
|
66
|
+
framework = "Next.js";
|
|
67
|
+
frameworkVersion = deps.next ?? null;
|
|
68
|
+
pkg = packageMap.next ?? packageMap.react ?? null;
|
|
69
|
+
} else if ("@remix-run/react" in deps) {
|
|
70
|
+
framework = "Remix";
|
|
71
|
+
frameworkVersion = deps["@remix-run/react"] ?? null;
|
|
72
|
+
pkg = packageMap.react ?? null;
|
|
73
|
+
} else if ("@vitejs/plugin-react" in deps || "@vitejs/plugin-react-swc" in deps) {
|
|
74
|
+
framework = "Vite + React";
|
|
75
|
+
frameworkVersion = deps["@vitejs/plugin-react"] ?? deps["@vitejs/plugin-react-swc"] ?? null;
|
|
76
|
+
pkg = packageMap.react ?? null;
|
|
77
|
+
} else if ("gatsby" in deps) {
|
|
78
|
+
framework = "Gatsby";
|
|
79
|
+
frameworkVersion = deps.gatsby ?? null;
|
|
80
|
+
pkg = packageMap.react ?? null;
|
|
81
|
+
} else if (hasReact) {
|
|
82
|
+
framework = "React";
|
|
83
|
+
frameworkVersion = reactVersion ?? null;
|
|
84
|
+
pkg = packageMap.react ?? null;
|
|
85
|
+
} else pkg = packageMap.core ?? null;
|
|
86
|
+
return {
|
|
87
|
+
framework,
|
|
88
|
+
frameworkVersion,
|
|
89
|
+
pkg,
|
|
90
|
+
hasReact,
|
|
91
|
+
reactVersion: reactVersion ?? null,
|
|
92
|
+
tailwindVersion
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
logger?.debug(`Framework detection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
96
|
+
return {
|
|
97
|
+
framework: null,
|
|
98
|
+
frameworkVersion: null,
|
|
99
|
+
pkg: packageMap.core ?? null,
|
|
100
|
+
hasReact: false,
|
|
101
|
+
reactVersion: null,
|
|
102
|
+
tailwindVersion: null
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function detectProjectRoot(cwd, logger) {
|
|
107
|
+
let projectRoot = cwd;
|
|
108
|
+
let previousDirectory = "";
|
|
109
|
+
let depth = 0;
|
|
110
|
+
const maxDepth = 10;
|
|
111
|
+
while (projectRoot !== previousDirectory && depth < maxDepth) try {
|
|
112
|
+
await fs.access(path.join(projectRoot, "package.json"));
|
|
113
|
+
return projectRoot;
|
|
114
|
+
} catch {
|
|
115
|
+
previousDirectory = projectRoot;
|
|
116
|
+
projectRoot = path.dirname(projectRoot);
|
|
117
|
+
depth++;
|
|
118
|
+
}
|
|
119
|
+
logger?.warn("Could not find project root; using current working directory");
|
|
120
|
+
return cwd;
|
|
121
|
+
}
|
|
122
|
+
async function detectFromLockFile(projectRoot, logger) {
|
|
123
|
+
for (const [lockFile, pm] of Object.entries(LOCK_FILE_MAP)) try {
|
|
124
|
+
await fs.access(path.join(projectRoot, lockFile));
|
|
125
|
+
logger?.debug(`Found ${lockFile}, using ${pm}`);
|
|
126
|
+
return pm;
|
|
127
|
+
} catch {}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
async function detectFromPackageJson(projectRoot, logger) {
|
|
131
|
+
try {
|
|
132
|
+
const match = (await readPackageJson(projectRoot)).packageManager?.match(/^(npm|yarn|pnpm|bun)@/);
|
|
133
|
+
if (match) {
|
|
134
|
+
logger?.debug(`Found packageManager field: ${match[1]}`);
|
|
135
|
+
return match[1];
|
|
136
|
+
}
|
|
137
|
+
} catch {}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
async function promptForPackageManager(logger) {
|
|
141
|
+
logger?.debug("Prompting user to select package manager");
|
|
142
|
+
const result = await p.select({
|
|
143
|
+
message: "Which package manager do you use?",
|
|
144
|
+
options: [
|
|
145
|
+
{
|
|
146
|
+
value: "bun",
|
|
147
|
+
label: "bun",
|
|
148
|
+
hint: "Fast all-in-one toolkit"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
value: "pnpm",
|
|
152
|
+
label: "pnpm",
|
|
153
|
+
hint: "Fast, disk space efficient"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
value: "yarn",
|
|
157
|
+
label: "yarn",
|
|
158
|
+
hint: "Classic package manager"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
value: "npm",
|
|
162
|
+
label: "npm",
|
|
163
|
+
hint: "Default Node.js package manager"
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
});
|
|
167
|
+
if (p.isCancel(result)) throw new Error("Package manager selection cancelled");
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
async function detectPackageManager(projectRoot, logger, options) {
|
|
171
|
+
let pm = await detectFromLockFile(projectRoot, logger);
|
|
172
|
+
if (!pm) pm = await detectFromPackageJson(projectRoot, logger);
|
|
173
|
+
if (!pm) if (options?.interactive === true && process.stdin.isTTY && !process.env.CI) pm = await promptForPackageManager(logger);
|
|
174
|
+
else {
|
|
175
|
+
pm = "npm";
|
|
176
|
+
logger?.debug("Defaulting to npm");
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
name: pm,
|
|
180
|
+
...PACKAGE_MANAGER_CONFIG[pm]
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function getInstallCommand(pm, packages, options) {
|
|
184
|
+
const pkgList = packages.join(" ");
|
|
185
|
+
const devFlag = options?.dev ? pm.name === "npm" ? "--save-dev" : "-D" : "";
|
|
186
|
+
return `${pm.addCommand} ${devFlag} ${pkgList}`.trim().replace(/\s+/g, " ");
|
|
187
|
+
}
|
|
188
|
+
function getRunCommand(pm, script) {
|
|
189
|
+
return `${pm.runCommand} ${script}`;
|
|
190
|
+
}
|
|
191
|
+
function getExecCommand(pm, binary, args) {
|
|
192
|
+
const argString = args?.join(" ") || "";
|
|
193
|
+
return `${pm.execCommand} ${binary} ${argString}`.trim();
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/errors.ts
|
|
197
|
+
const DEFAULT_ERROR_CATALOG = {
|
|
198
|
+
CONFIG_NOT_FOUND: {
|
|
199
|
+
code: "CONFIG_NOT_FOUND",
|
|
200
|
+
message: "Configuration not found",
|
|
201
|
+
hint: "Run the setup command to create a configuration"
|
|
202
|
+
},
|
|
203
|
+
FLAG_VALUE_REQUIRED: {
|
|
204
|
+
code: "FLAG_VALUE_REQUIRED",
|
|
205
|
+
message: "Flag requires a value"
|
|
206
|
+
},
|
|
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
|
+
UNKNOWN_ERROR: {
|
|
217
|
+
code: "UNKNOWN_ERROR",
|
|
218
|
+
message: "An unexpected error occurred"
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
let activeCatalog = { ...DEFAULT_ERROR_CATALOG };
|
|
222
|
+
function extendErrorCatalog(entries) {
|
|
223
|
+
activeCatalog = {
|
|
224
|
+
...activeCatalog,
|
|
225
|
+
...entries
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
var CliError = class CliError extends Error {
|
|
229
|
+
code;
|
|
230
|
+
context;
|
|
231
|
+
entry;
|
|
232
|
+
constructor(code, context) {
|
|
233
|
+
const entry = activeCatalog[code] ?? activeCatalog.UNKNOWN_ERROR ?? DEFAULT_ERROR_CATALOG.UNKNOWN_ERROR;
|
|
234
|
+
super(entry.message);
|
|
235
|
+
this.name = "CliError";
|
|
236
|
+
this.code = code;
|
|
237
|
+
this.context = context;
|
|
238
|
+
this.entry = entry;
|
|
239
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, CliError);
|
|
240
|
+
}
|
|
241
|
+
display(logger) {
|
|
242
|
+
let message = this.entry.message;
|
|
243
|
+
if (this.context?.details) message += `: ${this.context.details}`;
|
|
244
|
+
logger.error(message);
|
|
245
|
+
if (this.entry.hint) logger.info(`Hint: ${this.entry.hint}`);
|
|
246
|
+
if (this.entry.docs) logger.info(`Docs: ${this.entry.docs}`);
|
|
247
|
+
}
|
|
248
|
+
static from(error, fallbackCode = "UNKNOWN_ERROR") {
|
|
249
|
+
if (error instanceof CliError) return error;
|
|
250
|
+
return new CliError(fallbackCode, {
|
|
251
|
+
details: error instanceof Error ? error.message : String(error),
|
|
252
|
+
originalError: error
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
function isCliError(error, code) {
|
|
257
|
+
if (!(error instanceof CliError)) return false;
|
|
258
|
+
if (code) return error.code === code;
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
function createErrorHandlers(logger, telemetry) {
|
|
262
|
+
return {
|
|
263
|
+
handleError(error, command) {
|
|
264
|
+
const cliError = CliError.from(error);
|
|
265
|
+
try {
|
|
266
|
+
telemetry?.trackError(cliError, command);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
269
|
+
logger.warn(`Failed to track error telemetry: ${message}`);
|
|
270
|
+
}
|
|
271
|
+
cliError.display(logger);
|
|
272
|
+
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
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function withErrorHandling(fn, logger, context) {
|
|
282
|
+
return (async (...args) => {
|
|
283
|
+
try {
|
|
284
|
+
return await fn(...args);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
CliError.from(error).display(logger);
|
|
287
|
+
if (context?.command) logger.info(`Command: ${context.command}`);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/logger.ts
|
|
294
|
+
const LOG_LEVELS = [
|
|
295
|
+
"error",
|
|
296
|
+
"warn",
|
|
297
|
+
"info",
|
|
298
|
+
"debug"
|
|
299
|
+
];
|
|
300
|
+
const validLogLevels = LOG_LEVELS;
|
|
301
|
+
const LOG_LEVEL_PRIORITY = {
|
|
302
|
+
error: 0,
|
|
303
|
+
warn: 1,
|
|
304
|
+
info: 2,
|
|
305
|
+
debug: 3
|
|
306
|
+
};
|
|
307
|
+
function safeStringify(arg) {
|
|
308
|
+
try {
|
|
309
|
+
return JSON.stringify(arg, null, 2);
|
|
310
|
+
} catch {
|
|
311
|
+
return String(arg);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function formatArgs(args) {
|
|
315
|
+
if (args.length === 0) return "";
|
|
316
|
+
return `\n${args.map((arg) => ` - ${safeStringify(arg)}`).join("\n")}`;
|
|
317
|
+
}
|
|
318
|
+
function formatLogMessage(logLevel, message, args = []) {
|
|
319
|
+
const messageStr = typeof message === "string" ? message : String(message);
|
|
320
|
+
const formattedArgs = formatArgs(args);
|
|
321
|
+
switch (logLevel) {
|
|
322
|
+
case "error": return `${color.bgRed(color.black(" error "))} ${messageStr}${formattedArgs}`;
|
|
323
|
+
case "warn": return `${color.bgYellow(color.black(" warning "))} ${messageStr}${formattedArgs}`;
|
|
324
|
+
case "info": return `${color.bgGreen(color.black(" info "))} ${messageStr}${formattedArgs}`;
|
|
325
|
+
case "debug": return `${color.bgBlack(color.white(" debug "))} ${messageStr}${formattedArgs}`;
|
|
326
|
+
case "success": return `${color.bgGreen(color.white(" success "))} ${messageStr}${formattedArgs}`;
|
|
327
|
+
case "failed": return `${color.bgRed(color.white(" failed "))} ${messageStr}${formattedArgs}`;
|
|
328
|
+
default: return `[${logLevel.toUpperCase()}] ${messageStr}${formattedArgs}`;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function logMessage(logLevel, message, ...args) {
|
|
332
|
+
const formattedMessage = formatLogMessage(logLevel, message, args);
|
|
333
|
+
switch (logLevel) {
|
|
334
|
+
case "error":
|
|
335
|
+
p.log.error(formattedMessage);
|
|
336
|
+
break;
|
|
337
|
+
case "warn":
|
|
338
|
+
p.log.warn(formattedMessage);
|
|
339
|
+
break;
|
|
340
|
+
case "info":
|
|
341
|
+
case "debug":
|
|
342
|
+
p.log.info(formattedMessage);
|
|
343
|
+
break;
|
|
344
|
+
case "success":
|
|
345
|
+
case "failed":
|
|
346
|
+
p.outro(formattedMessage);
|
|
347
|
+
break;
|
|
348
|
+
default: p.log.message(formattedMessage);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function formatStep(current, total, label) {
|
|
352
|
+
const safeTotal = Math.max(0, total);
|
|
353
|
+
const safeCurrent = Math.min(Math.max(0, current), safeTotal);
|
|
354
|
+
return `[${color.green("█".repeat(safeCurrent))}${color.dim("░".repeat(safeTotal - safeCurrent))}] Step ${safeCurrent}/${safeTotal}: ${label}`;
|
|
355
|
+
}
|
|
356
|
+
function createCliLogger(level = "info") {
|
|
357
|
+
const currentLevelPriority = LOG_LEVEL_PRIORITY[level];
|
|
358
|
+
const shouldLog = (targetLevel) => LOG_LEVEL_PRIORITY[targetLevel] <= currentLevelPriority;
|
|
359
|
+
return {
|
|
360
|
+
debug(message, ...args) {
|
|
361
|
+
if (shouldLog("debug")) logMessage("debug", message, ...args);
|
|
362
|
+
},
|
|
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
|
+
error(message, ...args) {
|
|
370
|
+
if (shouldLog("error")) logMessage("error", message, ...args);
|
|
371
|
+
},
|
|
372
|
+
message(message) {
|
|
373
|
+
p.log.message(message);
|
|
374
|
+
},
|
|
375
|
+
note(content, title) {
|
|
376
|
+
p.note(content, title, { format: (line) => line });
|
|
377
|
+
},
|
|
378
|
+
success(message) {
|
|
379
|
+
logMessage("success", message);
|
|
380
|
+
},
|
|
381
|
+
failed(message, exitCode = 1) {
|
|
382
|
+
logMessage("failed", message);
|
|
383
|
+
process.exit(exitCode);
|
|
384
|
+
},
|
|
385
|
+
outro(message) {
|
|
386
|
+
p.outro(message);
|
|
387
|
+
},
|
|
388
|
+
step(current, total, label) {
|
|
389
|
+
p.log.step(formatStep(current, total, label));
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/parser.ts
|
|
395
|
+
const globalFlags = [
|
|
396
|
+
{
|
|
397
|
+
names: ["--help", "-h"],
|
|
398
|
+
description: "Show this help message",
|
|
399
|
+
type: "special",
|
|
400
|
+
expectsValue: false
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
names: ["--version", "-v"],
|
|
404
|
+
description: "Show the CLI version",
|
|
405
|
+
type: "special",
|
|
406
|
+
expectsValue: false
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
names: ["--logger"],
|
|
410
|
+
description: "Set log level (error, warn, info, debug)",
|
|
411
|
+
type: "string",
|
|
412
|
+
expectsValue: true,
|
|
413
|
+
defaultValue: "info"
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
names: ["--config"],
|
|
417
|
+
description: "Specify path to configuration file",
|
|
418
|
+
type: "string",
|
|
419
|
+
expectsValue: true
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
names: ["-y", "--yes"],
|
|
423
|
+
description: "Skip confirmation prompts",
|
|
424
|
+
type: "boolean",
|
|
425
|
+
expectsValue: false,
|
|
426
|
+
defaultValue: false
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
names: ["--no-telemetry"],
|
|
430
|
+
description: "Disable telemetry data collection",
|
|
431
|
+
type: "boolean",
|
|
432
|
+
expectsValue: false,
|
|
433
|
+
defaultValue: false
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
names: ["--telemetry-debug"],
|
|
437
|
+
description: "Enable debug mode for telemetry",
|
|
438
|
+
type: "boolean",
|
|
439
|
+
expectsValue: false,
|
|
440
|
+
defaultValue: false
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
names: ["--force"],
|
|
444
|
+
description: "Force operation even if files exist",
|
|
445
|
+
type: "boolean",
|
|
446
|
+
expectsValue: false,
|
|
447
|
+
defaultValue: false
|
|
448
|
+
}
|
|
449
|
+
];
|
|
450
|
+
function getPrimaryFlagName(flag) {
|
|
451
|
+
const longName = flag.names.find((name) => name.startsWith("--"));
|
|
452
|
+
const fallback = flag.names.reduce((longest, name) => name.length > longest.length ? name : longest, "");
|
|
453
|
+
return (longName ?? fallback).replace(/^--?/, "");
|
|
454
|
+
}
|
|
455
|
+
function parseCliArgs(rawArgs, commands) {
|
|
456
|
+
const parsedFlags = {};
|
|
457
|
+
const potentialCommandArgs = [];
|
|
458
|
+
let commandName;
|
|
459
|
+
const commandArgs = [];
|
|
460
|
+
for (const flag of globalFlags) {
|
|
461
|
+
const primaryName = getPrimaryFlagName(flag);
|
|
462
|
+
if (!primaryName) continue;
|
|
463
|
+
if (flag.type === "boolean") parsedFlags[primaryName] = flag.defaultValue ?? false;
|
|
464
|
+
else parsedFlags[primaryName] = flag.defaultValue;
|
|
465
|
+
}
|
|
466
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
467
|
+
const arg = rawArgs[i];
|
|
468
|
+
if (typeof arg !== "string") continue;
|
|
469
|
+
let isFlag = false;
|
|
470
|
+
for (const flag of globalFlags) {
|
|
471
|
+
if (!flag.names.includes(arg)) continue;
|
|
472
|
+
const primaryName = getPrimaryFlagName(flag);
|
|
473
|
+
if (!primaryName) continue;
|
|
474
|
+
isFlag = true;
|
|
475
|
+
if (flag.type === "boolean") parsedFlags[primaryName] = true;
|
|
476
|
+
else if (flag.expectsValue) {
|
|
477
|
+
const nextArg = rawArgs[i + 1];
|
|
478
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
479
|
+
parsedFlags[primaryName] = nextArg;
|
|
480
|
+
i++;
|
|
481
|
+
} else p.log.warn(formatLogMessage("warn", `Flag ${arg} expects a value, but none was provided`));
|
|
482
|
+
} else parsedFlags[primaryName] = true;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
if (!isFlag) potentialCommandArgs.push(arg);
|
|
486
|
+
}
|
|
487
|
+
const firstPositional = potentialCommandArgs[0];
|
|
488
|
+
if (typeof firstPositional === "string" && commands.some((cmd) => cmd.name === firstPositional)) {
|
|
489
|
+
commandName = firstPositional;
|
|
490
|
+
commandArgs.push(...potentialCommandArgs.slice(1));
|
|
491
|
+
} else commandArgs.push(...potentialCommandArgs);
|
|
492
|
+
return {
|
|
493
|
+
commandName,
|
|
494
|
+
commandArgs,
|
|
495
|
+
parsedFlags
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function formatFlagHelp(flag) {
|
|
499
|
+
return ` ${flag.names.join(", ")}${flag.expectsValue ? " <value>" : ""}\t${flag.description}`;
|
|
500
|
+
}
|
|
501
|
+
function generateFlagsHelp() {
|
|
502
|
+
return globalFlags.map(formatFlagHelp).join("\n");
|
|
503
|
+
}
|
|
504
|
+
function hasFlag(flags, name) {
|
|
505
|
+
return flags[name] === true;
|
|
506
|
+
}
|
|
507
|
+
function getFlagValue(flags, name) {
|
|
508
|
+
const value = flags[name];
|
|
509
|
+
if (typeof value === "string") return value;
|
|
510
|
+
}
|
|
511
|
+
function parseSubcommand(args, subcommands) {
|
|
512
|
+
const subcommandName = args[0];
|
|
513
|
+
const subcommand = subcommands.find((cmd) => cmd.name === subcommandName);
|
|
514
|
+
if (subcommand) return {
|
|
515
|
+
subcommand,
|
|
516
|
+
remainingArgs: args.slice(1)
|
|
517
|
+
};
|
|
518
|
+
return {
|
|
519
|
+
subcommand: void 0,
|
|
520
|
+
remainingArgs: args
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
//#endregion
|
|
524
|
+
//#region src/telemetry.ts
|
|
525
|
+
const TelemetryEventName = {
|
|
526
|
+
CLI_INVOKED: "cli_invoked",
|
|
527
|
+
CLI_ENVIRONMENT_DETECTED: "cli_environment_detected",
|
|
528
|
+
CLI_COMPLETED: "cli_completed",
|
|
529
|
+
COMMAND_INVOKED: "command_invoked",
|
|
530
|
+
COMMAND_SUCCEEDED: "command_succeeded",
|
|
531
|
+
COMMAND_FAILED: "command_failed",
|
|
532
|
+
COMMAND_UNKNOWN: "command_unknown",
|
|
533
|
+
ERROR_OCCURRED: "error_occurred",
|
|
534
|
+
HELP_DISPLAYED: "help_displayed",
|
|
535
|
+
VERSION_DISPLAYED: "version_displayed",
|
|
536
|
+
INTERACTIVE_MENU_OPENED: "interactive_menu_opened",
|
|
537
|
+
INTERACTIVE_MENU_EXITED: "interactive_menu_exited"
|
|
538
|
+
};
|
|
539
|
+
function isEnvDisabled(prefix) {
|
|
540
|
+
const value = process.env[`${prefix}_TELEMETRY_DISABLED`];
|
|
541
|
+
return value === "1" || value === "true";
|
|
542
|
+
}
|
|
543
|
+
function createDisabledTelemetry() {
|
|
544
|
+
return {
|
|
545
|
+
trackEvent: () => {},
|
|
546
|
+
trackCommand: () => {},
|
|
547
|
+
trackError: () => {},
|
|
548
|
+
flush: async () => {},
|
|
549
|
+
shutdown: async () => {},
|
|
550
|
+
isDisabled: () => true
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function createTelemetry(options = {}) {
|
|
554
|
+
const envVarPrefix = options.envVarPrefix ?? "APP";
|
|
555
|
+
const disabled = options.disabled === true || isEnvDisabled(envVarPrefix);
|
|
556
|
+
const events = [];
|
|
557
|
+
if (disabled) return createDisabledTelemetry();
|
|
558
|
+
const trackEvent = (eventName, properties = {}) => {
|
|
559
|
+
const payload = {
|
|
560
|
+
name: eventName,
|
|
561
|
+
properties: {
|
|
562
|
+
appName: options.appName ?? "cli",
|
|
563
|
+
...options.defaultProperties,
|
|
564
|
+
...properties,
|
|
565
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
events.push(payload);
|
|
569
|
+
if (options.debug) options.logger?.debug("Telemetry event queued", payload);
|
|
570
|
+
};
|
|
571
|
+
const flush = async () => {
|
|
572
|
+
if (!options.endpoint || events.length === 0) {
|
|
573
|
+
events.length = 0;
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const batch = events.splice(0, events.length);
|
|
577
|
+
try {
|
|
578
|
+
await fetch(options.endpoint, {
|
|
579
|
+
method: "POST",
|
|
580
|
+
headers: { "content-type": "application/json" },
|
|
581
|
+
body: JSON.stringify({ events: batch }),
|
|
582
|
+
keepalive: true
|
|
583
|
+
});
|
|
584
|
+
} catch (error) {
|
|
585
|
+
options.logger?.warn(`Failed to send telemetry: ${error instanceof Error ? error.message : String(error)}`);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
return {
|
|
589
|
+
trackEvent,
|
|
590
|
+
trackCommand(command, args = [], flags = {}) {
|
|
591
|
+
trackEvent(TelemetryEventName.COMMAND_INVOKED, {
|
|
592
|
+
command,
|
|
593
|
+
argsCount: args.length,
|
|
594
|
+
enabledFlags: Object.entries(flags).filter(([, value]) => value !== false && value !== void 0).map(([key]) => key).sort()
|
|
595
|
+
});
|
|
596
|
+
},
|
|
597
|
+
trackError(error, command) {
|
|
598
|
+
trackEvent(TelemetryEventName.ERROR_OCCURRED, {
|
|
599
|
+
command,
|
|
600
|
+
errorName: error.name,
|
|
601
|
+
errorMessage: error.message
|
|
602
|
+
});
|
|
603
|
+
},
|
|
604
|
+
flush,
|
|
605
|
+
async shutdown() {
|
|
606
|
+
await flush();
|
|
607
|
+
},
|
|
608
|
+
isDisabled() {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/context.ts
|
|
615
|
+
function getLogLevel(parsedFlags) {
|
|
616
|
+
const levelArg = parsedFlags.logger;
|
|
617
|
+
if (typeof levelArg === "string") {
|
|
618
|
+
if (validLogLevels.includes(levelArg)) return levelArg;
|
|
619
|
+
process.stderr.write(`[CLI Setup] Invalid log level '${levelArg}' provided via --logger. Using default 'info'.\n`);
|
|
620
|
+
}
|
|
621
|
+
return "info";
|
|
622
|
+
}
|
|
623
|
+
function createFileSystem(cwd) {
|
|
624
|
+
return {
|
|
625
|
+
getPackageInfo() {
|
|
626
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
627
|
+
try {
|
|
628
|
+
const content = fsSync.readFileSync(packageJsonPath, "utf-8");
|
|
629
|
+
const packageInfo = JSON.parse(content);
|
|
630
|
+
return {
|
|
631
|
+
...packageInfo,
|
|
632
|
+
name: packageInfo.name || "unknown",
|
|
633
|
+
version: packageInfo.version || "unknown"
|
|
634
|
+
};
|
|
635
|
+
} catch {
|
|
636
|
+
return {
|
|
637
|
+
name: "unknown",
|
|
638
|
+
version: "unknown"
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
async exists(filePath) {
|
|
643
|
+
try {
|
|
644
|
+
await fs.access(filePath);
|
|
645
|
+
return true;
|
|
646
|
+
} catch {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
read(filePath) {
|
|
651
|
+
return fs.readFile(filePath, "utf-8");
|
|
652
|
+
},
|
|
653
|
+
write(filePath, content) {
|
|
654
|
+
return fs.writeFile(filePath, content, "utf-8");
|
|
655
|
+
},
|
|
656
|
+
async mkdir(dirPath) {
|
|
657
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
async function createCliContext(options) {
|
|
662
|
+
const cwd = options.cwd ?? process.cwd();
|
|
663
|
+
const appName = options.appName ?? "cli";
|
|
664
|
+
const { commandName, commandArgs, parsedFlags } = parseCliArgs(options.rawArgs, options.commands);
|
|
665
|
+
const logger = createCliLogger(getLogLevel(parsedFlags));
|
|
666
|
+
const projectRoot = await detectProjectRoot(cwd, logger);
|
|
667
|
+
const fsUtils = createFileSystem(projectRoot);
|
|
668
|
+
const framework = await detectFramework(projectRoot, logger, options.packageMap);
|
|
669
|
+
const packageManager = await detectPackageManager(projectRoot, logger, { interactive: options.interactivePackageManagerDetection });
|
|
670
|
+
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
|
+
appName,
|
|
675
|
+
envVarPrefix: options.telemetry?.envVarPrefix ?? appName.toUpperCase(),
|
|
676
|
+
defaultProperties: {
|
|
677
|
+
entryCommand: commandName ?? "interactive",
|
|
678
|
+
commandArgsCount: commandArgs.length,
|
|
679
|
+
cliVersion: fsUtils.getPackageInfo().version,
|
|
680
|
+
framework: framework.framework ?? "unknown",
|
|
681
|
+
frameworkVersion: framework.frameworkVersion ?? "unknown",
|
|
682
|
+
packageManager: packageManager.name,
|
|
683
|
+
...options.telemetry?.defaultProperties
|
|
684
|
+
},
|
|
685
|
+
logger
|
|
686
|
+
});
|
|
687
|
+
const errorHandlers = createErrorHandlers(logger, telemetry);
|
|
688
|
+
const context = {
|
|
689
|
+
logger,
|
|
690
|
+
flags: parsedFlags,
|
|
691
|
+
commandName,
|
|
692
|
+
commandArgs,
|
|
693
|
+
cwd,
|
|
694
|
+
error: errorHandlers,
|
|
695
|
+
config: {
|
|
696
|
+
async loadConfig() {
|
|
697
|
+
const configPath = typeof parsedFlags.config === "string" ? parsedFlags.config : void 0;
|
|
698
|
+
const { config } = await loadConfig({
|
|
699
|
+
name: options.configName ?? appName,
|
|
700
|
+
cwd: projectRoot,
|
|
701
|
+
configFile: configPath
|
|
702
|
+
});
|
|
703
|
+
return config ?? null;
|
|
704
|
+
},
|
|
705
|
+
async requireConfig() {
|
|
706
|
+
const config = await this.loadConfig();
|
|
707
|
+
if (!config) throw new CliError("CONFIG_NOT_FOUND");
|
|
708
|
+
return config;
|
|
709
|
+
},
|
|
710
|
+
getPathAliases() {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
fs: fsUtils,
|
|
715
|
+
telemetry,
|
|
716
|
+
async confirm(message, initialValue = true) {
|
|
717
|
+
if (parsedFlags.y === true || parsedFlags.yes === true) return true;
|
|
718
|
+
const result = await p.confirm({
|
|
719
|
+
message,
|
|
720
|
+
initialValue
|
|
721
|
+
});
|
|
722
|
+
if (p.isCancel(result)) errorHandlers.handleCancel("Confirmation cancelled");
|
|
723
|
+
return result;
|
|
724
|
+
},
|
|
725
|
+
projectRoot,
|
|
726
|
+
framework,
|
|
727
|
+
packageManager
|
|
728
|
+
};
|
|
729
|
+
telemetry.trackEvent(TelemetryEventName.CLI_ENVIRONMENT_DETECTED, {
|
|
730
|
+
command: commandName ?? "interactive",
|
|
731
|
+
projectRootChanged: projectRoot !== cwd,
|
|
732
|
+
framework: framework.framework ?? "unknown",
|
|
733
|
+
frameworkVersion: framework.frameworkVersion ?? "unknown",
|
|
734
|
+
packageManager: packageManager.name,
|
|
735
|
+
hasReact: framework.hasReact,
|
|
736
|
+
reactVersion: framework.reactVersion ?? "unknown",
|
|
737
|
+
tailwindVersion: framework.tailwindVersion ?? "unknown"
|
|
738
|
+
});
|
|
739
|
+
return context;
|
|
740
|
+
}
|
|
741
|
+
function createTestContext(overrides = {}) {
|
|
742
|
+
const logger = createCliLogger("error");
|
|
743
|
+
const telemetry = createDisabledTelemetry();
|
|
744
|
+
const error = createErrorHandlers(logger, telemetry);
|
|
745
|
+
return {
|
|
746
|
+
logger,
|
|
747
|
+
flags: {},
|
|
748
|
+
commandName: void 0,
|
|
749
|
+
commandArgs: [],
|
|
750
|
+
cwd: process.cwd(),
|
|
751
|
+
error,
|
|
752
|
+
config: {
|
|
753
|
+
loadConfig: async () => null,
|
|
754
|
+
requireConfig: async () => {
|
|
755
|
+
throw new CliError("CONFIG_NOT_FOUND");
|
|
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 () => {}
|
|
768
|
+
},
|
|
769
|
+
telemetry,
|
|
770
|
+
confirm: async () => true,
|
|
771
|
+
projectRoot: process.cwd(),
|
|
772
|
+
framework: {
|
|
773
|
+
framework: null,
|
|
774
|
+
frameworkVersion: null,
|
|
775
|
+
pkg: null,
|
|
776
|
+
hasReact: false,
|
|
777
|
+
reactVersion: null,
|
|
778
|
+
tailwindVersion: null
|
|
779
|
+
},
|
|
780
|
+
packageManager: {
|
|
781
|
+
name: "npm",
|
|
782
|
+
installCommand: "npm install",
|
|
783
|
+
addCommand: "npm install",
|
|
784
|
+
runCommand: "npm run",
|
|
785
|
+
execCommand: "npx"
|
|
786
|
+
},
|
|
787
|
+
...overrides
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
//#endregion
|
|
791
|
+
//#region src/help.ts
|
|
792
|
+
function showHelpMenu(context, options, commands, flags) {
|
|
793
|
+
const commandRows = commands.filter((command) => !command.hidden).map((command) => ` ${command.name.padEnd(16)} ${command.description}`).join("\n");
|
|
794
|
+
const flagRows = flags.map((flag) => {
|
|
795
|
+
const valueHint = flag.expectsValue ? " <value>" : "";
|
|
796
|
+
return ` ${flag.names.join(", ")}${valueHint}\t${flag.description}`;
|
|
797
|
+
}).join("\n");
|
|
798
|
+
const docsLine = options.docsUrl ? `\n\nDocs:\n ${options.docsUrl}` : "";
|
|
799
|
+
context.logger.note(`${options.appName} ${options.version}\n\nUsage:\n ${options.appName} <command> [options]\n\nCommands:\n${commandRows}\n\nGlobal Flags:\n${flagRows}${docsLine}`, `${options.appName} CLI`);
|
|
800
|
+
}
|
|
801
|
+
//#endregion
|
|
802
|
+
//#region src/intro.ts
|
|
803
|
+
function renderFiglet(text) {
|
|
804
|
+
return new Promise((resolve) => {
|
|
805
|
+
figlet(text, (error, data) => {
|
|
806
|
+
if (error || !data) {
|
|
807
|
+
resolve(text);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
resolve(data);
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async function displayIntro(context, options) {
|
|
815
|
+
const banner = await renderFiglet(options.figletText ?? options.appName);
|
|
816
|
+
const versionLabel = options.version ? ` v${options.version}` : "";
|
|
817
|
+
context.logger.message(banner);
|
|
818
|
+
context.logger.note(options.tagline ?? `${options.appName}${versionLabel}`, options.appName);
|
|
819
|
+
}
|
|
820
|
+
//#endregion
|
|
821
|
+
//#region src/spinner.ts
|
|
822
|
+
function createSpinner(initialMessage) {
|
|
823
|
+
const spinner = p.spinner();
|
|
824
|
+
return {
|
|
825
|
+
start(message) {
|
|
826
|
+
spinner.start(message || initialMessage || "Processing...");
|
|
827
|
+
},
|
|
828
|
+
stop(message) {
|
|
829
|
+
spinner.stop(message || "Done");
|
|
830
|
+
},
|
|
831
|
+
message(message) {
|
|
832
|
+
spinner.message(message);
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
async function withSpinner(message, task, options) {
|
|
837
|
+
const spinner = createSpinner(message);
|
|
838
|
+
spinner.start();
|
|
839
|
+
try {
|
|
840
|
+
const result = await task();
|
|
841
|
+
spinner.stop(options?.successMessage || "Done");
|
|
842
|
+
return result;
|
|
843
|
+
} catch (error) {
|
|
844
|
+
spinner.stop(options?.errorMessage || "Failed");
|
|
845
|
+
throw error;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/version-check.ts
|
|
850
|
+
const DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
851
|
+
const DEFAULT_TIMEOUT_MS = 1500;
|
|
852
|
+
const DEFAULT_CACHE_TTL_MS = 1440 * 60 * 1e3;
|
|
853
|
+
const defaultLogger = {
|
|
854
|
+
message(message) {
|
|
855
|
+
process.stdout.write(`${message}\n`);
|
|
856
|
+
},
|
|
857
|
+
note(content, title) {
|
|
858
|
+
const prefix = title ? `${title}\n` : "";
|
|
859
|
+
process.stdout.write(`${prefix}${content}\n`);
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
function isVersionRequest(rawArgs) {
|
|
863
|
+
return rawArgs.includes("-v") || rawArgs.includes("--version");
|
|
864
|
+
}
|
|
865
|
+
function normalizePath(filePath) {
|
|
866
|
+
return filePath.replaceAll("\\", "/");
|
|
867
|
+
}
|
|
868
|
+
function expandHomePrefix(prefix) {
|
|
869
|
+
if (!prefix.startsWith("~/")) return prefix;
|
|
870
|
+
return path$1.join(os.homedir(), prefix.slice(2));
|
|
871
|
+
}
|
|
872
|
+
function safeRealpath(filePath) {
|
|
873
|
+
try {
|
|
874
|
+
return fsSync$1.realpathSync(filePath);
|
|
875
|
+
} catch {
|
|
876
|
+
return filePath;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function isPathUnder(candidate, parent) {
|
|
880
|
+
const normalizedCandidate = normalizePath(path$1.resolve(candidate));
|
|
881
|
+
const normalizedParent = normalizePath(path$1.resolve(expandHomePrefix(parent)));
|
|
882
|
+
return normalizedCandidate === normalizedParent || normalizedCandidate.startsWith(`${normalizedParent}/`);
|
|
883
|
+
}
|
|
884
|
+
function envValue(name) {
|
|
885
|
+
return process.env[name];
|
|
886
|
+
}
|
|
887
|
+
function detectInstallSource(binPath = process.argv[1] ?? "") {
|
|
888
|
+
const resolvedPath = normalizePath(safeRealpath(binPath || ""));
|
|
889
|
+
const npmPrefix = envValue("npm_config_prefix");
|
|
890
|
+
if (resolvedPath.includes("/opt/homebrew/") || resolvedPath.includes("/usr/local/Cellar/") || resolvedPath.includes("/home/linuxbrew/") || resolvedPath.includes("/Homebrew/Cellar/")) return "brew";
|
|
891
|
+
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";
|
|
893
|
+
if (resolvedPath.includes("/.pnpm-store/") || resolvedPath.includes("/dlx-")) return "pnpm-dlx";
|
|
894
|
+
if (resolvedPath.includes("/.yarn/berry/cache/") || resolvedPath.includes("/yarn/dlx-")) return "yarn-dlx";
|
|
895
|
+
if (resolvedPath.includes("/node_modules/.bin/") || resolvedPath.endsWith("/node_modules/.bin")) return "local";
|
|
896
|
+
if ([
|
|
897
|
+
"/usr/local/lib/node_modules",
|
|
898
|
+
"~/.npm-global/lib/node_modules",
|
|
899
|
+
"~/.nvm/versions/node",
|
|
900
|
+
process.env.APPDATA ? `${process.env.APPDATA}/npm/node_modules` : null,
|
|
901
|
+
npmPrefix ? `${npmPrefix}/lib/node_modules` : null
|
|
902
|
+
].filter((item) => typeof item === "string").some((prefix) => {
|
|
903
|
+
const expandedPrefix = expandHomePrefix(prefix);
|
|
904
|
+
if (prefix.endsWith("/node")) return resolvedPath.includes("/.nvm/versions/node/") && resolvedPath.includes("/lib/node_modules/");
|
|
905
|
+
return isPathUnder(resolvedPath, expandedPrefix);
|
|
906
|
+
})) return "npm-global";
|
|
907
|
+
return "unknown";
|
|
908
|
+
}
|
|
909
|
+
function getUpdateCommand(source, packageName, brewFormula = packageName) {
|
|
910
|
+
switch (source) {
|
|
911
|
+
case "npm-global": return `npm install -g ${packageName}@latest`;
|
|
912
|
+
case "brew": return `brew upgrade ${brewFormula}`;
|
|
913
|
+
case "local": return `npm install ${packageName}@latest`;
|
|
914
|
+
default: return null;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function sanitizeCacheName(packageName) {
|
|
918
|
+
return packageName.replaceAll("/", "__").replaceAll("@", "");
|
|
919
|
+
}
|
|
920
|
+
function getCacheDir(options) {
|
|
921
|
+
return options.cacheDir ?? path$1.join(os.tmpdir(), "hexbus-version-cache");
|
|
922
|
+
}
|
|
923
|
+
function getCachePath(options) {
|
|
924
|
+
return path$1.join(getCacheDir(options), `${sanitizeCacheName(options.packageName)}.json`);
|
|
925
|
+
}
|
|
926
|
+
function readCachedVersion(options) {
|
|
927
|
+
try {
|
|
928
|
+
const content = fsSync$1.readFileSync(getCachePath(options), "utf-8");
|
|
929
|
+
const parsed = JSON.parse(content);
|
|
930
|
+
if (typeof parsed.version === "string" && typeof parsed.fetchedAt === "number") return {
|
|
931
|
+
version: parsed.version,
|
|
932
|
+
fetchedAt: parsed.fetchedAt
|
|
933
|
+
};
|
|
934
|
+
} catch {}
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
function isCacheFresh(cache, options) {
|
|
938
|
+
const now = options.now?.() ?? Date.now();
|
|
939
|
+
const ttl = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
940
|
+
return now - cache.fetchedAt < ttl;
|
|
941
|
+
}
|
|
942
|
+
async function writeCachedVersion(options, version) {
|
|
943
|
+
const cachePath = getCachePath(options);
|
|
944
|
+
const cacheDir = path$1.dirname(cachePath);
|
|
945
|
+
const tempPath = `${cachePath}.${process.pid}.tmp`;
|
|
946
|
+
const payload = {
|
|
947
|
+
version,
|
|
948
|
+
fetchedAt: options.now?.() ?? Date.now()
|
|
949
|
+
};
|
|
950
|
+
await fs$1.mkdir(cacheDir, { recursive: true });
|
|
951
|
+
await fs$1.writeFile(tempPath, `${JSON.stringify(payload)}\n`, "utf-8");
|
|
952
|
+
await fs$1.rename(tempPath, cachePath);
|
|
953
|
+
}
|
|
954
|
+
function parseVersionParts(version) {
|
|
955
|
+
return (version.replace(/^[^\d]*/, "").split("-")[0] ?? "").split(".").map((part) => {
|
|
956
|
+
const parsed = Number.parseInt(part.replace(/\D.*$/, ""), 10);
|
|
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;
|
|
969
|
+
}
|
|
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
|
+
}
|
|
992
|
+
async function fetchLatestVersion(options) {
|
|
993
|
+
const controller = new AbortController();
|
|
994
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
995
|
+
const registryUrl = options.registryUrl ?? DEFAULT_REGISTRY_URL;
|
|
996
|
+
const encodedName = encodeURIComponent(options.packageName);
|
|
997
|
+
try {
|
|
998
|
+
const response = await fetch(`${registryUrl.replace(/\/$/, "")}/${encodedName}/latest`, { signal: controller.signal });
|
|
999
|
+
if (!response.ok) return null;
|
|
1000
|
+
const payload = await response.json();
|
|
1001
|
+
return typeof payload.version === "string" ? payload.version : null;
|
|
1002
|
+
} catch {
|
|
1003
|
+
return null;
|
|
1004
|
+
} finally {
|
|
1005
|
+
clearTimeout(timeout);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async function refreshCache(options) {
|
|
1009
|
+
const latestVersion = await fetchLatestVersion(options);
|
|
1010
|
+
if (latestVersion) await writeCachedVersion(options, latestVersion);
|
|
1011
|
+
return latestVersion;
|
|
1012
|
+
}
|
|
1013
|
+
async function checkForUpdate(options) {
|
|
1014
|
+
const cached = readCachedVersion(options);
|
|
1015
|
+
if (cached) return createResult(options, cached.version);
|
|
1016
|
+
return createResult(options, await refreshCache(options));
|
|
1017
|
+
}
|
|
1018
|
+
function formatUpdateHint(result) {
|
|
1019
|
+
if (!result.isOutdated || result.updateCommand === null || result.latestVersion === null) return null;
|
|
1020
|
+
if (result.source === "brew") return [
|
|
1021
|
+
`Latest npm version is ${color.green(result.latestVersion)}.`,
|
|
1022
|
+
"If you installed with Homebrew, update with:",
|
|
1023
|
+
` ${color.cyan(result.updateCommand)}`
|
|
1024
|
+
].join("\n");
|
|
1025
|
+
return [
|
|
1026
|
+
`A new version is available: ${color.dim(result.currentVersion)} -> ${color.green(result.latestVersion)}`,
|
|
1027
|
+
"Update with:",
|
|
1028
|
+
` ${color.cyan(result.updateCommand)}`
|
|
1029
|
+
].join("\n");
|
|
1030
|
+
}
|
|
1031
|
+
async function printVersionInfo(options) {
|
|
1032
|
+
const logger = options.logger ?? defaultLogger;
|
|
1033
|
+
logger.message(`${options.appName} v${options.currentVersion}`);
|
|
1034
|
+
const result = await checkForUpdate(options);
|
|
1035
|
+
if (result.hint) logger.note(result.hint, "Update available");
|
|
1036
|
+
}
|
|
1037
|
+
function startBackgroundUpdateCheck(options) {
|
|
1038
|
+
const logger = options.logger ?? defaultLogger;
|
|
1039
|
+
const cached = readCachedVersion(options);
|
|
1040
|
+
if (cached) {
|
|
1041
|
+
const result = createResult(options, cached.version);
|
|
1042
|
+
if (result.hint) logger.note(result.hint, "Update available");
|
|
1043
|
+
}
|
|
1044
|
+
if (cached && isCacheFresh(cached, options)) return;
|
|
1045
|
+
refreshCache(options).catch((error) => {
|
|
1046
|
+
logger.debug?.(`Update check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
//#endregion
|
|
1050
|
+
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 };
|