argsbarg 0.1.0 → 1.0.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/.cursor/rules/code.mdc +1 -0
- package/CHANGELOG.md +35 -0
- package/README.md +36 -24
- package/examples/minimal.ts +12 -7
- package/examples/nested.ts +18 -12
- package/justfile +32 -0
- package/package.json +6 -11
- package/plan.md +2 -2
- package/scripts/release.ts +154 -0
- package/src/completion.ts +33 -14
- package/src/context.ts +2 -1
- package/src/help.ts +57 -23
- package/src/index.test.ts +79 -35
- package/src/index.ts +3 -14
- package/src/parse.ts +48 -18
- package/src/runtime.ts +2 -2
- package/src/types.ts +37 -35
- package/src/validate.ts +7 -6
- 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,7 +10,7 @@ across every entry path.
|
|
|
10
10
|
import { CliContext } from "./context.ts";
|
|
11
11
|
import {
|
|
12
12
|
CliCommand,
|
|
13
|
-
|
|
13
|
+
CliOption,
|
|
14
14
|
CliOptionKind,
|
|
15
15
|
CliFallbackMode,
|
|
16
16
|
} from "./types.ts";
|
|
@@ -18,22 +18,35 @@ 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,8 +234,6 @@ function finishLeaf(
|
|
|
208
234
|
const args: string[] = [];
|
|
209
235
|
|
|
210
236
|
for (const p of node.positionals ?? []) {
|
|
211
|
-
if (!p.positional) continue;
|
|
212
|
-
|
|
213
237
|
if (p.argMax === 1) {
|
|
214
238
|
if (p.argMin >= 1) {
|
|
215
239
|
if (idx >= argv.length) {
|
|
@@ -252,6 +276,7 @@ function finishLeaf(
|
|
|
252
276
|
|
|
253
277
|
// ── Main Parser ───────────────────────────────────────────────────────────────
|
|
254
278
|
|
|
279
|
+
/** Builds a help-request result for the current routing path. */
|
|
255
280
|
function helpResult(p: string[], explicit: boolean): ParseResult {
|
|
256
281
|
return {
|
|
257
282
|
kind: ParseKind.Help,
|
|
@@ -265,6 +290,9 @@ function helpResult(p: string[], explicit: boolean): ParseResult {
|
|
|
265
290
|
};
|
|
266
291
|
}
|
|
267
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Parses `argv` against the program root, routing into subcommands and filling `opts` / `args`.
|
|
295
|
+
*/
|
|
268
296
|
export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
269
297
|
let i = 0;
|
|
270
298
|
const path: string[] = [];
|
|
@@ -302,7 +330,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
302
330
|
if (i >= argv.length) {
|
|
303
331
|
if (root.fallbackCommand !== undefined && ((root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOnly || (root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOrUnknown)) {
|
|
304
332
|
cmdName = root.fallbackCommand;
|
|
305
|
-
node = findChild(root.
|
|
333
|
+
node = findChild(root.commands ?? [], cmdName);
|
|
306
334
|
if (!node) {
|
|
307
335
|
return { kind: ParseKind.Error, path: [], opts: {}, args: [], helpExplicit: false, helpPath: [], errorMsg: `Unknown command: ${cmdName}`, errorHelpPath: path };
|
|
308
336
|
}
|
|
@@ -311,7 +339,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
311
339
|
}
|
|
312
340
|
} else {
|
|
313
341
|
const peek = argv[i];
|
|
314
|
-
const childPick = !forcePositionals ? findChild(root.
|
|
342
|
+
const childPick = !forcePositionals ? findChild(root.commands ?? [], peek) : undefined;
|
|
315
343
|
|
|
316
344
|
if (childPick !== undefined) {
|
|
317
345
|
cmdName = peek;
|
|
@@ -325,14 +353,14 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
325
353
|
|
|
326
354
|
if (canRouteUnknown) {
|
|
327
355
|
cmdName = root.fallbackCommand!;
|
|
328
|
-
node = findChild(root.
|
|
356
|
+
node = findChild(root.commands ?? [], cmdName);
|
|
329
357
|
if (!node) {
|
|
330
358
|
return { kind: ParseKind.Error, path: [], opts: {}, args: [], helpExplicit: false, helpPath: [], errorMsg: `Unknown command: ${cmdName}`, errorHelpPath: path };
|
|
331
359
|
}
|
|
332
360
|
} else {
|
|
333
361
|
cmdName = peek;
|
|
334
362
|
if (!forcePositionals) i += 1;
|
|
335
|
-
node = findChild(root.
|
|
363
|
+
node = findChild(root.commands ?? [], cmdName);
|
|
336
364
|
if (!node) {
|
|
337
365
|
return {
|
|
338
366
|
kind: ParseKind.Error,
|
|
@@ -379,7 +407,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
379
407
|
}
|
|
380
408
|
|
|
381
409
|
if (i >= argv.length) {
|
|
382
|
-
if ((current.
|
|
410
|
+
if ((current.commands ?? []).length > 0) {
|
|
383
411
|
return helpResult(path, false);
|
|
384
412
|
}
|
|
385
413
|
return finishLeaf(current, i, argv, path, opts);
|
|
@@ -400,7 +428,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
400
428
|
}
|
|
401
429
|
|
|
402
430
|
if (!forcePositionals) {
|
|
403
|
-
const childOpt = findChild(current.
|
|
431
|
+
const childOpt = findChild(current.commands ?? [], tok);
|
|
404
432
|
if (childOpt !== undefined) {
|
|
405
433
|
i += 1;
|
|
406
434
|
path.push(tok);
|
|
@@ -409,7 +437,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
409
437
|
}
|
|
410
438
|
}
|
|
411
439
|
|
|
412
|
-
if ((current.
|
|
440
|
+
if ((current.commands ?? []).length > 0) {
|
|
413
441
|
return {
|
|
414
442
|
kind: ParseKind.Error,
|
|
415
443
|
path,
|
|
@@ -428,11 +456,14 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
428
456
|
|
|
429
457
|
// ── Post-Parse Validation ─────────────────────────────────────────────────────
|
|
430
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Validates option keys and numeric values for an Ok parse, merging in-scope options along `pr.path`.
|
|
461
|
+
*/
|
|
431
462
|
export function postParseValidate(root: CliCommand, pr: ParseResult): ParseResult {
|
|
432
463
|
if (pr.kind !== ParseKind.Ok) return pr;
|
|
433
464
|
|
|
434
465
|
let defs = [...(root.options ?? [])];
|
|
435
|
-
let cmds = root.
|
|
466
|
+
let cmds = root.commands ?? [];
|
|
436
467
|
|
|
437
468
|
for (const seg of pr.path) {
|
|
438
469
|
const ch = findChild(cmds, seg);
|
|
@@ -449,8 +480,7 @@ export function postParseValidate(root: CliCommand, pr: ParseResult): ParseResul
|
|
|
449
480
|
};
|
|
450
481
|
}
|
|
451
482
|
defs.push(...(ch.options ?? []));
|
|
452
|
-
|
|
453
|
-
cmds = ch.children ?? [];
|
|
483
|
+
cmds = ch.commands ?? [];
|
|
454
484
|
}
|
|
455
485
|
|
|
456
486
|
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,29 @@ export interface CliOptionDef {
|
|
|
42
52
|
kind: CliOptionKind;
|
|
43
53
|
/** Short option character (e.g., 'n' for -n). */
|
|
44
54
|
shortName?: string;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
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
|
+
/** Minimum number of values required. */
|
|
48
68
|
argMin: number;
|
|
49
|
-
/** Maximum number of values
|
|
69
|
+
/** Maximum number of values (0 = unlimited, for a varargs tail). */
|
|
50
70
|
argMax: number;
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
/**
|
|
54
|
-
* A command node: routing group (has
|
|
74
|
+
* A command node: routing group (has commands) or leaf (has handler).
|
|
55
75
|
*
|
|
56
76
|
* The value passed to cliRun is the program root: name is the app/binary name,
|
|
57
|
-
*
|
|
77
|
+
* commands are top-level subcommands, options are global flags.
|
|
58
78
|
* The root must not set handler or declare positionals (validated at startup).
|
|
59
79
|
*/
|
|
60
80
|
export interface CliCommand {
|
|
@@ -65,11 +85,11 @@ export interface CliCommand {
|
|
|
65
85
|
/** Additional notes shown in help (supports {app} placeholder). */
|
|
66
86
|
notes?: string;
|
|
67
87
|
/** Global or command-level flags/options. */
|
|
68
|
-
options?:
|
|
88
|
+
options?: CliOption[];
|
|
69
89
|
/** Positional argument definitions. */
|
|
70
|
-
positionals?:
|
|
71
|
-
/**
|
|
72
|
-
|
|
90
|
+
positionals?: CliPositional[];
|
|
91
|
+
/** Nested subcommands (empty for leaf commands). */
|
|
92
|
+
commands?: CliCommand[];
|
|
73
93
|
/** Handler function for leaf commands. */
|
|
74
94
|
handler?: CliHandler;
|
|
75
95
|
/** Default top-level subcommand when argv omits a command or uses an unknown first token (root only). */
|
|
@@ -88,27 +108,9 @@ export type CliHandler = (ctx: CliContext) => void | Promise<void>;
|
|
|
88
108
|
* Error thrown when the static CliCommand tree violates ArgsBarg rules.
|
|
89
109
|
*/
|
|
90
110
|
export class CliSchemaValidationError extends Error {
|
|
111
|
+
/** Creates a schema validation error with a human-readable rule violation. */
|
|
91
112
|
constructor(message: string) {
|
|
92
113
|
super(message);
|
|
93
114
|
this.name = "CliSchemaValidationError";
|
|
94
115
|
}
|
|
95
116
|
}
|
|
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
|
-
}
|