argsbarg 1.2.0 → 1.2.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/.github/copilot-instructions.md.md +8 -0
- package/.github/pull_request_template.md +13 -0
- package/CHANGELOG.md +9 -1
- package/CLAUDE.md +8 -0
- package/examples/nested.ts +12 -1
- package/package.json +1 -1
- package/src/index.test.ts +117 -0
- package/src/parse.ts +49 -6
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
<!-- What does this PR change and why? -->
|
|
4
|
+
|
|
5
|
+
## Changelog
|
|
6
|
+
|
|
7
|
+
**Every PR must update `CHANGELOG.md` under `## [Unreleased]`**: use `### Added`, `### Changed`, `### Fixed`, or `### Removed` as appropriate; one idea per bullet.
|
|
8
|
+
|
|
9
|
+
<!-- If you claimed no-op above, briefly say why no changelog entry is warranted. -->
|
|
10
|
+
|
|
11
|
+
## Testing
|
|
12
|
+
|
|
13
|
+
<!-- How did you verify this (local build, tests, manual run, etc.)? -->
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.1] - 2026-06-18
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Trailing options** — when a leaf command has only bounded positionals (`argMax !== 0`), options may appear after positional arguments (e.g. `cmd ./file --verbose`). Commands with a varargs tail (`argMax: 0`) keep the previous behavior.
|
|
15
|
+
- **`examples/nested.ts`** — `stat` accepts `--json`; `stat owner lookup` prints JSON when the flag is set.
|
|
16
|
+
|
|
10
17
|
## [1.2.0] - 2026-04-24
|
|
11
18
|
|
|
12
19
|
### Added
|
|
@@ -64,7 +71,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
64
71
|
- Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
|
|
65
72
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
66
73
|
|
|
67
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.2.
|
|
74
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.2.1...HEAD
|
|
75
|
+
[1.2.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.2.1
|
|
68
76
|
[1.2.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.2.0
|
|
69
77
|
[1.1.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.1.1
|
|
70
78
|
[1.1.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.1.0
|
package/CLAUDE.md
ADDED
package/examples/nested.ts
CHANGED
|
@@ -16,6 +16,13 @@ const cli: CliCommand = {
|
|
|
16
16
|
{
|
|
17
17
|
key: "stat",
|
|
18
18
|
description: "File metadata.",
|
|
19
|
+
options: [
|
|
20
|
+
{
|
|
21
|
+
name: "json",
|
|
22
|
+
description: "Emit handler output as JSON.",
|
|
23
|
+
kind: CliOptionKind.Presence,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
19
26
|
commands: [
|
|
20
27
|
{
|
|
21
28
|
key: "owner",
|
|
@@ -46,7 +53,11 @@ const cli: CliCommand = {
|
|
|
46
53
|
console.error("Missing path.");
|
|
47
54
|
process.exit(1);
|
|
48
55
|
}
|
|
49
|
-
|
|
56
|
+
if (ctx.hasFlag("json")) {
|
|
57
|
+
console.log(JSON.stringify({ user, path }));
|
|
58
|
+
} else {
|
|
59
|
+
console.log(`lookup user=${user} path=${path}`);
|
|
60
|
+
}
|
|
50
61
|
},
|
|
51
62
|
},
|
|
52
63
|
],
|
package/package.json
CHANGED
package/src/index.test.ts
CHANGED
|
@@ -238,6 +238,123 @@ test("completion scripts keep dotted app names in registration names", () => {
|
|
|
238
238
|
expect(zsh).toContain("compdef _minimal_ts minimal.ts");
|
|
239
239
|
});
|
|
240
240
|
|
|
241
|
+
test("trailing options after bounded positionals", () => {
|
|
242
|
+
const root: CliCommand = {
|
|
243
|
+
key: "app",
|
|
244
|
+
description: "",
|
|
245
|
+
commands: [
|
|
246
|
+
{
|
|
247
|
+
key: "x",
|
|
248
|
+
description: "cmd",
|
|
249
|
+
options: [
|
|
250
|
+
{
|
|
251
|
+
name: "verbose",
|
|
252
|
+
description: "",
|
|
253
|
+
kind: CliOptionKind.Presence,
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
positionals: [
|
|
257
|
+
{
|
|
258
|
+
name: "path",
|
|
259
|
+
description: "",
|
|
260
|
+
kind: CliOptionKind.String,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
handler: () => {},
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
};
|
|
267
|
+
cliValidateRoot(root);
|
|
268
|
+
const pr = postParseValidate(root, parse(root, ["x", "./file", "--verbose"]));
|
|
269
|
+
expect(pr.kind).toBe(ParseKind.Ok);
|
|
270
|
+
expect(pr.args).toEqual(["./file"]);
|
|
271
|
+
expect(pr.opts["verbose"]).toBe("1");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("trailing options include parent-scoped flags", () => {
|
|
275
|
+
const root: CliCommand = {
|
|
276
|
+
key: "app",
|
|
277
|
+
description: "",
|
|
278
|
+
commands: [
|
|
279
|
+
{
|
|
280
|
+
key: "group",
|
|
281
|
+
description: "group",
|
|
282
|
+
options: [
|
|
283
|
+
{
|
|
284
|
+
name: "json",
|
|
285
|
+
description: "",
|
|
286
|
+
kind: CliOptionKind.Presence,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
commands: [
|
|
290
|
+
{
|
|
291
|
+
key: "leaf",
|
|
292
|
+
description: "leaf",
|
|
293
|
+
options: [
|
|
294
|
+
{
|
|
295
|
+
name: "user",
|
|
296
|
+
description: "",
|
|
297
|
+
kind: CliOptionKind.String,
|
|
298
|
+
shortName: "u",
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
positionals: [
|
|
302
|
+
{
|
|
303
|
+
name: "path",
|
|
304
|
+
description: "",
|
|
305
|
+
kind: CliOptionKind.String,
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
handler: () => {},
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
cliValidateRoot(root);
|
|
315
|
+
const pr = postParseValidate(root, parse(root, ["group", "leaf", "-u", "alice", "./file", "--json"]));
|
|
316
|
+
expect(pr.kind).toBe(ParseKind.Ok);
|
|
317
|
+
expect(pr.path).toEqual(["group", "leaf"]);
|
|
318
|
+
expect(pr.args).toEqual(["./file"]);
|
|
319
|
+
expect(pr.opts["user"]).toBe("alice");
|
|
320
|
+
expect(pr.opts["json"]).toBe("1");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("varargs tail does not parse trailing options", () => {
|
|
324
|
+
const root: CliCommand = {
|
|
325
|
+
key: "app",
|
|
326
|
+
description: "",
|
|
327
|
+
commands: [
|
|
328
|
+
{
|
|
329
|
+
key: "x",
|
|
330
|
+
description: "cmd",
|
|
331
|
+
options: [
|
|
332
|
+
{
|
|
333
|
+
name: "json",
|
|
334
|
+
description: "",
|
|
335
|
+
kind: CliOptionKind.Presence,
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
positionals: [
|
|
339
|
+
{
|
|
340
|
+
name: "files",
|
|
341
|
+
description: "",
|
|
342
|
+
kind: CliOptionKind.String,
|
|
343
|
+
argMin: 0,
|
|
344
|
+
argMax: 0,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
handler: () => {},
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
cliValidateRoot(root);
|
|
352
|
+
const pr = postParseValidate(root, parse(root, ["x", "./file", "--json"]));
|
|
353
|
+
expect(pr.kind).toBe(ParseKind.Ok);
|
|
354
|
+
expect(pr.args).toEqual(["./file", "--json"]);
|
|
355
|
+
expect(pr.opts["json"]).toBeUndefined();
|
|
356
|
+
});
|
|
357
|
+
|
|
241
358
|
test("stops parsing options at --", () => {
|
|
242
359
|
const root: CliCommand = {
|
|
243
360
|
key: "app",
|
package/src/parse.ts
CHANGED
|
@@ -207,6 +207,26 @@ function consumeOptions(
|
|
|
207
207
|
|
|
208
208
|
// ── Positional Collection ─────────────────────────────────────────────────────
|
|
209
209
|
|
|
210
|
+
/** Merges option defs from the program root along the routed command path. */
|
|
211
|
+
function collectOptionDefs(root: CliCommand, path: string[]): CliOption[] {
|
|
212
|
+
let defs = [...(root.options ?? [])];
|
|
213
|
+
let cmds = root.commands ?? [];
|
|
214
|
+
|
|
215
|
+
for (const seg of path) {
|
|
216
|
+
const ch = findChild(cmds, seg);
|
|
217
|
+
if (!ch) break;
|
|
218
|
+
defs.push(...(ch.options ?? []));
|
|
219
|
+
cmds = ch.commands ?? [];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return defs;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** True when every positional slot has bounded arity (no `argMax: 0` varargs tail). */
|
|
226
|
+
function allowsTrailingOptions(positionals: CliCommand["positionals"]): boolean {
|
|
227
|
+
return (positionals ?? []).every((p) => (p.argMax ?? 1) !== 0);
|
|
228
|
+
}
|
|
229
|
+
|
|
210
230
|
/** Fills `args` for a leaf from `startIdx` according to `node.positionals`. */
|
|
211
231
|
function finishLeaf(
|
|
212
232
|
node: CliCommand,
|
|
@@ -214,6 +234,8 @@ function finishLeaf(
|
|
|
214
234
|
argv: string[],
|
|
215
235
|
path: string[],
|
|
216
236
|
opts: Record<string, string>,
|
|
237
|
+
optionDefs: CliOption[],
|
|
238
|
+
forcePositionals: boolean,
|
|
217
239
|
): ParseResult {
|
|
218
240
|
/** Builds a parse error for positional consumption failures. */
|
|
219
241
|
function errorResult(msg: string): ParseResult {
|
|
@@ -243,8 +265,13 @@ function finishLeaf(
|
|
|
243
265
|
args.push(argv[idx]);
|
|
244
266
|
idx += 1;
|
|
245
267
|
} else if (idx < argv.length) {
|
|
246
|
-
|
|
247
|
-
|
|
268
|
+
const tok = argv[idx];
|
|
269
|
+
if (argMin < 1 && tok.startsWith("-")) {
|
|
270
|
+
// Optional slot: leave `-` tokens for trailing option parsing.
|
|
271
|
+
} else {
|
|
272
|
+
args.push(tok);
|
|
273
|
+
idx += 1;
|
|
274
|
+
}
|
|
248
275
|
}
|
|
249
276
|
continue;
|
|
250
277
|
}
|
|
@@ -269,7 +296,23 @@ function finishLeaf(
|
|
|
269
296
|
}
|
|
270
297
|
|
|
271
298
|
if (idx < argv.length) {
|
|
272
|
-
|
|
299
|
+
if (forcePositionals || !allowsTrailingOptions(node.positionals)) {
|
|
300
|
+
return errorResult("Unexpected extra arguments");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (isHelpTok(argv[idx])) {
|
|
304
|
+
return helpResult(path, true);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const tailRep = consumeOptions(optionDefs, false, argv, idx, opts);
|
|
308
|
+
if (tailRep.report.err) {
|
|
309
|
+
return errorResult(tailRep.report.err);
|
|
310
|
+
}
|
|
311
|
+
idx = tailRep.nextIndex;
|
|
312
|
+
|
|
313
|
+
if (idx < argv.length) {
|
|
314
|
+
return errorResult("Unexpected extra arguments");
|
|
315
|
+
}
|
|
273
316
|
}
|
|
274
317
|
|
|
275
318
|
return { kind: ParseKind.Ok, path, opts, args, helpExplicit: false, helpPath: [], errorMsg: "", errorHelpPath: [] };
|
|
@@ -329,7 +372,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
329
372
|
let node: CliCommand | undefined;
|
|
330
373
|
|
|
331
374
|
if (root.handler) {
|
|
332
|
-
return finishLeaf(root as
|
|
375
|
+
return finishLeaf(root as CliCommand, i, argv, path, opts, root.options ?? [], forcePositionals);
|
|
333
376
|
}
|
|
334
377
|
|
|
335
378
|
if (i >= argv.length) {
|
|
@@ -415,7 +458,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
415
458
|
if ((current.commands ?? []).length > 0) {
|
|
416
459
|
return helpResult(path, false);
|
|
417
460
|
}
|
|
418
|
-
return finishLeaf(current, i, argv, path, opts);
|
|
461
|
+
return finishLeaf(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals);
|
|
419
462
|
}
|
|
420
463
|
|
|
421
464
|
const tok = argv[i];
|
|
@@ -455,7 +498,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
455
498
|
};
|
|
456
499
|
}
|
|
457
500
|
|
|
458
|
-
return finishLeaf(current, i, argv, path, opts);
|
|
501
|
+
return finishLeaf(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals);
|
|
459
502
|
}
|
|
460
503
|
}
|
|
461
504
|
|