argsbarg 7.0.4 → 7.0.5

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/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [7.0.5] - 2026-09-15
11
+
12
+ ### Added
13
+
14
+ - **Interleaved options between positionals** — options (presence flags and value options) can now be placed anywhere between bounded or optional positional arguments (e.g. `cmd file1 --force file2`), matching varargs tail behavior rather than requiring all options to precede or follow all bounded positionals.
15
+
10
16
  ## [7.0.4] - 2026-08-17
11
17
 
12
18
  ### Changed
@@ -959,7 +965,8 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
959
965
  - 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`).
960
966
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
961
967
 
962
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v7.0.4...HEAD
968
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v7.0.5...HEAD
969
+ [7.0.5]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.5
963
970
  [7.0.4]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.4
964
971
  [7.0.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.3
965
972
  [7.0.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "7.0.4",
3
+ "version": "7.0.5",
4
4
  "main": "./src/index.ts",
5
5
  "module": "./src/index.ts",
6
6
  "dependencies": {
@@ -370,6 +370,184 @@ test("trailing options after bounded positionals", () => {
370
370
  expect(pr.opts.verbose).toBe("1");
371
371
  });
372
372
 
373
+ /** Tests that options can be interleaved between bounded positionals. */
374
+ test("options interleaved between bounded positionals", () => {
375
+ const root = testProgram({
376
+ key: "app",
377
+ description: "",
378
+ commands: [
379
+ {
380
+ key: "copy",
381
+ description: "copy",
382
+ options: [
383
+ {
384
+ name: "force",
385
+ description: "",
386
+ kind: CliOptionKind.Presence,
387
+ shortName: "f",
388
+ },
389
+ {
390
+ name: "mode",
391
+ description: "",
392
+ kind: CliOptionKind.String,
393
+ },
394
+ ],
395
+ positionals: [
396
+ {
397
+ name: "src",
398
+ description: "",
399
+ kind: CliOptionKind.String,
400
+ },
401
+ {
402
+ name: "dest",
403
+ description: "",
404
+ kind: CliOptionKind.String,
405
+ },
406
+ ],
407
+ handler: () => {},
408
+ },
409
+ ],
410
+ });
411
+ cliValidateProgram(root);
412
+
413
+ // Presence flag interleaved between positionals
414
+ const prPresence = postParseValidate(root, parse(root, ["copy", "file1", "--force", "file2"]));
415
+ expect(prPresence.kind).toBe(ParseKind.Ok);
416
+ expect(prPresence.args).toEqual(["file1", "file2"]);
417
+ expect(prPresence.opts.force).toBe("1");
418
+
419
+ // String option with value interleaved between positionals
420
+ const prString = postParseValidate(root, parse(root, ["copy", "file1", "--mode", "fast", "file2"]));
421
+ expect(prString.kind).toBe(ParseKind.Ok);
422
+ expect(prString.args).toEqual(["file1", "file2"]);
423
+ expect(prString.opts.mode).toBe("fast");
424
+
425
+ // Multiple flags interleaved between positionals
426
+ const prMulti = postParseValidate(root, parse(root, ["copy", "file1", "--mode", "fast", "-f", "file2"]));
427
+ expect(prMulti.kind).toBe(ParseKind.Ok);
428
+ expect(prMulti.args).toEqual(["file1", "file2"]);
429
+ expect(prMulti.opts.mode).toBe("fast");
430
+ expect(prMulti.opts.force).toBe("1");
431
+
432
+ // Unknown option interleaved between positionals returns error
433
+ const prUnknown = postParseValidate(root, parse(root, ["copy", "file1", "--unknown", "file2"]));
434
+ expect(prUnknown.kind).toBe(ParseKind.Error);
435
+ expect(prUnknown.errorMsg).toContain("Unknown option: --unknown");
436
+
437
+ // Interleaved help request triggers contextual help
438
+ const prHelp = parse(root, ["copy", "file1", "-h"]);
439
+ expect(prHelp.kind).toBe(ParseKind.Help);
440
+ expect(prHelp.helpPath).toEqual(["copy"]);
441
+
442
+ // Double dash between positionals disables option consumption
443
+ const prDoubleDash = postParseValidate(root, parse(root, ["copy", "file1", "--", "--force"]));
444
+ expect(prDoubleDash.kind).toBe(ParseKind.Ok);
445
+ expect(prDoubleDash.args).toEqual(["file1", "--force"]);
446
+ expect(prDoubleDash.opts.force).toBeUndefined();
447
+ });
448
+
449
+ /** Tests that options can be interleaved with optional positionals. */
450
+ test("options interleaved with optional positionals", () => {
451
+ const root = testProgram({
452
+ key: "app",
453
+ description: "",
454
+ commands: [
455
+ {
456
+ key: "deploy",
457
+ description: "deploy",
458
+ options: [
459
+ {
460
+ name: "force",
461
+ description: "",
462
+ kind: CliOptionKind.Presence,
463
+ },
464
+ ],
465
+ positionals: [
466
+ {
467
+ name: "env",
468
+ description: "",
469
+ kind: CliOptionKind.String,
470
+ argMin: 0,
471
+ argMax: 1,
472
+ },
473
+ {
474
+ name: "target",
475
+ description: "",
476
+ kind: CliOptionKind.String,
477
+ argMin: 0,
478
+ argMax: 1,
479
+ },
480
+ ],
481
+ handler: () => {},
482
+ },
483
+ ],
484
+ });
485
+ cliValidateProgram(root);
486
+
487
+ // Interleaved between two optional positionals
488
+ const prBoth = postParseValidate(root, parse(root, ["deploy", "prod", "--force", "us-east"]));
489
+ expect(prBoth.kind).toBe(ParseKind.Ok);
490
+ expect(prBoth.args).toEqual(["prod", "us-east"]);
491
+ expect(prBoth.opts.force).toBe("1");
492
+
493
+ // Option after first optional positional when second is omitted
494
+ const prOne = postParseValidate(root, parse(root, ["deploy", "prod", "--force"]));
495
+ expect(prOne.kind).toBe(ParseKind.Ok);
496
+ expect(prOne.args).toEqual(["prod"]);
497
+ expect(prOne.opts.force).toBe("1");
498
+
499
+ // Option before optional positionals when all are omitted
500
+ const prNone = postParseValidate(root, parse(root, ["deploy", "--force"]));
501
+ expect(prNone.kind).toBe(ParseKind.Ok);
502
+ expect(prNone.args).toEqual([]);
503
+ expect(prNone.opts.force).toBe("1");
504
+ });
505
+
506
+ /** Tests that options can be interleaved between bounded positional and varargs tail. */
507
+ test("options interleaved between bounded positional and varargs tail", () => {
508
+ const root = testProgram({
509
+ key: "app",
510
+ description: "",
511
+ commands: [
512
+ {
513
+ key: "upload",
514
+ description: "upload",
515
+ options: [
516
+ {
517
+ name: "json",
518
+ description: "",
519
+ kind: CliOptionKind.Presence,
520
+ },
521
+ ],
522
+ positionals: [
523
+ {
524
+ name: "target",
525
+ description: "",
526
+ kind: CliOptionKind.String,
527
+ argMin: 1,
528
+ argMax: 1,
529
+ },
530
+ {
531
+ name: "files",
532
+ description: "",
533
+ kind: CliOptionKind.String,
534
+ argMin: 1,
535
+ argMax: 0,
536
+ },
537
+ ],
538
+ handler: () => {},
539
+ },
540
+ ],
541
+ });
542
+ cliValidateProgram(root);
543
+
544
+ // Flag between target and files does not get captured as first file
545
+ const pr = postParseValidate(root, parse(root, ["upload", "s3", "--json", "a.txt", "b.txt"]));
546
+ expect(pr.kind).toBe(ParseKind.Ok);
547
+ expect(pr.args).toEqual(["s3", "a.txt", "b.txt"]);
548
+ expect(pr.opts.json).toBe("1");
549
+ });
550
+
373
551
  /** Tests that options on routing groups are rejected at schema validation. */
374
552
  test("rejects options on routing groups", () => {
375
553
  const root = testProgram({
package/src/core/parse.ts CHANGED
@@ -351,9 +351,47 @@ function finishLeaf(
351
351
  const args: string[] = [];
352
352
  let forcePositionals = forcePositionalsIn;
353
353
 
354
+ /**
355
+ * Consumes any pending options, `--`, or help flags at the current argv index.
356
+ * Sets `forcePositionals = true` when `--` is encountered.
357
+ * Returns a help or error ParseResult if parsing halts, or null to continue positional consumption.
358
+ */
359
+ function consumePendingOptions(): ParseResult | null {
360
+ while (!forcePositionals && idx < argv.length) {
361
+ const tok = argv[idx];
362
+ if (tok === "--") {
363
+ forcePositionals = true;
364
+ idx += 1;
365
+ break;
366
+ }
367
+ if (isHelpTok(tok)) {
368
+ return helpResult(path, true, pathParams);
369
+ }
370
+ if (tok.startsWith("-")) {
371
+ const rep = consumeOptions(optionDefs, false, argv, idx, opts);
372
+ if (rep.report.err) {
373
+ return errorResult(rep.report.err, path, [], pathParams);
374
+ }
375
+ if (rep.report.sawDoubleDash) {
376
+ forcePositionals = true;
377
+ }
378
+ if (rep.nextIndex > idx) {
379
+ idx = rep.nextIndex;
380
+ continue;
381
+ }
382
+ return errorResult(`Unexpected option token: ${tok}`, path, [], pathParams);
383
+ }
384
+ break;
385
+ }
386
+ return null;
387
+ }
388
+
354
389
  for (const p of node.positionals ?? []) {
355
390
  const { argMin = 1, argMax = 1 } = p;
356
391
  if (argMax === 1) {
392
+ const pendingErr = consumePendingOptions();
393
+ if (pendingErr) return pendingErr;
394
+
357
395
  if (argMin >= 1) {
358
396
  if (idx >= argv.length) {
359
397
  return errorResult(`Missing positional argument: ${p.name}`, path, [], pathParams);
@@ -361,13 +399,8 @@ function finishLeaf(
361
399
  args.push(argv[idx]);
362
400
  idx += 1;
363
401
  } else if (idx < argv.length) {
364
- const tok = argv[idx];
365
- if (argMin < 1 && tok.startsWith("-")) {
366
- // Optional slot: leave `-` tokens for trailing option parsing.
367
- } else {
368
- args.push(tok);
369
- idx += 1;
370
- }
402
+ args.push(argv[idx]);
403
+ idx += 1;
371
404
  }
372
405
  continue;
373
406
  }
@@ -375,40 +408,20 @@ function finishLeaf(
375
408
  let count = 0;
376
409
  if (argMax === 0) {
377
410
  while (idx < argv.length) {
378
- const tok = argv[idx];
379
-
380
- if (!forcePositionals && tok === "--") {
381
- forcePositionals = true;
382
- idx++;
383
- continue;
384
- }
411
+ const pendingErr = consumePendingOptions();
412
+ if (pendingErr) return pendingErr;
413
+ if (idx >= argv.length) break;
385
414
 
386
- if (!forcePositionals && isHelpTok(tok)) {
387
- return helpResult(path, true, pathParams);
388
- }
389
-
390
- if (!forcePositionals && tok.startsWith("-")) {
391
- // MUST be false — lenient mode swallows unknown flags as positionals silently
392
- const tailRep = consumeOptions(optionDefs, false, argv, idx, opts);
393
- if (tailRep.report.err) {
394
- return errorResult(tailRep.report.err, path, [], pathParams);
395
- }
396
- if (tailRep.report.sawDoubleDash) {
397
- forcePositionals = true;
398
- }
399
- if (tailRep.nextIndex > idx) {
400
- idx = tailRep.nextIndex;
401
- continue;
402
- }
403
- return errorResult(`Unexpected option token: ${tok}`, path, [], pathParams);
404
- }
405
-
406
- args.push(tok);
407
- idx++;
408
- count++;
415
+ args.push(argv[idx]);
416
+ idx += 1;
417
+ count += 1;
409
418
  }
410
419
  } else {
411
420
  while (count < argMax && idx < argv.length) {
421
+ const pendingErr = consumePendingOptions();
422
+ if (pendingErr) return pendingErr;
423
+ if (idx >= argv.length) break;
424
+
412
425
  args.push(argv[idx]);
413
426
  idx += 1;
414
427
  count += 1;
@@ -419,24 +432,11 @@ function finishLeaf(
419
432
  }
420
433
  }
421
434
 
422
- if (idx < argv.length) {
423
- if (forcePositionals) {
424
- return errorResult("Unexpected extra arguments", path, [], pathParams);
425
- }
426
-
427
- if (isHelpTok(argv[idx])) {
428
- return helpResult(path, true, pathParams);
429
- }
435
+ const trailingErr = consumePendingOptions();
436
+ if (trailingErr) return trailingErr;
430
437
 
431
- const tailRep = consumeOptions(optionDefs, false, argv, idx, opts);
432
- if (tailRep.report.err) {
433
- return errorResult(tailRep.report.err, path, [], pathParams);
434
- }
435
- idx = tailRep.nextIndex;
436
-
437
- if (idx < argv.length) {
438
- return errorResult("Unexpected extra arguments", path, [], pathParams);
439
- }
438
+ if (idx < argv.length) {
439
+ return errorResult("Unexpected extra arguments", path, [], pathParams);
440
440
  }
441
441
 
442
442
  return {