argsbarg 0.1.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/code.mdc +2 -2
- package/CHANGELOG.md +45 -0
- package/README.md +40 -28
- package/examples/minimal.ts +12 -7
- package/examples/nested.ts +16 -13
- package/justfile +32 -0
- package/package.json +6 -11
- package/plan.md +2 -2
- package/scripts/release.ts +249 -0
- package/src/completion.ts +33 -14
- package/src/context.ts +2 -1
- package/src/help.ts +58 -23
- package/src/index.test.ts +79 -35
- package/src/index.ts +3 -14
- package/src/parse.ts +56 -25
- package/src/runtime.ts +2 -2
- package/src/types.ts +45 -37
- package/src/validate.ts +15 -11
- package/bun.lock +0 -21
package/src/completion.ts
CHANGED
|
@@ -8,43 +8,51 @@ It keeps completion aligned with the runtime schema so the generated commands,
|
|
|
8
8
|
options, and descriptions stay in sync with the CLI definition.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { CliCommand,
|
|
11
|
+
import { CliCommand, CliOption } from "./types.ts";
|
|
12
12
|
|
|
13
13
|
// ── Shared Types ───────────────────────────────────────────────────────────────
|
|
14
14
|
|
|
15
|
+
/** One tab-completion scope: child commands, options, and path key for the schema walk. */
|
|
15
16
|
interface ScopeRec {
|
|
17
|
+
/** Child `CliCommand` keys at this scope. */
|
|
16
18
|
kids: CliCommand[];
|
|
17
|
-
|
|
19
|
+
/** Options in scope (for option token completion). */
|
|
20
|
+
opts: CliOption[];
|
|
21
|
+
/** `/`-separated path of command keys from the root, or `""` for the root. */
|
|
18
22
|
path: string;
|
|
23
|
+
/** True when a positional tail exists (bash/zsh may offer filenames). */
|
|
19
24
|
wantsFiles: boolean;
|
|
20
25
|
}
|
|
21
26
|
|
|
27
|
+
/** True if the command declares at least one positional (used to enable file completion). */
|
|
22
28
|
function hasPositionalArguments(cmd: CliCommand): boolean {
|
|
23
|
-
return (cmd.positionals ?? []).
|
|
29
|
+
return (cmd.positionals ?? []).length > 0;
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
/** Recursively appends a scope record for `cmd` and its subtree into `acc`. */
|
|
26
33
|
function walkScopes(cmdPath: string, cmd: CliCommand, acc: ScopeRec[]): void {
|
|
27
34
|
acc.push({
|
|
28
|
-
kids: cmd.
|
|
35
|
+
kids: cmd.commands ?? [],
|
|
29
36
|
opts: cmd.options ?? [],
|
|
30
37
|
path: cmdPath,
|
|
31
38
|
wantsFiles: hasPositionalArguments(cmd),
|
|
32
39
|
});
|
|
33
|
-
for (const ch of cmd.
|
|
40
|
+
for (const ch of cmd.commands ?? []) {
|
|
34
41
|
const nextPath = cmdPath === "" ? ch.key : cmdPath + "/" + ch.key;
|
|
35
42
|
walkScopes(nextPath, ch, acc);
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
/** Flattens the schema into a list of completion scopes (root + every command path). */
|
|
39
47
|
function collectScopes(schema: CliCommand): ScopeRec[] {
|
|
40
48
|
const acc: ScopeRec[] = [];
|
|
41
49
|
acc.push({
|
|
42
|
-
kids: schema.
|
|
50
|
+
kids: schema.commands ?? [],
|
|
43
51
|
opts: schema.options ?? [],
|
|
44
52
|
path: "",
|
|
45
53
|
wantsFiles: false,
|
|
46
54
|
});
|
|
47
|
-
for (const c of schema.
|
|
55
|
+
for (const c of schema.commands ?? []) {
|
|
48
56
|
walkScopes(c.key, c, acc);
|
|
49
57
|
}
|
|
50
58
|
return acc;
|
|
@@ -52,14 +60,17 @@ function collectScopes(schema: CliCommand): ScopeRec[] {
|
|
|
52
60
|
|
|
53
61
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
54
62
|
|
|
63
|
+
/** Produces a shell-safe identifier from the app or command name (alnum → `_`). */
|
|
55
64
|
function identToken(s: string): string {
|
|
56
65
|
return s.replace(/[^a-zA-Z0-9]/g, "_");
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
/** Escapes a string for use inside single quotes in generated shell scripts. */
|
|
59
69
|
function escShellSingleQuoted(s: string): string {
|
|
60
70
|
return s.replace(/'/g, "'\\''");
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
/** Sanitizes the app key for generated shell function names (non-alnum removed). */
|
|
63
74
|
function mainName(schemaName: string): string {
|
|
64
75
|
return schemaName.replace(/[^a-zA-Z0-9]/g, "_");
|
|
65
76
|
}
|
|
@@ -69,6 +80,7 @@ const kHelpShort = "-h";
|
|
|
69
80
|
|
|
70
81
|
// ── Bash Completion ────────────────────────────────────────────────────────────
|
|
71
82
|
|
|
83
|
+
/** Emits the bash helper that classifies a `--long` or `--long=val` token for each scope. */
|
|
72
84
|
function emitConsumeLong(ident: string, scopes: ScopeRec[]): string {
|
|
73
85
|
let o = "_${ident}_nac_consume_long() {\n".replace("${ident}", ident);
|
|
74
86
|
o += " local sid=\"$1\" w=\"$2\" nw=\"$3\"\n";
|
|
@@ -78,7 +90,6 @@ function emitConsumeLong(ident: string, scopes: ScopeRec[]): string {
|
|
|
78
90
|
o += " case $w in\n";
|
|
79
91
|
o += " " + kHelpLong + "|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n".replace(/\$\{kHelpLong\}/g, kHelpLong).replace(/\$\{kHelpShort\}/g, kHelpShort);
|
|
80
92
|
for (const op of sc.opts) {
|
|
81
|
-
if (op.positional) continue;
|
|
82
93
|
const base = "--" + op.name;
|
|
83
94
|
if (op.kind === "presence") {
|
|
84
95
|
o += " " + base + "|${base}=*) echo 1 ;;\n".replace(/\$\{base\}/g, base);
|
|
@@ -97,6 +108,7 @@ function emitConsumeLong(ident: string, scopes: ScopeRec[]): string {
|
|
|
97
108
|
return o;
|
|
98
109
|
}
|
|
99
110
|
|
|
111
|
+
/** Emits the bash helper that classifies a bundled/short `-x` / `-abc` token for each scope. */
|
|
100
112
|
function emitConsumeShort(ident: string, scopes: ScopeRec[]): string {
|
|
101
113
|
let o = "_${ident}_nac_consume_short() {\n".replace("${ident}", ident);
|
|
102
114
|
o += " local sid=\"$1\" w=\"$2\"\n";
|
|
@@ -112,7 +124,6 @@ function emitConsumeShort(ident: string, scopes: ScopeRec[]): string {
|
|
|
112
124
|
o += " case $ch in\n";
|
|
113
125
|
let boolChars = "";
|
|
114
126
|
for (const op of sc.opts) {
|
|
115
|
-
if (op.positional) continue;
|
|
116
127
|
if (!op.shortName) continue;
|
|
117
128
|
if (op.kind === "presence") {
|
|
118
129
|
boolChars += op.shortName + "|";
|
|
@@ -139,6 +150,7 @@ function emitConsumeShort(ident: string, scopes: ScopeRec[]): string {
|
|
|
139
150
|
return o;
|
|
140
151
|
}
|
|
141
152
|
|
|
153
|
+
/** Emits the bash helper that maps a non-option token to the child scope id. */
|
|
142
154
|
function emitMatchChild(ident: string, scopes: ScopeRec[], pathIndex: Record<string, number>): string {
|
|
143
155
|
let o = "_${ident}_nac_match_child() {\n".replace("${ident}", ident);
|
|
144
156
|
o += " local sid=\"$1\" w=\"$2\"\n";
|
|
@@ -161,6 +173,7 @@ function emitMatchChild(ident: string, scopes: ScopeRec[], pathIndex: Record<str
|
|
|
161
173
|
return o;
|
|
162
174
|
}
|
|
163
175
|
|
|
176
|
+
/** Emits bash that replays argv up to the current word to find the active completion scope. */
|
|
164
177
|
function emitSimulate(ident: string): string {
|
|
165
178
|
let o = "_${ident}_nac_simulate() {\n".replace("${ident}", ident);
|
|
166
179
|
o += " local i=1 sid=0 w steps next\n";
|
|
@@ -198,6 +211,7 @@ function emitSimulate(ident: string): string {
|
|
|
198
211
|
return o;
|
|
199
212
|
}
|
|
200
213
|
|
|
214
|
+
/** Emits the main `COMPREPLY` driver and `complete -F` registration for bash. */
|
|
201
215
|
function emitMainBodyBash(schema: CliCommand, ident: string): string {
|
|
202
216
|
const main = mainName(schema.key);
|
|
203
217
|
let o = "_${main}() {\n".replace("${main}", main);
|
|
@@ -231,6 +245,7 @@ function emitMainBodyBash(schema: CliCommand, ident: string): string {
|
|
|
231
245
|
return o;
|
|
232
246
|
}
|
|
233
247
|
|
|
248
|
+
/** Returns a self-contained bash `complete` script for the given program schema. */
|
|
234
249
|
export function completionBashScript(schema: CliCommand): string {
|
|
235
250
|
const ident = identToken(schema.key);
|
|
236
251
|
const scopes = collectScopes(schema);
|
|
@@ -246,7 +261,6 @@ export function completionBashScript(schema: CliCommand): string {
|
|
|
246
261
|
out += "A_" + ident + "_" + i + "_opts=()\n";
|
|
247
262
|
out += "A_" + ident + "_" + i + "_opts+=('" + kHelpLong + "' '" + kHelpShort + "')\n";
|
|
248
263
|
for (const o of sc.opts) {
|
|
249
|
-
if (o.positional) continue;
|
|
250
264
|
out += "A_" + ident + "_" + i + "_opts+=('--" + o.name + "')\n";
|
|
251
265
|
if (o.shortName) {
|
|
252
266
|
out += "A_" + ident + "_" + i + "_opts+=('-" + o.shortName + "')\n";
|
|
@@ -274,6 +288,7 @@ export function completionBashScript(schema: CliCommand): string {
|
|
|
274
288
|
|
|
275
289
|
// ── Zsh Completion ─────────────────────────────────────────────────────────────
|
|
276
290
|
|
|
291
|
+
/** Emits zsh `typeset` arrays of options and subcommands for each scope. */
|
|
277
292
|
function emitScopeArraysZsh(ident: string, scopes: ScopeRec[]): string {
|
|
278
293
|
let out = "";
|
|
279
294
|
for (const [i, sc] of scopes.entries()) {
|
|
@@ -305,6 +320,7 @@ function emitScopeArraysZsh(ident: string, scopes: ScopeRec[]): string {
|
|
|
305
320
|
return out;
|
|
306
321
|
}
|
|
307
322
|
|
|
323
|
+
/** Zsh: long-option classifier function source (mirrors bash consume_long). */
|
|
308
324
|
function emitConsumeLongZsh(ident: string, scopes: ScopeRec[]): string {
|
|
309
325
|
let o = "_${ident}_nac_consume_long() {\n".replace("${ident}", ident);
|
|
310
326
|
o += " local sid=\"$1\" w=\"$2\" nw=\"$3\"\n";
|
|
@@ -314,7 +330,6 @@ function emitConsumeLongZsh(ident: string, scopes: ScopeRec[]): string {
|
|
|
314
330
|
o += " case $w in\n";
|
|
315
331
|
o += " " + kHelpLong + "|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n".replace(/\$\{kHelpLong\}/g, kHelpLong).replace(/\$\{kHelpShort\}/g, kHelpShort);
|
|
316
332
|
for (const op of sc.opts) {
|
|
317
|
-
if (op.positional) continue;
|
|
318
333
|
const base = "--" + op.name;
|
|
319
334
|
if (op.kind === "presence") {
|
|
320
335
|
o += " " + base + "|${base}=*) echo 1 ;;\n".replace(/\$\{base\}/g, base);
|
|
@@ -333,6 +348,7 @@ function emitConsumeLongZsh(ident: string, scopes: ScopeRec[]): string {
|
|
|
333
348
|
return o;
|
|
334
349
|
}
|
|
335
350
|
|
|
351
|
+
/** Zsh: short-option classifier function source. */
|
|
336
352
|
function emitConsumeShortZsh(ident: string, scopes: ScopeRec[]): string {
|
|
337
353
|
let o = "_${ident}_nac_consume_short() {\n".replace("${ident}", ident);
|
|
338
354
|
o += " local sid=\"$1\" w=\"$2\"\n";
|
|
@@ -348,7 +364,6 @@ function emitConsumeShortZsh(ident: string, scopes: ScopeRec[]): string {
|
|
|
348
364
|
o += " case $ch in\n";
|
|
349
365
|
let boolChars = "";
|
|
350
366
|
for (const op of sc.opts) {
|
|
351
|
-
if (op.positional) continue;
|
|
352
367
|
if (!op.shortName) continue;
|
|
353
368
|
if (op.kind === "presence") {
|
|
354
369
|
boolChars += op.shortName + "|";
|
|
@@ -375,6 +390,7 @@ function emitConsumeShortZsh(ident: string, scopes: ScopeRec[]): string {
|
|
|
375
390
|
return o;
|
|
376
391
|
}
|
|
377
392
|
|
|
393
|
+
/** Zsh: subcommand name → scope id matching helper. */
|
|
378
394
|
function emitMatchChildZsh(ident: string, scopes: ScopeRec[], pathIndex: Record<string, number>): string {
|
|
379
395
|
let o = "_${ident}_nac_match_child() {\n".replace("${ident}", ident);
|
|
380
396
|
o += " local sid=\"$1\" w=\"$2\"\n";
|
|
@@ -397,6 +413,7 @@ function emitMatchChildZsh(ident: string, scopes: ScopeRec[], pathIndex: Record<
|
|
|
397
413
|
return o;
|
|
398
414
|
}
|
|
399
415
|
|
|
416
|
+
/** Zsh: simulates word traversal to the current completion context (sets `REPLY_SID`). */
|
|
400
417
|
function emitSimulateZsh(ident: string): string {
|
|
401
418
|
let o = "_${ident}_nac_simulate() {\n".replace("${ident}", ident);
|
|
402
419
|
o += " local i=2 sid=0 w steps next\n";
|
|
@@ -434,6 +451,7 @@ function emitSimulateZsh(ident: string): string {
|
|
|
434
451
|
return o;
|
|
435
452
|
}
|
|
436
453
|
|
|
454
|
+
/** Zsh: `_main` completer and `compdef` registration. */
|
|
437
455
|
function emitMainBodyZsh(schema: CliCommand, ident: string): string {
|
|
438
456
|
const main = mainName(schema.key);
|
|
439
457
|
let o = "_${main}() {\n".replace("${main}", main);
|
|
@@ -465,6 +483,7 @@ function emitMainBodyZsh(schema: CliCommand, ident: string): string {
|
|
|
465
483
|
return o;
|
|
466
484
|
}
|
|
467
485
|
|
|
486
|
+
/** Returns a self-contained zsh completion script for the given program schema. */
|
|
468
487
|
export function completionZshScript(schema: CliCommand): string {
|
|
469
488
|
const ident = identToken(schema.key);
|
|
470
489
|
const scopes = collectScopes(schema);
|
|
@@ -484,13 +503,13 @@ export function completionZshScript(schema: CliCommand): string {
|
|
|
484
503
|
}
|
|
485
504
|
|
|
486
505
|
/**
|
|
487
|
-
* Builds the static `completion` / `bash` / `zsh` subtree
|
|
506
|
+
* Builds the static `completion` / `bash` / `zsh` command subtree (merged into the program root at runtime).
|
|
488
507
|
*/
|
|
489
508
|
export function cliBuiltinCompletionGroup(appName: string): CliCommand {
|
|
490
509
|
return {
|
|
491
510
|
key: "completion",
|
|
492
511
|
description: "Generate the autocompletion script for shells.",
|
|
493
|
-
|
|
512
|
+
commands: [
|
|
494
513
|
{
|
|
495
514
|
key: "bash",
|
|
496
515
|
description: "Print a bash tab-completion script.",
|
package/src/context.ts
CHANGED
|
@@ -20,6 +20,7 @@ export class CliContext {
|
|
|
20
20
|
readonly schema: CliCommand;
|
|
21
21
|
readonly opts: Record<string, string>;
|
|
22
22
|
|
|
23
|
+
/** Captures the merged program root, routed path, positional words, and option map for a leaf handler. */
|
|
23
24
|
constructor(
|
|
24
25
|
appName: string,
|
|
25
26
|
commandPath: string[],
|
|
@@ -35,7 +36,7 @@ export class CliContext {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
/** Returns whether a presence flag was set (including implicit "1" for boolean options). */
|
|
38
|
-
|
|
39
|
+
hasFlag(name: string): boolean {
|
|
39
40
|
return this.opts[name] !== undefined;
|
|
40
41
|
}
|
|
41
42
|
|
package/src/help.ts
CHANGED
|
@@ -7,32 +7,41 @@ It keeps help formatting shared across help and error paths so users see one con
|
|
|
7
7
|
style no matter how help is reached.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { CliCommand,
|
|
10
|
+
import { CliCommand, CliOption, CliOptionKind, CliPositional } from "./types.ts";
|
|
11
11
|
|
|
12
12
|
// ── ANSI Style Helpers ────────────────────────────────────────────────────────
|
|
13
13
|
|
|
14
|
+
/** SGR wrappers for TTY help output. */
|
|
14
15
|
const style = {
|
|
16
|
+
/** Joins a message with a prefix and a reset (or suffix) for ANSI SGR. */
|
|
15
17
|
wrap(prefix: string, body: string, suffix: string): string {
|
|
16
18
|
return prefix + body + suffix;
|
|
17
19
|
},
|
|
20
|
+
/** Renders the message in red. */
|
|
18
21
|
red(msg: string): string {
|
|
19
22
|
return this.wrap("\u001B[31m", msg, "\u001B[0m");
|
|
20
23
|
},
|
|
24
|
+
/** Renders the message in gray. */
|
|
21
25
|
gray(msg: string): string {
|
|
22
26
|
return this.wrap("\u001B[90m", msg, "\u001B[0m");
|
|
23
27
|
},
|
|
28
|
+
/** Renders the message in bold. */
|
|
24
29
|
bold(msg: string): string {
|
|
25
30
|
return this.wrap("\u001B[1m", msg, "\u001B[0m");
|
|
26
31
|
},
|
|
32
|
+
/** Renders the message in white. */
|
|
27
33
|
white(msg: string): string {
|
|
28
34
|
return this.wrap("\u001B[37m", msg, "\u001B[0m");
|
|
29
35
|
},
|
|
36
|
+
/** Renders the message in bright aqua + bold. */
|
|
30
37
|
aquaBold(msg: string): string {
|
|
31
38
|
return this.wrap("\u001B[96m\u001B[1m", msg, "\u001B[0m");
|
|
32
39
|
},
|
|
40
|
+
/** Renders the message in bright green. */
|
|
33
41
|
greenBright(msg: string): string {
|
|
34
42
|
return this.wrap("\u001B[92m", msg, "\u001B[0m");
|
|
35
43
|
},
|
|
44
|
+
/** Renders a section title: gray and bold. */
|
|
36
45
|
grayBoldTitle(title: string): string {
|
|
37
46
|
return this.gray(this.bold(title));
|
|
38
47
|
},
|
|
@@ -49,11 +58,13 @@ const kBoxH = "\u2500"; // ─
|
|
|
49
58
|
|
|
50
59
|
// ── Terminal Detection ────────────────────────────────────────────────────────
|
|
51
60
|
|
|
61
|
+
/** Returns a minimum column width for help, clamped to stdout width when known. */
|
|
52
62
|
function getHelpWidth(): number {
|
|
53
63
|
return Math.max(40, process.stdout.columns || 80);
|
|
54
64
|
}
|
|
55
65
|
|
|
56
|
-
|
|
66
|
+
/** True when stdout is a TTY (used to decide on color). */
|
|
67
|
+
function isTTY(): boolean {
|
|
57
68
|
return process.stdout.isTTY !== undefined;
|
|
58
69
|
}
|
|
59
70
|
|
|
@@ -78,20 +89,24 @@ function visibleWidth(s: string): number {
|
|
|
78
89
|
return w;
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
/** Repeats the horizontal box-drawing character `n` times. */
|
|
81
93
|
function repeatBoxH(n: number): string {
|
|
82
94
|
return kBoxH.repeat(Math.max(0, n));
|
|
83
95
|
}
|
|
84
96
|
|
|
97
|
+
/** Returns a string of `n` spaces. */
|
|
85
98
|
function spaces(n: number): string {
|
|
86
99
|
return " ".repeat(Math.max(0, n));
|
|
87
100
|
}
|
|
88
101
|
|
|
102
|
+
/** Pads `s` to visible width (ANSI-aware) to `width` columns. */
|
|
89
103
|
function padVisible(s: string, width: number): string {
|
|
90
104
|
return s + spaces(Math.max(0, width - visibleWidth(s)));
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
// ── Text Wrapping ─────────────────────────────────────────────────────────────
|
|
94
108
|
|
|
109
|
+
/** Word-wraps a single line of text to a maximum `width` in columns. */
|
|
95
110
|
function wrapParagraph(text: string, width: number): string[] {
|
|
96
111
|
const available = Math.max(1, width);
|
|
97
112
|
const out: string[] = [];
|
|
@@ -113,6 +128,7 @@ function wrapParagraph(text: string, width: number): string[] {
|
|
|
113
128
|
return out;
|
|
114
129
|
}
|
|
115
130
|
|
|
131
|
+
/** Splits on newlines and wraps each logical line, preserving intentional leading-indent lines. */
|
|
116
132
|
function wrapText(text: string, width: number): string[] {
|
|
117
133
|
const out: string[] = [];
|
|
118
134
|
const lines = text.split("\n");
|
|
@@ -134,6 +150,7 @@ function wrapText(text: string, width: number): string[] {
|
|
|
134
150
|
|
|
135
151
|
// ── Option Label Formatting ───────────────────────────────────────────────────
|
|
136
152
|
|
|
153
|
+
/** Suffix for `--name` in usage (e.g. ` <string>`) based on value kind. */
|
|
137
154
|
function optKindLabel(k: CliOptionKind): string {
|
|
138
155
|
switch (k) {
|
|
139
156
|
case CliOptionKind.Presence:
|
|
@@ -145,13 +162,8 @@ function optKindLabel(k: CliOptionKind): string {
|
|
|
145
162
|
}
|
|
146
163
|
}
|
|
147
164
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (o.argMax === 1) {
|
|
151
|
-
return o.argMin === 0 ? "[" + o.name + "]" : "<" + o.name + ">";
|
|
152
|
-
}
|
|
153
|
-
return o.argMin === 0 ? "[" + o.name + "...]" : "<" + o.name + "...>";
|
|
154
|
-
}
|
|
165
|
+
/** Formats a flag/value option for help tables: `--name`, optional short, optional kind hint. */
|
|
166
|
+
export function cliOptionLabel(o: CliOption, color: boolean): string {
|
|
155
167
|
let r = "--" + o.name + optKindLabel(o.kind);
|
|
156
168
|
if (o.shortName) r += ", -" + o.shortName;
|
|
157
169
|
if (!color) return r;
|
|
@@ -163,13 +175,30 @@ export function cliOptionLabel(o: CliOptionDef, color: boolean): string {
|
|
|
163
175
|
return style.aquaBold(left) + " " + style.greenBright(right);
|
|
164
176
|
}
|
|
165
177
|
|
|
178
|
+
/** Formats a positional slot label (`<n>`, `[n]`, or varargs) for help. */
|
|
179
|
+
export function cliPositionalLabel(p: CliPositional, color: boolean): string {
|
|
180
|
+
const { argMin = 1, argMax = 1 } = p;
|
|
181
|
+
let r: string;
|
|
182
|
+
if (argMax === 1) {
|
|
183
|
+
r = argMin === 0 ? "[" + p.name + "]" : "<" + p.name + ">";
|
|
184
|
+
} else {
|
|
185
|
+
r = argMin === 0 ? "[" + p.name + "...]" : "<" + p.name + "...>";
|
|
186
|
+
}
|
|
187
|
+
if (!color) return r;
|
|
188
|
+
return style.aquaBold(r);
|
|
189
|
+
}
|
|
190
|
+
|
|
166
191
|
// ── Box Rendering ─────────────────────────────────────────────────────────────
|
|
167
192
|
|
|
193
|
+
/** A single help table row: left column text and right-column description. */
|
|
168
194
|
interface HelpRow {
|
|
195
|
+
/** Option flag or subcommand / positional label. */
|
|
169
196
|
label: string;
|
|
197
|
+
/** Explanatory text (may be wrapped to multiple display lines). */
|
|
170
198
|
description: string;
|
|
171
199
|
}
|
|
172
200
|
|
|
201
|
+
/** Renders a free-text or notes box with a Unicode border and `title` header. */
|
|
173
202
|
function renderTextBox(title: string, lines: string[], hw: number, color: boolean): string[] {
|
|
174
203
|
if (lines.length === 0) return [];
|
|
175
204
|
|
|
@@ -208,6 +237,7 @@ function renderTextBox(title: string, lines: string[], hw: number, color: boolea
|
|
|
208
237
|
return out;
|
|
209
238
|
}
|
|
210
239
|
|
|
240
|
+
/** Renders a two-column label/description table in a box (options, subcommands, positionals). */
|
|
211
241
|
function renderTableBox(title: string, rows: HelpRow[], hw: number, color: boolean): string[] {
|
|
212
242
|
if (rows.length === 0) return [];
|
|
213
243
|
|
|
@@ -273,6 +303,7 @@ function renderTableBox(title: string, rows: HelpRow[], hw: number, color: boole
|
|
|
273
303
|
|
|
274
304
|
// ── Usage & Rows ──────────────────────────────────────────────────────────────
|
|
275
305
|
|
|
306
|
+
/** Builds one or two usage line strings (OPTIONS / COMMAND / ARGS) for the help header. */
|
|
276
307
|
function usageLines(
|
|
277
308
|
appName: string,
|
|
278
309
|
helpPath: string[],
|
|
@@ -304,25 +335,25 @@ function usageLines(
|
|
|
304
335
|
return out;
|
|
305
336
|
}
|
|
306
337
|
|
|
307
|
-
|
|
338
|
+
/** Table rows for named options, including a synthetic `--help, -h` row. */
|
|
339
|
+
function rowsForOptions(defs: CliOption[], color: boolean): HelpRow[] {
|
|
308
340
|
const rows: HelpRow[] = [];
|
|
309
341
|
const helpLabel = color
|
|
310
342
|
? style.aquaBold("--help, ") + style.greenBright("-h")
|
|
311
343
|
: "--help, -h";
|
|
312
344
|
rows.push({ label: helpLabel, description: "Show help for this command." });
|
|
313
345
|
for (const o of defs) {
|
|
314
|
-
if (o.positional) continue;
|
|
315
346
|
rows.push({ label: cliOptionLabel(o, color), description: o.description });
|
|
316
347
|
}
|
|
317
348
|
return rows;
|
|
318
349
|
}
|
|
319
350
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
.map((o) => ({ label: cliOptionLabel(o, color), description: o.description }));
|
|
351
|
+
/** Table rows for positional `CliPositional` definitions. */
|
|
352
|
+
function rowsForPositionals(defs: CliPositional[], color: boolean): HelpRow[] {
|
|
353
|
+
return defs.map((p) => ({ label: cliPositionalLabel(p, color), description: p.description }));
|
|
324
354
|
}
|
|
325
355
|
|
|
356
|
+
/** Table rows for subcommands, sorted by key. */
|
|
326
357
|
function rowsForSubcommands(cmds: CliCommand[]): HelpRow[] {
|
|
327
358
|
return cmds
|
|
328
359
|
.sort((a, b) => a.key.localeCompare(b.key))
|
|
@@ -331,9 +362,13 @@ function rowsForSubcommands(cmds: CliCommand[]): HelpRow[] {
|
|
|
331
362
|
|
|
332
363
|
// ── Main Help Render ──────────────────────────────────────────────────────────
|
|
333
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Renders full help for the app root or a nested command, following `helpPath` from the root key.
|
|
367
|
+
* `useStderr` is reserved for call-site consistency; width and color use stdout TTY.
|
|
368
|
+
*/
|
|
334
369
|
export function cliHelpRender(schema: CliCommand, helpPath: string[], useStderr: boolean): string {
|
|
335
370
|
const hw = getHelpWidth();
|
|
336
|
-
const color = isTTY(
|
|
371
|
+
const color = isTTY();
|
|
337
372
|
|
|
338
373
|
if (helpPath.length === 0) {
|
|
339
374
|
const lines: string[] = [];
|
|
@@ -345,7 +380,7 @@ export function cliHelpRender(schema: CliCommand, helpPath: string[], useStderr:
|
|
|
345
380
|
lines.push(
|
|
346
381
|
renderTextBox(
|
|
347
382
|
"Usage",
|
|
348
|
-
usageLines(schema.key, helpPath, (schema.
|
|
383
|
+
usageLines(schema.key, helpPath, (schema.commands ?? []).length > 0, false, color),
|
|
349
384
|
hw,
|
|
350
385
|
color,
|
|
351
386
|
).join("\n"),
|
|
@@ -356,16 +391,16 @@ export function cliHelpRender(schema: CliCommand, helpPath: string[], useStderr:
|
|
|
356
391
|
lines.push("");
|
|
357
392
|
lines.push(optBox.join("\n"));
|
|
358
393
|
}
|
|
359
|
-
if ((schema.
|
|
394
|
+
if ((schema.commands ?? []).length > 0) {
|
|
360
395
|
lines.push("");
|
|
361
396
|
lines.push(
|
|
362
|
-
renderTableBox("Commands", rowsForSubcommands(schema.
|
|
397
|
+
renderTableBox("Commands", rowsForSubcommands(schema.commands ?? []), hw, color).join("\n"),
|
|
363
398
|
);
|
|
364
399
|
}
|
|
365
400
|
return lines.join("\n") + "\n\n";
|
|
366
401
|
}
|
|
367
402
|
|
|
368
|
-
let layer = schema.
|
|
403
|
+
let layer = schema.commands ?? [];
|
|
369
404
|
let node: CliCommand | undefined;
|
|
370
405
|
for (const seg of helpPath) {
|
|
371
406
|
const ch = layer.find((c) => c.key === seg);
|
|
@@ -373,7 +408,7 @@ export function cliHelpRender(schema: CliCommand, helpPath: string[], useStderr:
|
|
|
373
408
|
return (color ? style.red("Unknown help path.") : "Unknown help path.") + "\n";
|
|
374
409
|
}
|
|
375
410
|
node = ch;
|
|
376
|
-
layer = ch.
|
|
411
|
+
layer = ch.commands ?? [];
|
|
377
412
|
}
|
|
378
413
|
if (!node) {
|
|
379
414
|
return (color ? style.red("Unknown help path.") : "Unknown help path.") + "\n";
|
|
@@ -388,7 +423,7 @@ export function cliHelpRender(schema: CliCommand, helpPath: string[], useStderr:
|
|
|
388
423
|
lines.push(
|
|
389
424
|
renderTextBox(
|
|
390
425
|
"Usage",
|
|
391
|
-
usageLines(schema.key, helpPath, (node.
|
|
426
|
+
usageLines(schema.key, helpPath, (node.commands ?? []).length > 0, (node.positionals ?? []).length > 0, color),
|
|
392
427
|
hw,
|
|
393
428
|
color,
|
|
394
429
|
).join("\n"),
|
|
@@ -406,7 +441,7 @@ export function cliHelpRender(schema: CliCommand, helpPath: string[], useStderr:
|
|
|
406
441
|
lines.push(posBox.join("\n"));
|
|
407
442
|
}
|
|
408
443
|
|
|
409
|
-
const subBox = renderTableBox("Subcommands", rowsForSubcommands(node.
|
|
444
|
+
const subBox = renderTableBox("Subcommands", rowsForSubcommands(node.commands ?? []), hw, color);
|
|
410
445
|
if (subBox.length > 0) {
|
|
411
446
|
lines.push("");
|
|
412
447
|
lines.push(subBox.join("\n"));
|