@ls-stack/cli 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/dist/main.mjs ADDED
@@ -0,0 +1,723 @@
1
+ import { checkbox, confirm, input, number, search, select } from "@inquirer/prompts";
2
+ import * as readline from "readline";
3
+ import { styleText } from "node:util";
4
+
5
+ //#region src/cliInput.ts
6
+ function isUserCancellation(e) {
7
+ return e instanceof Error && (e.name === "ExitPromptError" || e.name === "AbortError" || e.name === "AbortPromptError");
8
+ }
9
+ function handlePromptError(e) {
10
+ if (isUserCancellation(e)) process.exit(0);
11
+ console.error(e);
12
+ process.exit(1);
13
+ }
14
+ function createEscapeAbortController() {
15
+ const controller = new AbortController();
16
+ readline.emitKeypressEvents(process.stdin);
17
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
18
+ function onKeypress(_, key) {
19
+ if (key.name === "escape") controller.abort();
20
+ }
21
+ process.stdin.on("keypress", onKeypress);
22
+ function cleanup() {
23
+ process.stdin.removeListener("keypress", onKeypress);
24
+ }
25
+ return {
26
+ signal: controller.signal,
27
+ cleanup
28
+ };
29
+ }
30
+ async function withEscapeSupport(promptFn) {
31
+ const { signal, cleanup } = createEscapeAbortController();
32
+ try {
33
+ return await promptFn(signal);
34
+ } finally {
35
+ cleanup();
36
+ }
37
+ }
38
+ /**
39
+ * Interactive CLI input utilities with ESC-to-cancel support.
40
+ *
41
+ * All prompts exit the process with code 0 when the user presses ESC or Ctrl+C.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { cliInput } from '@ls-stack/cli';
46
+ *
47
+ * const name = await cliInput.text('Enter your name');
48
+ * const confirm = await cliInput.confirm('Proceed?', { initial: true });
49
+ * ```
50
+ */
51
+ const cliInput = {
52
+ select: async (title, { options }) => {
53
+ try {
54
+ return await withEscapeSupport((signal) => select({
55
+ message: title,
56
+ choices: options.map((option) => ({
57
+ value: option.value,
58
+ name: option.label ?? option.value,
59
+ description: option.hint
60
+ }))
61
+ }, { signal }));
62
+ } catch (e) {
63
+ handlePromptError(e);
64
+ }
65
+ },
66
+ textWithAutocomplete: async (title, { options, validate }) => {
67
+ try {
68
+ const choices = options.map((option) => ({
69
+ value: option.value,
70
+ name: option.label ?? option.value,
71
+ description: option.hint
72
+ }));
73
+ return await withEscapeSupport((signal) => search({
74
+ message: title,
75
+ source: (term) => {
76
+ if (!term) return choices;
77
+ const lowerTerm = term.toLowerCase();
78
+ return choices.filter((c) => c.name.toLowerCase().includes(lowerTerm) || c.value.toLowerCase().includes(lowerTerm) || c.description?.toLowerCase().includes(lowerTerm));
79
+ },
80
+ validate
81
+ }, { signal }));
82
+ } catch (e) {
83
+ handlePromptError(e);
84
+ }
85
+ },
86
+ text: async (title, { initial, validate } = {}) => {
87
+ try {
88
+ return await withEscapeSupport((signal) => input({
89
+ message: title,
90
+ default: initial,
91
+ required: true,
92
+ validate
93
+ }, { signal }));
94
+ } catch (e) {
95
+ handlePromptError(e);
96
+ }
97
+ },
98
+ confirm: async (title, { initial } = {}) => {
99
+ try {
100
+ return await withEscapeSupport((signal) => confirm({
101
+ message: title,
102
+ default: initial
103
+ }, { signal }));
104
+ } catch (e) {
105
+ handlePromptError(e);
106
+ }
107
+ },
108
+ multipleSelect: async (title, { options }) => {
109
+ try {
110
+ return await withEscapeSupport((signal) => checkbox({
111
+ message: title,
112
+ required: true,
113
+ choices: options.map((option) => ({
114
+ value: option.value,
115
+ name: option.label ?? option.value,
116
+ description: option.hint
117
+ }))
118
+ }, { signal }));
119
+ } catch (e) {
120
+ handlePromptError(e);
121
+ }
122
+ },
123
+ number: async (title, { initial } = {}) => {
124
+ try {
125
+ return await withEscapeSupport((signal) => number({
126
+ message: title,
127
+ default: initial,
128
+ required: true
129
+ }, { signal }));
130
+ } catch (e) {
131
+ if (isUserCancellation(e)) process.exit(0);
132
+ console.error(e);
133
+ return null;
134
+ }
135
+ }
136
+ };
137
+
138
+ //#endregion
139
+ //#region node_modules/.pnpm/@ls-stack+utils@3.68.0/node_modules/@ls-stack/utils/dist/assertions-CsE3pD8P.mjs
140
+ /**
141
+ * Ensures exhaustive type checking in switch statements or conditional logic.
142
+ *
143
+ * This function should be used in the default case of switch statements or the
144
+ * final else branch of conditional logic to ensure all possible cases are
145
+ * handled. It helps catch missing cases at compile time when new union members
146
+ * are added.
147
+ *
148
+ * @example
149
+ * ```typescript
150
+ * type Status = 'pending' | 'success' | 'error';
151
+ *
152
+ * function handleStatus(status: Status) {
153
+ * switch (status) {
154
+ * case 'pending':
155
+ * return 'Loading...';
156
+ * case 'success':
157
+ * return 'Done!';
158
+ * case 'error':
159
+ * return 'Failed!';
160
+ * default:
161
+ * throw exhaustiveCheck(status); // TypeScript error if Status gains new members
162
+ * }
163
+ * }
164
+ *
165
+ * // In conditional logic
166
+ * function processValue(value: string | number) {
167
+ * if (typeof value === 'string') {
168
+ * return value.toUpperCase();
169
+ * } else if (typeof value === 'number') {
170
+ * return value.toString();
171
+ * } else {
172
+ * throw exhaustiveCheck(value); // Ensures all cases are covered
173
+ * }
174
+ * }
175
+ * ```;
176
+ *
177
+ * @template Except - The type that should never be reached
178
+ * @param narrowedType - The value that should never exist at runtime
179
+ * @returns An Error object (this function should never actually execute)
180
+ */
181
+ function exhaustiveCheck(narrowedType) {
182
+ return /* @__PURE__ */ new Error("This should never happen");
183
+ }
184
+
185
+ //#endregion
186
+ //#region node_modules/.pnpm/@ls-stack+utils@3.68.0/node_modules/@ls-stack/utils/dist/arrayUtils.mjs
187
+ /**
188
+ * Sort an array based on a value
189
+ *
190
+ * Sort by `ascending` order by default
191
+ *
192
+ * Use `Infinity` as as wildcard to absolute max and min values
193
+ *
194
+ * @example
195
+ * const items = [1, 3, 2, 4];
196
+ *
197
+ * const sortedItems = sortBy(items, (item) => item);
198
+ * // [1, 2, 3, 4]
199
+ *
200
+ * const items2 = [
201
+ * { a: 1, b: 2 },
202
+ * { a: 2, b: 1 },
203
+ * { a: 1, b: 1 },
204
+ * ];
205
+ *
206
+ * // return a array to sort by multiple values
207
+ * const sortedItems = sortBy(items, (item) => [item.a, item.b]);
208
+ *
209
+ * @param arr
210
+ * @param sortByValue
211
+ * @param props
212
+ */
213
+ function sortBy(arr, sortByValue, props = "asc") {
214
+ const order = Array.isArray(props) || typeof props === "string" ? props : props.order ?? "asc";
215
+ return [...arr].sort((a, b) => {
216
+ const _aPriority = sortByValue(a);
217
+ const _bPriority = sortByValue(b);
218
+ const aPriority = Array.isArray(_aPriority) ? _aPriority : [_aPriority];
219
+ const bPriority = Array.isArray(_bPriority) ? _bPriority : [_bPriority];
220
+ for (let i = 0; i < aPriority.length; i++) {
221
+ const levelOrder = typeof order === "string" ? order : order[i] ?? "asc";
222
+ const aP = aPriority[i] ?? 0;
223
+ const bP = bPriority[i] ?? 0;
224
+ if (aP === bP) continue;
225
+ if (bP === Infinity || aP === -Infinity || aP < bP) return levelOrder === "asc" ? -1 : 1;
226
+ if (aP === Infinity || bP === -Infinity || aP > bP) return levelOrder === "asc" ? 1 : -1;
227
+ }
228
+ return 0;
229
+ });
230
+ }
231
+ /**
232
+ * Get the correct 0 based value for sync with other array in ascending order
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * const items = [1, 2, 3];
237
+ *
238
+ * const index = sortBy(
239
+ * items,
240
+ * (item) => getAscIndexOrder(
241
+ * followOrder.findIndex((order) => order === item)
242
+ * )
243
+ * );
244
+ * ```;
245
+ *
246
+ * @param index
247
+ */
248
+ function getAscIndexOrder(index) {
249
+ return index === -1 ? Infinity : index ?? Infinity;
250
+ }
251
+
252
+ //#endregion
253
+ //#region node_modules/.pnpm/@ls-stack+utils@3.68.0/node_modules/@ls-stack/utils/dist/dedent.mjs
254
+ /**
255
+ * Remove common leading indentation from multi-line strings while preserving
256
+ * relative indentation. Can be used as a tagged template literal or called with
257
+ * a plain string.
258
+ *
259
+ * By default, it will dedent interpolated multi-line strings to match the
260
+ * surrounding context. And it will not show falsy values.
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * const text = dedent`;
265
+ * function hello() {
266
+ * console.log('world');
267
+ * }
268
+ * `;
269
+ * // Result:
270
+ * "function hello() {
271
+ * console.log('world');
272
+ * }"
273
+ * ```;
274
+ */
275
+ const dedent = createDedent({ identInterpolations: true });
276
+ function createDedent(options) {
277
+ d.withOptions = (newOptions) => createDedent({
278
+ ...options,
279
+ ...newOptions
280
+ });
281
+ return d;
282
+ function d(strings, ...values) {
283
+ const raw = typeof strings === "string" ? [strings] : strings.raw;
284
+ const { escapeSpecialCharacters = Array.isArray(strings), trimWhitespace = true, identInterpolations = true, showNullishOrFalseValues = false } = options;
285
+ let result = "";
286
+ for (let i = 0; i < raw.length; i++) {
287
+ let next = raw[i];
288
+ if (escapeSpecialCharacters) next = next.replace(/\\\n[ \t]*/g, "").replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\\{/g, "{");
289
+ result += next;
290
+ if (i < values.length) {
291
+ let val = values[i];
292
+ if (!showNullishOrFalseValues && (val === false || val === null || val === void 0)) continue;
293
+ val = String(val);
294
+ if (identInterpolations && val.includes("\n")) {
295
+ let withIdent = val;
296
+ const currentIndent = getCurrentIndent(result);
297
+ if (currentIndent && withIdent) withIdent = withIdent.split("\n").map((line, index) => {
298
+ return index === 0 || line === "" ? line : currentIndent + line;
299
+ }).join("\n");
300
+ result += withIdent;
301
+ } else result += val;
302
+ }
303
+ }
304
+ const lines = result.split("\n");
305
+ let mindent = null;
306
+ for (const l of lines) {
307
+ const m = l.match(/^(\s+)\S+/);
308
+ if (m) {
309
+ const indent = m[1].length;
310
+ if (!mindent) mindent = indent;
311
+ else mindent = Math.min(mindent, indent);
312
+ }
313
+ }
314
+ if (mindent !== null) {
315
+ const m = mindent;
316
+ result = lines.map((l) => l[0] === " " || l[0] === " " ? l.slice(m) : l).join("\n");
317
+ }
318
+ if (trimWhitespace) result = result.trim();
319
+ if (escapeSpecialCharacters) result = result.replace(/\\n/g, "\n");
320
+ return result;
321
+ }
322
+ }
323
+ function getCurrentIndent(str) {
324
+ const lines = str.split("\n");
325
+ const lastLine = lines[lines.length - 1];
326
+ if (!lastLine) return "";
327
+ const match = lastLine.match(/^(\s*)/);
328
+ return match ? match[1] : "";
329
+ }
330
+
331
+ //#endregion
332
+ //#region node_modules/.pnpm/@ls-stack+utils@3.68.0/node_modules/@ls-stack/utils/dist/stringUtils-DEEj_Z1p.mjs
333
+ function removeANSIColors(str) {
334
+ return str.replace(/\u001b\[\d+m/g, "");
335
+ }
336
+
337
+ //#endregion
338
+ //#region node_modules/.pnpm/@ls-stack+utils@3.68.0/node_modules/@ls-stack/utils/dist/typingFnUtils-BLxmDUp5.mjs
339
+ /**
340
+ * A wrapper to Object.entries with a better typing inference
341
+ *
342
+ * @param obj
343
+ */
344
+ function typedObjectEntries(obj) {
345
+ return Object.entries(obj);
346
+ }
347
+ /**
348
+ * Type helper to narrow a string to a key of an object.
349
+ *
350
+ * @param key
351
+ * @param obj
352
+ */
353
+ function isObjKey(key, obj) {
354
+ return typeof key === "string" && key in obj;
355
+ }
356
+
357
+ //#endregion
358
+ //#region src/createCli.ts
359
+ const [, , cmdFromTerminal, ...cmdArgs] = process.argv;
360
+ /**
361
+ * Creates a type-safe command definition for use with `createCLI`.
362
+ *
363
+ * The `run` function receives fully typed arguments based on the `args` definition.
364
+ * Positional arguments are parsed in declaration order.
365
+ *
366
+ * @template Args - Record of argument definitions
367
+ * @param options - Command configuration
368
+ * @param options.description - Command description shown in help
369
+ * @param options.short - Optional single-character alias (cannot be 'i' or 'h')
370
+ * @param options.args - Typed argument definitions
371
+ * @param options.run - Handler function receiving parsed arguments
372
+ * @param options.examples - Optional usage examples for help text
373
+ * @returns Command definition object
374
+ *
375
+ * @example
376
+ * ```ts
377
+ * const deploy = createCmd({
378
+ * short: 'd',
379
+ * description: 'Deploy the application',
380
+ * args: {
381
+ * env: {
382
+ * type: 'positional-string',
383
+ * name: 'env',
384
+ * description: 'Target environment',
385
+ * },
386
+ * port: {
387
+ * type: 'value-number-flag',
388
+ * name: 'port',
389
+ * description: 'Port number',
390
+ * default: 3000,
391
+ * },
392
+ * verbose: {
393
+ * type: 'flag',
394
+ * name: 'verbose',
395
+ * description: 'Enable verbose logging',
396
+ * },
397
+ * },
398
+ * examples: [
399
+ * { args: ['production'], description: 'Deploy to production' },
400
+ * { args: ['staging', '--port', '8080'], description: 'Deploy to staging on port 8080' },
401
+ * ],
402
+ * run: async ({ env, port, verbose }) => {
403
+ * // env: string, port: number, verbose: boolean
404
+ * console.log(`Deploying to ${env} on port ${port}`);
405
+ * },
406
+ * });
407
+ * ```
408
+ */
409
+ function createCmd({ short, description, run, args, examples }) {
410
+ return {
411
+ short,
412
+ description,
413
+ run,
414
+ args,
415
+ examples
416
+ };
417
+ }
418
+ /**
419
+ * Creates and runs a CLI application with the given commands.
420
+ *
421
+ * Automatically handles argument parsing, help generation, and interactive mode.
422
+ *
423
+ * **Built-in commands:**
424
+ * - `h` or `--help` - Shows help with all commands
425
+ * - `i` - Interactive mode (select command from list)
426
+ * - `<command> -h` - Shows help for a specific command
427
+ *
428
+ * @template C - String literal union of command names
429
+ * @param options - CLI configuration
430
+ * @param options.name - CLI display name shown in header
431
+ * @param options.baseCmd - Command prefix for help text (e.g., 'my-cli')
432
+ * @param options.sort - Optional array to customize command display order
433
+ * @param cmds - Record of command definitions created with `createCmd`
434
+ *
435
+ * @example
436
+ * ```ts
437
+ * await createCLI(
438
+ * { name: 'My CLI', baseCmd: 'my-cli' },
439
+ * {
440
+ * hello: createCmd({
441
+ * short: 'hi',
442
+ * description: 'Say hello',
443
+ * run: async () => console.log('Hello!'),
444
+ * }),
445
+ * deploy: createCmd({
446
+ * short: 'd',
447
+ * description: 'Deploy the app',
448
+ * args: {
449
+ * env: { type: 'positional-string', name: 'env', description: 'Environment' },
450
+ * },
451
+ * run: async ({ env }) => console.log(`Deploying to ${env}`),
452
+ * }),
453
+ * },
454
+ * );
455
+ * ```
456
+ *
457
+ * @example
458
+ * ```bash
459
+ * # Usage examples:
460
+ * my-cli # Show interactive menu
461
+ * my-cli h # Show help
462
+ * my-cli i # Interactive mode
463
+ * my-cli hello # Run hello command
464
+ * my-cli hi # Run hello via short alias
465
+ * my-cli deploy prod # Run deploy with argument
466
+ * my-cli deploy -h # Show deploy command help
467
+ * ```
468
+ */
469
+ async function createCLI({ name, sort, baseCmd }, cmds) {
470
+ function getCmdId(cmd) {
471
+ if (isObjKey(cmd, cmds)) return cmd;
472
+ console.error(styleText(["red", "bold"], `Command '${cmd}' not found`));
473
+ process.exit(1);
474
+ }
475
+ process.stdout.write("\x1Bc");
476
+ console.info(styleText(["blue", "bold"], name));
477
+ const addedShortCmds = /* @__PURE__ */ new Set();
478
+ const reservedShortCmds = ["i", "h"];
479
+ let runCmdId = cmdFromTerminal;
480
+ for (const [, cmd] of typedObjectEntries(cmds)) if (cmd.short) {
481
+ if (reservedShortCmds.includes(cmd.short)) {
482
+ console.error(styleText(["red", "bold"], `Short cmd "${cmd.short}" is reserved for built-in commands`));
483
+ process.exit(1);
484
+ }
485
+ if (addedShortCmds.has(cmd.short)) {
486
+ console.error(styleText(["red", "bold"], `Short cmd "${cmd.short}" is duplicated`));
487
+ process.exit(1);
488
+ }
489
+ addedShortCmds.add(cmd.short);
490
+ }
491
+ function printHelp() {
492
+ const pipeChar = styleText(["dim"], " or ");
493
+ const fmtCmd = (c) => styleText(["blue", "bold"], c);
494
+ const beforeDescription = styleText(["dim"], "->");
495
+ const largestCmdTextLength = Math.max(...typedObjectEntries(cmds).map(([cmd, { short }]) => `${cmd}${short ? ` or ${short}` : ""}`.length));
496
+ console.info(dedent`
497
+ ${styleText(["blue", "bold"], "Docs:")}
498
+
499
+ ${styleText(["bold", "underline"], "Usage:")} ${baseCmd} <command> [command-args...]
500
+
501
+ ${styleText(["bold", "underline"], "Commands:")}
502
+
503
+ ${typedObjectEntries(cmds).map(([cmd, { description, short, args }]) => {
504
+ const cmdText = `${fmtCmd(cmd)}${short ? `${pipeChar}${fmtCmd(short)}` : ""}`;
505
+ const unformattedCmdText = removeANSIColors(cmdText);
506
+ let result = `${cmdText}${" ".repeat(largestCmdTextLength - unformattedCmdText.length + 1)}${beforeDescription} ${description}`;
507
+ if (args && Object.keys(args).length > 0) {
508
+ const briefArgs = typedObjectEntries(args).sort(([, a], [, b]) => {
509
+ if (a.type.startsWith("positional") && !b.type.startsWith("positional")) return -1;
510
+ if (!a.type.startsWith("positional") && b.type.startsWith("positional")) return 1;
511
+ return 0;
512
+ }).map(([, arg]) => {
513
+ switch (arg.type) {
514
+ case "positional-string":
515
+ case "positional-number": return `[${arg.name}]`;
516
+ case "flag": return `[--${arg.name}]`;
517
+ case "value-string-flag":
518
+ case "value-number-flag": return `[--${arg.name}]`;
519
+ default: throw exhaustiveCheck(arg);
520
+ }
521
+ }).join(" ");
522
+ result += `\n${styleText(["dim"], ` └─ args: ${briefArgs}`)}`;
523
+ }
524
+ return result;
525
+ }).join("\n")}
526
+
527
+ ${styleText(["dim"], `Use ${baseCmd} <cmd> -h for more details about a command`)}
528
+
529
+ ${fmtCmd("i")} ${beforeDescription} Starts in interactive mode
530
+ ${fmtCmd("h")} ${beforeDescription} Prints this help message
531
+ `);
532
+ console.info(styleText(["dim"], "Use a command to get started!"));
533
+ }
534
+ function printCmdHelp(cmdId) {
535
+ const cmd = cmds[getCmdId(cmdId)];
536
+ function formatArg(arg) {
537
+ switch (arg.type) {
538
+ case "positional-string":
539
+ case "positional-number": return `[${arg.name}]`;
540
+ case "flag": return `[--${arg.name}]`;
541
+ case "value-string-flag": return `[--${arg.name} <value>]`;
542
+ case "value-number-flag": return `[--${arg.name} <number>]`;
543
+ default: throw exhaustiveCheck(arg);
544
+ }
545
+ }
546
+ function getArgsUsage(args) {
547
+ if (!args) return "";
548
+ const sortedArgs = typedObjectEntries(args).sort(([, a], [, b]) => {
549
+ if (a.type.startsWith("positional") && !b.type.startsWith("positional")) return -1;
550
+ if (!a.type.startsWith("positional") && b.type.startsWith("positional")) return 1;
551
+ return 0;
552
+ });
553
+ const colors = [
554
+ "cyan",
555
+ "yellow",
556
+ "magenta",
557
+ "blue",
558
+ "green",
559
+ "red"
560
+ ];
561
+ return sortedArgs.map(([, arg], index) => {
562
+ const color = colors[index % colors.length];
563
+ if (!color) return formatArg(arg);
564
+ return styleText([color], formatArg(arg)) || formatArg(arg);
565
+ }).join(" ");
566
+ }
567
+ const cmdTitle = cmd.short ? `${cmdId} (${cmd.short})` : cmdId;
568
+ let helpText = `${styleText(["blue", "bold"], `${cmdTitle}:`) || `${cmdTitle}:`} ${cmd.description}\n\n${styleText(["bold", "underline"], "Usage:") || "Usage:"} ${styleText(["dim"], baseCmd) || baseCmd} ${styleText(["bold"], cmdId) || cmdId}`;
569
+ if (cmd.args) {
570
+ const argsUsage = getArgsUsage(cmd.args);
571
+ if (argsUsage) helpText += ` ${argsUsage}`;
572
+ }
573
+ if (cmd.short) helpText += `\n ${styleText(["dim"], baseCmd) || baseCmd} ${styleText(["bold"], cmd.short) || cmd.short} ...`;
574
+ if (cmd.args) {
575
+ const argEntries = typedObjectEntries(cmd.args);
576
+ if (argEntries.length > 0) {
577
+ helpText += `\n\n${styleText(["bold", "underline"], "Arguments:") || "Arguments:"}`;
578
+ const colors = [
579
+ "cyan",
580
+ "yellow",
581
+ "magenta",
582
+ "blue",
583
+ "green",
584
+ "red"
585
+ ];
586
+ for (const [index, [, arg]] of argEntries.entries()) {
587
+ const color = colors[index % colors.length];
588
+ if (!color) continue;
589
+ const argDisplayName = arg.type === "flag" ? `--${arg.name}` : arg.type === "value-string-flag" ? `--${arg.name}` : arg.type === "value-number-flag" ? `--${arg.name}` : arg.name;
590
+ const argName = styleText([color], argDisplayName) || argDisplayName;
591
+ helpText += `\n ${argName} - ${arg.description}`;
592
+ }
593
+ }
594
+ }
595
+ if (cmd.examples) {
596
+ helpText += `\n\n${styleText(["bold", "underline"], "Examples:") || "Examples:"}`;
597
+ for (const { args: exampleArgs, description: exampleDesc } of cmd.examples) helpText += `\n ${baseCmd} ${cmdId} ${exampleArgs.join(" ")} ${styleText(["dim"], `# ${exampleDesc}`) || `# ${exampleDesc}`}`;
598
+ }
599
+ console.info(helpText);
600
+ console.info(styleText(["dim"], "Use a command to get started!"));
601
+ }
602
+ if (!cmdFromTerminal) if (await cliInput.select("Choose an action", { options: [{
603
+ value: "run-cmd",
604
+ label: "Start interactive mode",
605
+ hint: `Select a command to run from a list | ${baseCmd} i`
606
+ }, {
607
+ value: "print-help",
608
+ label: "Print help",
609
+ hint: `${baseCmd} h`
610
+ }] }) === "print-help") {
611
+ printHelp();
612
+ process.exit(0);
613
+ } else runCmdId = "i";
614
+ if (runCmdId === "-h" || runCmdId === "--help" || runCmdId === "help" || runCmdId === "h") {
615
+ printHelp();
616
+ process.exit(0);
617
+ }
618
+ function parseArgs(rawArgs, commandArgs) {
619
+ if (!commandArgs) return {};
620
+ const parsed = {};
621
+ const argEntries = typedObjectEntries(commandArgs);
622
+ for (const [key, argDef] of argEntries) if (argDef.type === "flag") parsed[key] = false;
623
+ else if ("default" in argDef && argDef.default !== void 0) parsed[key] = argDef.default;
624
+ const positionalArgs = argEntries.filter(([, argDef]) => argDef.type.startsWith("positional"));
625
+ let positionalIndex = 0;
626
+ let i = 0;
627
+ while (i < rawArgs.length) {
628
+ const currentArg = rawArgs[i];
629
+ if (currentArg?.startsWith("--")) {
630
+ const flagName = currentArg.slice(2);
631
+ const flagEntry = argEntries.find(([, argDef]) => argDef.name === flagName);
632
+ if (flagEntry) {
633
+ const [key, argDef] = flagEntry;
634
+ if (argDef.type === "flag") {
635
+ parsed[key] = true;
636
+ i++;
637
+ } else if (argDef.type === "value-string-flag" || argDef.type === "value-number-flag") {
638
+ const value = rawArgs[i + 1];
639
+ if (value && !value.startsWith("--")) {
640
+ if (argDef.type === "value-number-flag") {
641
+ const numValue = Number(value);
642
+ if (isNaN(numValue)) {
643
+ console.error(styleText(["red", "bold"], `Error: Invalid number "${value}" for --${argDef.name}`));
644
+ process.exit(1);
645
+ }
646
+ parsed[key] = numValue;
647
+ } else parsed[key] = value;
648
+ i += 2;
649
+ } else {
650
+ console.error(styleText(["red", "bold"], `Error: Missing value for --${argDef.name}`));
651
+ process.exit(1);
652
+ }
653
+ } else i++;
654
+ } else {
655
+ console.error(styleText(["red", "bold"], `Error: Unknown flag --${flagName}`));
656
+ process.exit(1);
657
+ }
658
+ } else {
659
+ if (positionalIndex < positionalArgs.length) {
660
+ const positionalEntry = positionalArgs[positionalIndex];
661
+ if (positionalEntry) {
662
+ const [key, argDef] = positionalEntry;
663
+ if (argDef.type === "positional-number") {
664
+ const numValue = Number(currentArg);
665
+ if (isNaN(numValue)) {
666
+ console.error(styleText(["red", "bold"], `Error: Invalid number "${currentArg}" for ${argDef.name}`));
667
+ process.exit(1);
668
+ }
669
+ parsed[key] = numValue;
670
+ } else parsed[key] = currentArg;
671
+ positionalIndex++;
672
+ }
673
+ }
674
+ i++;
675
+ }
676
+ }
677
+ for (const [key, argDef] of positionalArgs) if (!("default" in argDef) && parsed[key] === void 0) {
678
+ console.error(styleText(["red", "bold"], `Error: Missing required argument <${argDef.name}>`));
679
+ process.exit(1);
680
+ }
681
+ return parsed;
682
+ }
683
+ async function runCmd(cmd, args) {
684
+ process.stdout.write("\x1Bc");
685
+ for (const [cmdId, { short, run: fn, args: commandArgs }] of typedObjectEntries(cmds)) if (cmd === short || cmd === cmdId) {
686
+ if (args.includes("-h") || args.includes("--help")) {
687
+ printCmdHelp(cmdId);
688
+ process.exit(0);
689
+ }
690
+ console.info(`Running ${styleText(["blue", "bold"], cmdId)}${short ? styleText(["dim"], `|${short}`) : ""}:\n`);
691
+ await fn(parseArgs(args, commandArgs));
692
+ process.exit(0);
693
+ }
694
+ console.error(styleText(["red", "bold"], `Command '${cmd}' not found`));
695
+ printHelp();
696
+ process.exit(1);
697
+ }
698
+ if (runCmdId === "i") {
699
+ function hasRequiredArgs(args) {
700
+ if (!args) return false;
701
+ return typedObjectEntries(args).some(([, arg]) => (arg.type === "positional-string" || arg.type === "positional-number") && !("default" in arg));
702
+ }
703
+ let cmdEntries = typedObjectEntries(cmds);
704
+ if (sort) cmdEntries = sortBy(cmdEntries, ([cmd]) => getAscIndexOrder(sort.indexOf(cmd)));
705
+ const availableCmds = cmdEntries.filter(([, cmd]) => !hasRequiredArgs(cmd.args));
706
+ await runCmd(await cliInput.select("Select a command", { options: availableCmds.map(([cmd, { short, description }]) => ({
707
+ value: cmd,
708
+ label: short ? `${cmd} ${styleText(["dim"], "|")} ${short}` : cmd,
709
+ hint: description
710
+ })) }), []);
711
+ } else {
712
+ if (!runCmdId) {
713
+ console.error(styleText(["red", "bold"], `Command not found, use \`${baseCmd} h\` to list all supported commands`));
714
+ console.info(styleText(["dim"], "Use a command to get started!"));
715
+ process.exit(1);
716
+ }
717
+ await runCmd(runCmdId, cmdArgs);
718
+ }
719
+ }
720
+
721
+ //#endregion
722
+ export { cliInput, createCLI, createCmd };
723
+ //# sourceMappingURL=main.mjs.map