@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/README.md +532 -0
- package/dist/main.d.mts +409 -0
- package/dist/main.mjs +723 -0
- package/dist/main.mjs.map +1 -0
- package/package.json +47 -0
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
|