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.
@@ -0,0 +1,8 @@
1
+ ---
2
+ description: Project context for AI agents
3
+ ---
4
+
5
+ Always include in context before answering or making changes in this repository:
6
+
7
+ - `README.md`
8
+ - `.cursor/rules/*`
@@ -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.0...HEAD
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
@@ -0,0 +1,8 @@
1
+ ---
2
+ description: Project context for AI agents
3
+ ---
4
+
5
+ Always include in context before answering or making changes in this repository:
6
+
7
+ - `./README.md`
8
+ - .cursor/rules/*
@@ -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
- console.log(`lookup user=${user} path=${path}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "//just": "echo this app uses justfile for development tasks"
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
- args.push(argv[idx]);
247
- idx += 1;
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
- return errorResult("Unexpected extra arguments");
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 any, i, argv, path, opts);
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