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/index.test.ts
CHANGED
|
@@ -7,31 +7,33 @@ It keeps the CLI contract stable by catching routing, option handling, and gener
|
|
|
7
7
|
shell output regressions.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
CliFallbackMode,
|
|
15
|
-
cliValidateRoot,
|
|
16
|
-
parse,
|
|
17
|
-
postParseValidate,
|
|
18
|
-
completionBashScript,
|
|
19
|
-
completionZshScript,
|
|
20
|
-
ParseKind,
|
|
21
|
-
} from "./index.ts";
|
|
10
|
+
import { completionBashScript, completionZshScript } from "./completion.ts";
|
|
11
|
+
import { CliCommand, CliFallbackMode, CliOptionKind } from "./index.ts";
|
|
12
|
+
import { ParseKind, parse, postParseValidate } from "./parse.ts";
|
|
13
|
+
import { cliValidateRoot } from "./validate.ts";
|
|
22
14
|
import { expect, test } from "bun:test";
|
|
23
15
|
|
|
24
16
|
test("bundled short presence flags", () => {
|
|
25
17
|
const root: CliCommand = {
|
|
26
18
|
key: "app",
|
|
27
19
|
description: "",
|
|
28
|
-
|
|
20
|
+
commands: [
|
|
29
21
|
{
|
|
30
22
|
key: "x",
|
|
31
23
|
description: "cmd",
|
|
32
24
|
options: [
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
{
|
|
26
|
+
name: "a",
|
|
27
|
+
description: "",
|
|
28
|
+
kind: CliOptionKind.Presence,
|
|
29
|
+
shortName: "a",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "b",
|
|
33
|
+
description: "",
|
|
34
|
+
kind: CliOptionKind.Presence,
|
|
35
|
+
shortName: "b",
|
|
36
|
+
},
|
|
35
37
|
],
|
|
36
38
|
handler: () => {},
|
|
37
39
|
},
|
|
@@ -48,11 +50,17 @@ test("long option equals", () => {
|
|
|
48
50
|
const root: CliCommand = {
|
|
49
51
|
key: "app",
|
|
50
52
|
description: "",
|
|
51
|
-
|
|
53
|
+
commands: [
|
|
52
54
|
{
|
|
53
55
|
key: "x",
|
|
54
56
|
description: "cmd",
|
|
55
|
-
options: [
|
|
57
|
+
options: [
|
|
58
|
+
{
|
|
59
|
+
name: "name",
|
|
60
|
+
description: "",
|
|
61
|
+
kind: CliOptionKind.String,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
56
64
|
handler: () => {},
|
|
57
65
|
},
|
|
58
66
|
],
|
|
@@ -67,11 +75,17 @@ test("fallback missing or unknown root flags", () => {
|
|
|
67
75
|
const root: CliCommand = {
|
|
68
76
|
key: "app",
|
|
69
77
|
description: "",
|
|
70
|
-
|
|
78
|
+
commands: [
|
|
71
79
|
{
|
|
72
80
|
key: "hello",
|
|
73
81
|
description: "Say hi.",
|
|
74
|
-
options: [
|
|
82
|
+
options: [
|
|
83
|
+
{
|
|
84
|
+
name: "name",
|
|
85
|
+
description: "",
|
|
86
|
+
kind: CliOptionKind.String,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
75
89
|
handler: () => {},
|
|
76
90
|
},
|
|
77
91
|
],
|
|
@@ -89,7 +103,7 @@ test("unknown command", () => {
|
|
|
89
103
|
const root: CliCommand = {
|
|
90
104
|
key: "app",
|
|
91
105
|
description: "",
|
|
92
|
-
|
|
106
|
+
commands: [{ key: "hello", description: "", handler: () => {} }],
|
|
93
107
|
};
|
|
94
108
|
cliValidateRoot(root);
|
|
95
109
|
const pr = parse(root, ["nope"]);
|
|
@@ -101,7 +115,7 @@ test("implicit help empty", () => {
|
|
|
101
115
|
const root: CliCommand = {
|
|
102
116
|
key: "app",
|
|
103
117
|
description: "",
|
|
104
|
-
|
|
118
|
+
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
105
119
|
};
|
|
106
120
|
cliValidateRoot(root);
|
|
107
121
|
const pr = parse(root, []);
|
|
@@ -113,11 +127,17 @@ test("invalid number post validate", () => {
|
|
|
113
127
|
const root: CliCommand = {
|
|
114
128
|
key: "app",
|
|
115
129
|
description: "",
|
|
116
|
-
|
|
130
|
+
commands: [
|
|
117
131
|
{
|
|
118
132
|
key: "x",
|
|
119
133
|
description: "",
|
|
120
|
-
options: [
|
|
134
|
+
options: [
|
|
135
|
+
{
|
|
136
|
+
name: "n",
|
|
137
|
+
description: "",
|
|
138
|
+
kind: CliOptionKind.Number,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
121
141
|
handler: () => {},
|
|
122
142
|
},
|
|
123
143
|
],
|
|
@@ -133,11 +153,17 @@ test("supports scientific notation in numbers", () => {
|
|
|
133
153
|
const root: CliCommand = {
|
|
134
154
|
key: "app",
|
|
135
155
|
description: "",
|
|
136
|
-
|
|
156
|
+
commands: [
|
|
137
157
|
{
|
|
138
158
|
key: "x",
|
|
139
159
|
description: "",
|
|
140
|
-
options: [
|
|
160
|
+
options: [
|
|
161
|
+
{
|
|
162
|
+
name: "n",
|
|
163
|
+
description: "",
|
|
164
|
+
kind: CliOptionKind.Number,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
141
167
|
handler: () => {},
|
|
142
168
|
},
|
|
143
169
|
],
|
|
@@ -153,7 +179,7 @@ test("root must not have handler", () => {
|
|
|
153
179
|
const root: CliCommand = {
|
|
154
180
|
key: "app",
|
|
155
181
|
description: "",
|
|
156
|
-
|
|
182
|
+
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
157
183
|
handler: () => {},
|
|
158
184
|
};
|
|
159
185
|
expect(() => cliValidateRoot(root)).toThrow(/Program root must not set handler/);
|
|
@@ -164,9 +190,15 @@ test("root must not have positionals", () => {
|
|
|
164
190
|
key: "app",
|
|
165
191
|
description: "",
|
|
166
192
|
positionals: [
|
|
167
|
-
|
|
193
|
+
{
|
|
194
|
+
name: "p",
|
|
195
|
+
description: "",
|
|
196
|
+
kind: CliOptionKind.String,
|
|
197
|
+
argMin: 1,
|
|
198
|
+
argMax: 1,
|
|
199
|
+
},
|
|
168
200
|
],
|
|
169
|
-
|
|
201
|
+
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
170
202
|
};
|
|
171
203
|
expect(() => cliValidateRoot(root)).toThrow(/Program root must not declare positionals/);
|
|
172
204
|
});
|
|
@@ -175,7 +207,7 @@ test("completion scripts contain app name", () => {
|
|
|
175
207
|
const root: CliCommand = {
|
|
176
208
|
key: "myapp",
|
|
177
209
|
description: "Test",
|
|
178
|
-
|
|
210
|
+
commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
|
|
179
211
|
};
|
|
180
212
|
cliValidateRoot(root);
|
|
181
213
|
const bash = completionBashScript(root);
|
|
@@ -192,7 +224,7 @@ test("completion scripts do not emit invalid bash substitutions", () => {
|
|
|
192
224
|
const root: CliCommand = {
|
|
193
225
|
key: "app",
|
|
194
226
|
description: "Test",
|
|
195
|
-
|
|
227
|
+
commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
|
|
196
228
|
};
|
|
197
229
|
cliValidateRoot(root);
|
|
198
230
|
const bash = completionBashScript(root);
|
|
@@ -203,7 +235,7 @@ test("completion scripts escape shell-sensitive command text in zsh", () => {
|
|
|
203
235
|
const root: CliCommand = {
|
|
204
236
|
key: "app",
|
|
205
237
|
description: "Test",
|
|
206
|
-
|
|
238
|
+
commands: [
|
|
207
239
|
{
|
|
208
240
|
key: "quote'cmd",
|
|
209
241
|
description: "Say 'hello' and keep going.",
|
|
@@ -220,7 +252,7 @@ test("completion scripts keep dotted app names in registration names", () => {
|
|
|
220
252
|
const root: CliCommand = {
|
|
221
253
|
key: "minimal.ts",
|
|
222
254
|
description: "Test",
|
|
223
|
-
|
|
255
|
+
commands: [{ key: "hello", description: "Say hello.", handler: () => {} }],
|
|
224
256
|
};
|
|
225
257
|
cliValidateRoot(root);
|
|
226
258
|
|
|
@@ -235,13 +267,25 @@ test("stops parsing options at --", () => {
|
|
|
235
267
|
const root: CliCommand = {
|
|
236
268
|
key: "app",
|
|
237
269
|
description: "",
|
|
238
|
-
|
|
270
|
+
commands: [
|
|
239
271
|
{
|
|
240
272
|
key: "x",
|
|
241
273
|
description: "cmd",
|
|
242
|
-
options: [
|
|
274
|
+
options: [
|
|
275
|
+
{
|
|
276
|
+
name: "name",
|
|
277
|
+
description: "",
|
|
278
|
+
kind: CliOptionKind.String,
|
|
279
|
+
},
|
|
280
|
+
],
|
|
243
281
|
positionals: [
|
|
244
|
-
|
|
282
|
+
{
|
|
283
|
+
name: "files",
|
|
284
|
+
description: "",
|
|
285
|
+
kind: CliOptionKind.String,
|
|
286
|
+
argMin: 0,
|
|
287
|
+
argMax: 0,
|
|
288
|
+
},
|
|
245
289
|
],
|
|
246
290
|
handler: () => {},
|
|
247
291
|
},
|
package/src/index.ts
CHANGED
|
@@ -7,18 +7,7 @@ It gives consumers one stable import path without forcing them to know the inter
|
|
|
7
7
|
module layout.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
export { cliBuiltinCompletionGroup, completionBashScript, completionZshScript } from "./completion.ts";
|
|
11
|
-
export { cliRun, cliErrWithHelp } from "./runtime";
|
|
12
10
|
export { CliContext } from "./context.ts";
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
15
|
-
export type {
|
|
16
|
-
export {
|
|
17
|
-
CliOptionKind,
|
|
18
|
-
CliFallbackMode,
|
|
19
|
-
CliSchemaValidationError,
|
|
20
|
-
createOption,
|
|
21
|
-
} from "./types.ts";
|
|
22
|
-
export type { CliOptionDef, CliCommand, CliHandler } from "./types.ts";
|
|
23
|
-
export { fullStringIsDouble, strictParseDouble } from "./utils.ts";
|
|
24
|
-
export { cliValidateRoot } from "./validate.ts";
|
|
11
|
+
export { cliErrWithHelp, cliRun } from "./runtime";
|
|
12
|
+
export { CliFallbackMode, CliOptionKind, CliSchemaValidationError } from "./types.ts";
|
|
13
|
+
export type { CliCommand, CliHandler, CliOption, CliPositional } from "./types.ts";
|
package/src/parse.ts
CHANGED
|
@@ -10,30 +10,43 @@ across every entry path.
|
|
|
10
10
|
import { CliContext } from "./context.ts";
|
|
11
11
|
import {
|
|
12
12
|
CliCommand,
|
|
13
|
-
CliOptionDef,
|
|
14
|
-
CliOptionKind,
|
|
15
13
|
CliFallbackMode,
|
|
14
|
+
CliOption,
|
|
15
|
+
CliOptionKind,
|
|
16
16
|
} from "./types.ts";
|
|
17
17
|
import { fullStringIsDouble } from "./utils.ts";
|
|
18
18
|
|
|
19
19
|
// ── Parse Result ──────────────────────────────────────────────────────────────
|
|
20
20
|
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Outcome of a parse: success, help request, or fatal user error.
|
|
23
|
+
*/
|
|
22
24
|
export enum ParseKind {
|
|
25
|
+
/** Parsed successfully; options and positionals are valid. */
|
|
23
26
|
Ok = "ok",
|
|
27
|
+
/** User requested help (explicit or implicit). */
|
|
24
28
|
Help = "help",
|
|
29
|
+
/** User error (unknown command, bad option, etc.). */
|
|
25
30
|
Error = "error",
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/** Structured parse output: routed path, merged options, positional args, and help/error metadata. */
|
|
29
34
|
export interface ParseResult {
|
|
35
|
+
/** Parse outcome (ok, help, or error). */
|
|
30
36
|
kind: ParseKind;
|
|
37
|
+
/** Routed subcommand keys from the program root (e.g. `["hello"]`). */
|
|
31
38
|
path: string[];
|
|
39
|
+
/** Merged long/short option values as string values (presence → `"1"`). */
|
|
32
40
|
opts: Record<string, string>;
|
|
41
|
+
/** Positional arguments for the leaf command, in order. */
|
|
33
42
|
args: string[];
|
|
43
|
+
/** True when the user passed `-h` / `--help` explicitly. */
|
|
34
44
|
helpExplicit: boolean;
|
|
45
|
+
/** Path segments for scoped help (empty for root help). */
|
|
35
46
|
helpPath: string[];
|
|
47
|
+
/** User-facing error message when `kind === Error`. */
|
|
36
48
|
errorMsg: string;
|
|
49
|
+
/** Help path to render next to an error (for contextual help). */
|
|
37
50
|
errorHelpPath: string[];
|
|
38
51
|
}
|
|
39
52
|
|
|
@@ -42,32 +55,41 @@ export interface ParseResult {
|
|
|
42
55
|
const helpShort = "-h";
|
|
43
56
|
const helpLong = "--help";
|
|
44
57
|
|
|
58
|
+
/** Returns true if the argv token is `-h` or `--help`. */
|
|
45
59
|
function isHelpTok(tok: string): boolean {
|
|
46
60
|
return tok === helpShort || tok === helpLong;
|
|
47
61
|
}
|
|
48
62
|
|
|
63
|
+
/** Looks up a subcommand or routing node by `key`. */
|
|
49
64
|
function findChild(cmds: CliCommand[], name: string): CliCommand | undefined {
|
|
50
65
|
return cmds.find((c) => c.key === name);
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
|
|
68
|
+
/** Resolves a long-option definition by name (without leading `--`). */
|
|
69
|
+
function findOptionByName(defs: CliOption[], name: string): CliOption | undefined {
|
|
54
70
|
return defs.find((o) => o.name === name);
|
|
55
71
|
}
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
/** Resolves a short-option definition by its single character. */
|
|
74
|
+
function findOptionDefByShort(defs: CliOption[], short: string): CliOption | undefined {
|
|
75
|
+
return defs.find((o) => o.shortName === short);
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
// ── Option Consumption ────────────────────────────────────────────────────────
|
|
62
79
|
|
|
80
|
+
/** State from scanning argv for flags: error text, lenient early exit, or `--` seen. */
|
|
63
81
|
interface ConsumeReport {
|
|
82
|
+
/** User-facing error when option parsing failed; null on success. */
|
|
64
83
|
err: string | null;
|
|
84
|
+
/** True when lenient mode stopped on an unknown option token. */
|
|
65
85
|
stoppedOnUnknown: boolean;
|
|
86
|
+
/** True when `--` was read (remaining argv is positional-only). */
|
|
66
87
|
sawDoubleDash: boolean;
|
|
67
88
|
}
|
|
68
89
|
|
|
90
|
+
/** Consumes argv from index `i` for long/short options, updating `opts` until a non-option or `--`. */
|
|
69
91
|
function consumeOptions(
|
|
70
|
-
defs:
|
|
92
|
+
defs: CliOption[],
|
|
71
93
|
lenientUnknown: boolean,
|
|
72
94
|
argv: string[],
|
|
73
95
|
i: number,
|
|
@@ -75,6 +97,7 @@ function consumeOptions(
|
|
|
75
97
|
): { report: ConsumeReport; nextIndex: number } {
|
|
76
98
|
let idx = i;
|
|
77
99
|
|
|
100
|
+
/** Parses a single `--name` or `--name=value` token. Returns an error string, `""` if unknown and lenient, or `null` on success. */
|
|
78
101
|
function consumeLong(tok: string): string | null {
|
|
79
102
|
const body = tok.slice(2);
|
|
80
103
|
let optName: string;
|
|
@@ -118,6 +141,7 @@ function consumeOptions(
|
|
|
118
141
|
return null;
|
|
119
142
|
}
|
|
120
143
|
|
|
144
|
+
/** Parses a bundled or single `-x` / `-nval` short token. */
|
|
121
145
|
function consumeShort(tok: string): string | null {
|
|
122
146
|
if (tok.length < 2) return `Unexpected option token: ${tok}`;
|
|
123
147
|
const shorts = tok.slice(1);
|
|
@@ -183,6 +207,7 @@ function consumeOptions(
|
|
|
183
207
|
|
|
184
208
|
// ── Positional Collection ─────────────────────────────────────────────────────
|
|
185
209
|
|
|
210
|
+
/** Fills `args` for a leaf from `startIdx` according to `node.positionals`. */
|
|
186
211
|
function finishLeaf(
|
|
187
212
|
node: CliCommand,
|
|
188
213
|
startIdx: number,
|
|
@@ -190,6 +215,7 @@ function finishLeaf(
|
|
|
190
215
|
path: string[],
|
|
191
216
|
opts: Record<string, string>,
|
|
192
217
|
): ParseResult {
|
|
218
|
+
/** Builds a parse error for positional consumption failures. */
|
|
193
219
|
function errorResult(msg: string): ParseResult {
|
|
194
220
|
const pr: ParseResult = {
|
|
195
221
|
kind: ParseKind.Error,
|
|
@@ -208,10 +234,9 @@ function finishLeaf(
|
|
|
208
234
|
const args: string[] = [];
|
|
209
235
|
|
|
210
236
|
for (const p of node.positionals ?? []) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (p.argMin >= 1) {
|
|
237
|
+
const { argMin = 1, argMax = 1 } = p;
|
|
238
|
+
if (argMax === 1) {
|
|
239
|
+
if (argMin >= 1) {
|
|
215
240
|
if (idx >= argv.length) {
|
|
216
241
|
return errorResult(`Missing positional argument: ${p.name}`);
|
|
217
242
|
}
|
|
@@ -225,21 +250,21 @@ function finishLeaf(
|
|
|
225
250
|
}
|
|
226
251
|
|
|
227
252
|
let count = 0;
|
|
228
|
-
if (
|
|
253
|
+
if (argMax === 0) {
|
|
229
254
|
while (idx < argv.length) {
|
|
230
255
|
args.push(argv[idx]);
|
|
231
256
|
idx += 1;
|
|
232
257
|
count += 1;
|
|
233
258
|
}
|
|
234
259
|
} else {
|
|
235
|
-
while (count <
|
|
260
|
+
while (count < argMax && idx < argv.length) {
|
|
236
261
|
args.push(argv[idx]);
|
|
237
262
|
idx += 1;
|
|
238
263
|
count += 1;
|
|
239
264
|
}
|
|
240
265
|
}
|
|
241
|
-
if (count <
|
|
242
|
-
return errorResult(`Expected at least ${
|
|
266
|
+
if (count < argMin) {
|
|
267
|
+
return errorResult(`Expected at least ${argMin} argument(s) for ${p.name}, got ${count}`);
|
|
243
268
|
}
|
|
244
269
|
}
|
|
245
270
|
|
|
@@ -252,6 +277,7 @@ function finishLeaf(
|
|
|
252
277
|
|
|
253
278
|
// ── Main Parser ───────────────────────────────────────────────────────────────
|
|
254
279
|
|
|
280
|
+
/** Builds a help-request result for the current routing path. */
|
|
255
281
|
function helpResult(p: string[], explicit: boolean): ParseResult {
|
|
256
282
|
return {
|
|
257
283
|
kind: ParseKind.Help,
|
|
@@ -265,6 +291,9 @@ function helpResult(p: string[], explicit: boolean): ParseResult {
|
|
|
265
291
|
};
|
|
266
292
|
}
|
|
267
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Parses `argv` against the program root, routing into subcommands and filling `opts` / `args`.
|
|
296
|
+
*/
|
|
268
297
|
export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
269
298
|
let i = 0;
|
|
270
299
|
const path: string[] = [];
|
|
@@ -302,7 +331,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
302
331
|
if (i >= argv.length) {
|
|
303
332
|
if (root.fallbackCommand !== undefined && ((root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOnly || (root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOrUnknown)) {
|
|
304
333
|
cmdName = root.fallbackCommand;
|
|
305
|
-
node = findChild(root.
|
|
334
|
+
node = findChild(root.commands ?? [], cmdName);
|
|
306
335
|
if (!node) {
|
|
307
336
|
return { kind: ParseKind.Error, path: [], opts: {}, args: [], helpExplicit: false, helpPath: [], errorMsg: `Unknown command: ${cmdName}`, errorHelpPath: path };
|
|
308
337
|
}
|
|
@@ -311,7 +340,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
311
340
|
}
|
|
312
341
|
} else {
|
|
313
342
|
const peek = argv[i];
|
|
314
|
-
const childPick = !forcePositionals ? findChild(root.
|
|
343
|
+
const childPick = !forcePositionals ? findChild(root.commands ?? [], peek) : undefined;
|
|
315
344
|
|
|
316
345
|
if (childPick !== undefined) {
|
|
317
346
|
cmdName = peek;
|
|
@@ -325,14 +354,14 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
325
354
|
|
|
326
355
|
if (canRouteUnknown) {
|
|
327
356
|
cmdName = root.fallbackCommand!;
|
|
328
|
-
node = findChild(root.
|
|
357
|
+
node = findChild(root.commands ?? [], cmdName);
|
|
329
358
|
if (!node) {
|
|
330
359
|
return { kind: ParseKind.Error, path: [], opts: {}, args: [], helpExplicit: false, helpPath: [], errorMsg: `Unknown command: ${cmdName}`, errorHelpPath: path };
|
|
331
360
|
}
|
|
332
361
|
} else {
|
|
333
362
|
cmdName = peek;
|
|
334
363
|
if (!forcePositionals) i += 1;
|
|
335
|
-
node = findChild(root.
|
|
364
|
+
node = findChild(root.commands ?? [], cmdName);
|
|
336
365
|
if (!node) {
|
|
337
366
|
return {
|
|
338
367
|
kind: ParseKind.Error,
|
|
@@ -379,7 +408,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
379
408
|
}
|
|
380
409
|
|
|
381
410
|
if (i >= argv.length) {
|
|
382
|
-
if ((current.
|
|
411
|
+
if ((current.commands ?? []).length > 0) {
|
|
383
412
|
return helpResult(path, false);
|
|
384
413
|
}
|
|
385
414
|
return finishLeaf(current, i, argv, path, opts);
|
|
@@ -400,7 +429,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
400
429
|
}
|
|
401
430
|
|
|
402
431
|
if (!forcePositionals) {
|
|
403
|
-
const childOpt = findChild(current.
|
|
432
|
+
const childOpt = findChild(current.commands ?? [], tok);
|
|
404
433
|
if (childOpt !== undefined) {
|
|
405
434
|
i += 1;
|
|
406
435
|
path.push(tok);
|
|
@@ -409,7 +438,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
409
438
|
}
|
|
410
439
|
}
|
|
411
440
|
|
|
412
|
-
if ((current.
|
|
441
|
+
if ((current.commands ?? []).length > 0) {
|
|
413
442
|
return {
|
|
414
443
|
kind: ParseKind.Error,
|
|
415
444
|
path,
|
|
@@ -428,11 +457,14 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
428
457
|
|
|
429
458
|
// ── Post-Parse Validation ─────────────────────────────────────────────────────
|
|
430
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Validates option keys and numeric values for an Ok parse, merging in-scope options along `pr.path`.
|
|
462
|
+
*/
|
|
431
463
|
export function postParseValidate(root: CliCommand, pr: ParseResult): ParseResult {
|
|
432
464
|
if (pr.kind !== ParseKind.Ok) return pr;
|
|
433
465
|
|
|
434
466
|
let defs = [...(root.options ?? [])];
|
|
435
|
-
let cmds = root.
|
|
467
|
+
let cmds = root.commands ?? [];
|
|
436
468
|
|
|
437
469
|
for (const seg of pr.path) {
|
|
438
470
|
const ch = findChild(cmds, seg);
|
|
@@ -449,8 +481,7 @@ export function postParseValidate(root: CliCommand, pr: ParseResult): ParseResul
|
|
|
449
481
|
};
|
|
450
482
|
}
|
|
451
483
|
defs.push(...(ch.options ?? []));
|
|
452
|
-
|
|
453
|
-
cmds = ch.children ?? [];
|
|
484
|
+
cmds = ch.commands ?? [];
|
|
454
485
|
}
|
|
455
486
|
|
|
456
487
|
for (const [k, v] of Object.entries(pr.opts)) {
|
package/src/runtime.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { cliValidateRoot } from "./validate.ts";
|
|
|
19
19
|
*/
|
|
20
20
|
function cliRootMergedWithBuiltins(root: CliCommand): CliCommand {
|
|
21
21
|
const merged = { ...root };
|
|
22
|
-
merged.
|
|
22
|
+
merged.commands = [...(root.commands ?? []), cliBuiltinCompletionGroup(root.key)];
|
|
23
23
|
return merged;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -76,7 +76,7 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
|
|
|
76
76
|
|
|
77
77
|
let current = merged;
|
|
78
78
|
for (const seg of pr.path) {
|
|
79
|
-
const ch = (current.
|
|
79
|
+
const ch = (current.commands ?? []).find((candidate: CliCommand) => candidate.key === seg);
|
|
80
80
|
if (!ch) {
|
|
81
81
|
process.stderr.write("Internal error: missing handler for path.\n");
|
|
82
82
|
process.exit(1);
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
This module defines the CLI schema, option kinds, fallback modes
|
|
2
|
+
This module defines the CLI schema, option kinds, and fallback modes.
|
|
3
3
|
It is the shared declarative model that parsing, validation, help, and completion all
|
|
4
4
|
read from, so the package has one source of truth.
|
|
5
5
|
|
|
@@ -12,8 +12,11 @@ import type { CliContext } from "./context.ts";
|
|
|
12
12
|
* Option kinds: presence (boolean flag), string (free-form text), or number (strict double).
|
|
13
13
|
*/
|
|
14
14
|
export enum CliOptionKind {
|
|
15
|
+
/** Boolean flag: no value token (may be implicit `"1"` when set). */
|
|
15
16
|
Presence = "presence",
|
|
17
|
+
/** Free-form string value. */
|
|
16
18
|
String = "string",
|
|
19
|
+
/** Strict floating-point value (parsed at validation time). */
|
|
17
20
|
Number = "number",
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -22,18 +25,25 @@ export enum CliOptionKind {
|
|
|
22
25
|
* Only the program root may set a non-default mode or a non-nil fallbackCommand.
|
|
23
26
|
*/
|
|
24
27
|
export enum CliFallbackMode {
|
|
25
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* If argv has no first subcommand, route to `fallbackCommand`; if the first token is unknown, error.
|
|
30
|
+
*/
|
|
26
31
|
MissingOnly = "missingOnly",
|
|
27
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* If argv has no first subcommand or the first token is not a known child, route to `fallbackCommand`.
|
|
34
|
+
*/
|
|
28
35
|
MissingOrUnknown = "missingOrUnknown",
|
|
29
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* If the first token is present but not a known child, route to `fallbackCommand`.
|
|
38
|
+
* When the first subcommand token is missing (empty argv), do not use fallback (implicit root help).
|
|
39
|
+
*/
|
|
30
40
|
UnknownOnly = "unknownOnly",
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
/**
|
|
34
|
-
*
|
|
44
|
+
* A named flag or value option (`--long`, `-short`), listed on `CliCommand.options`.
|
|
35
45
|
*/
|
|
36
|
-
export interface
|
|
46
|
+
export interface CliOption {
|
|
37
47
|
/** Option name (e.g., "name", "verbose"). */
|
|
38
48
|
name: string;
|
|
39
49
|
/** Description shown in help. */
|
|
@@ -42,19 +52,35 @@ export interface CliOptionDef {
|
|
|
42
52
|
kind: CliOptionKind;
|
|
43
53
|
/** Short option character (e.g., 'n' for -n). */
|
|
44
54
|
shortName?: string;
|
|
45
|
-
/** Whether this is a positional argument (true) or a flag/option (false). */
|
|
46
|
-
positional: boolean;
|
|
47
|
-
/** Minimum number of values required (for positionals). */
|
|
48
|
-
argMin: number;
|
|
49
|
-
/** Maximum number of values allowed (0 = unlimited, for positionals). */
|
|
50
|
-
argMax: number;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
/**
|
|
54
|
-
*
|
|
58
|
+
* An ordered positional argument slot, listed on `CliCommand.positionals`.
|
|
59
|
+
*/
|
|
60
|
+
export interface CliPositional {
|
|
61
|
+
/** Positional name (used in help and error messages). */
|
|
62
|
+
name: string;
|
|
63
|
+
/** Description shown in help. */
|
|
64
|
+
description: string;
|
|
65
|
+
/** Value kind for each consumed token. */
|
|
66
|
+
kind: CliOptionKind;
|
|
67
|
+
/**
|
|
68
|
+
* Minimum number of values required (default 1).
|
|
69
|
+
* Use `0` for an optional slot when paired with `argMax: 1`, or a varargs tail with `argMax: 0`.
|
|
70
|
+
*/
|
|
71
|
+
argMin?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Maximum number of values (`1` = a single required or optional word; default 1). Use `0` for an
|
|
74
|
+
* unbounded varargs tail (must be the last slot in the command’s `positionals` list).
|
|
75
|
+
*/
|
|
76
|
+
argMax?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A command node: routing group (has commands) or leaf (has handler).
|
|
55
81
|
*
|
|
56
82
|
* The value passed to cliRun is the program root: name is the app/binary name,
|
|
57
|
-
*
|
|
83
|
+
* commands are top-level subcommands, options are global flags.
|
|
58
84
|
* The root must not set handler or declare positionals (validated at startup).
|
|
59
85
|
*/
|
|
60
86
|
export interface CliCommand {
|
|
@@ -65,11 +91,11 @@ export interface CliCommand {
|
|
|
65
91
|
/** Additional notes shown in help (supports {app} placeholder). */
|
|
66
92
|
notes?: string;
|
|
67
93
|
/** Global or command-level flags/options. */
|
|
68
|
-
options?:
|
|
94
|
+
options?: CliOption[];
|
|
69
95
|
/** Positional argument definitions. */
|
|
70
|
-
positionals?:
|
|
71
|
-
/**
|
|
72
|
-
|
|
96
|
+
positionals?: CliPositional[];
|
|
97
|
+
/** Nested subcommands (empty for leaf commands). */
|
|
98
|
+
commands?: CliCommand[];
|
|
73
99
|
/** Handler function for leaf commands. */
|
|
74
100
|
handler?: CliHandler;
|
|
75
101
|
/** Default top-level subcommand when argv omits a command or uses an unknown first token (root only). */
|
|
@@ -88,27 +114,9 @@ export type CliHandler = (ctx: CliContext) => void | Promise<void>;
|
|
|
88
114
|
* Error thrown when the static CliCommand tree violates ArgsBarg rules.
|
|
89
115
|
*/
|
|
90
116
|
export class CliSchemaValidationError extends Error {
|
|
117
|
+
/** Creates a schema validation error with a human-readable rule violation. */
|
|
91
118
|
constructor(message: string) {
|
|
92
119
|
super(message);
|
|
93
120
|
this.name = "CliSchemaValidationError";
|
|
94
121
|
}
|
|
95
122
|
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Creates a new CliOptionDef with sensible defaults.
|
|
99
|
-
*/
|
|
100
|
-
export function createOption(
|
|
101
|
-
name: string,
|
|
102
|
-
description: string,
|
|
103
|
-
options?: Partial<CliOptionDef>,
|
|
104
|
-
): CliOptionDef {
|
|
105
|
-
return {
|
|
106
|
-
name,
|
|
107
|
-
description,
|
|
108
|
-
kind: options?.kind ?? CliOptionKind.Presence,
|
|
109
|
-
shortName: options?.shortName,
|
|
110
|
-
positional: options?.positional ?? false,
|
|
111
|
-
argMin: options?.argMin ?? 1,
|
|
112
|
-
argMax: options?.argMax ?? 1,
|
|
113
|
-
};
|
|
114
|
-
}
|