argsbarg 1.0.0 → 1.1.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.
@@ -3,8 +3,7 @@ description: Code quality and style rules for TypeScript files
3
3
  globs: **/*.ts
4
4
  ---
5
5
 
6
- # TypeScript Coding Standards
7
-
6
+ - Changes must be summarized in CHANGELOG.md under the UNRELEASED section
8
7
  - All imports must be ordered alphabetically by their source module path (the `from` clause).
9
8
  - Explicit exports using the `export { ... } from "..."` or `export type { ... } from "..."` syntax must be ordered alphabetically by their source module path and placed at the top of the file, immediately below the imports.
10
9
  - Types, Interfaces, Functions must have a docstring
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.0] - 2026-04-23
11
+
12
+
13
+ ## [1.0.1] - 2026-04-22
14
+
15
+ ### Changed
16
+
17
+ - **`CliPositional`** — `argMin` and `argMax` are optional. When omitted, they behave as `argMin: 1` and `argMax: 1` (one required word). Set `argMax: 0` for an unbounded varargs tail.
18
+ - **Release** — `just release <major|minor|patch>` runs `just test` first, then `scripts/release.ts`, which no longer runs typecheck, lint, or tests itself. The release commit uses `git add -A` so all local changes in the repo are included, not only `package.json` and `CHANGELOG.md`.
19
+
10
20
  ## [1.0.0] - 2026-04-22
11
21
 
12
22
  ### Added
@@ -33,3 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
43
  - 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`).
34
44
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
35
45
 
46
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.1.0...HEAD
47
+ [1.1.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.1.0
48
+ [1.0.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.0.1
49
+ [1.0.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.0.0
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
- ![Logo](logo.png)
1
+ ![Logo](https://github.com/bdombro/bun-argsbarg/blob/main/logo.png)
2
2
  <!-- Big money NE - https://patorjk.com/software/taag/#p=testall&f=Bulbhead&t=shebangsy&x=none&v=4&h=4&w=80&we=false> -->
3
3
 
4
4
  [![GitHub](https://img.shields.io/badge/GitHub-bdombro%2Fbun--argsbarg-181717?logo=github)](https://github.com/bdombro/bun-argsbarg)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
- [![npm version](https://img.shields.io/npm/v/bun-argsbarg.svg)](https://www.npmjs.com/package/argsbarg)
6
+ [![npm version](https://img.shields.io/npm/v/argsbarg.svg)](https://www.npmjs.com/package/argsbarg)
7
7
  [![Bun](https://img.shields.io/badge/Bun-%23000000.svg?logo=bun&logoColor=white)](https://bun.sh)
8
8
 
9
9
  Build beautiful, well-behaved CLI apps with Bun — **no third-party runtime dependencies**.
@@ -17,13 +17,13 @@ Why another CLI parser?
17
17
  Also checkout ArgsBarg for [cpp](https://github.com/bdombro/cpp-argsbarg), [nim](https://github.com/bdombro/nim-argsbarg), and [swift](https://github.com/bdombro/swift-argsbarg)!
18
18
 
19
19
  Halps! -->
20
- ![help-preview.png](docs/help-preview.png)
20
+ ![help-preview.png](https://github.com/bdombro/bun-argsbarg/blob/main/docs/help-preview.png)
21
21
 
22
22
  Sub-level Halps! -->
23
- ![help-l2-preview.png](docs/help-l2-preview.png)
23
+ ![help-l2-preview.png](https://github.com/bdombro/bun-argsbarg/blob/main/docs/help-l2-preview.png)
24
24
 
25
25
  Shell completions! -->
26
- ![completions-preview.png](docs/completions-preview.png)
26
+ ![completions-preview.png](https://github.com/bdombro/bun-argsbarg/blob/main/docs/completions-preview.png)
27
27
 
28
28
 
29
29
  ## Usage
@@ -140,7 +140,7 @@ Add `CliPositional` entries to the command’s `positionals` list (separate from
140
140
 
141
141
  | Fields | Label |
142
142
  | --- | --- |
143
- | default `argMin`/`argMax` (single-slot) | `<n>` |
143
+ | omit `argMin` / `argMax` (defaults `1` / `1`, one required word) | `<n>` |
144
144
  | `argMin: 0`, `argMax: 1` | `[n]` |
145
145
  | `argMin: 0`, `argMax: 0` | `[n...]` |
146
146
  | `argMin: 1`, `argMax: 0` | `<n...>` |
@@ -37,8 +37,6 @@ const cli: CliCommand = {
37
37
  name: "path",
38
38
  description: "File or directory.",
39
39
  kind: CliOptionKind.String,
40
- argMin: 1,
41
- argMax: 1,
42
40
  },
43
41
  ],
44
42
  handler: (ctx) => {
@@ -64,7 +62,6 @@ const cli: CliCommand = {
64
62
  name: "files",
65
63
  description: "Paths to read.",
66
64
  kind: CliOptionKind.String,
67
- argMin: 1,
68
65
  argMax: 0,
69
66
  },
70
67
  ],
package/index.d.ts ADDED
@@ -0,0 +1,141 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ /**
4
+ * Option kinds: presence (boolean flag), string (free-form text), or number (strict double).
5
+ */
6
+ export declare enum CliOptionKind {
7
+ /** Boolean flag: no value token (may be implicit `"1"` when set). */
8
+ Presence = "presence",
9
+ /** Free-form string value. */
10
+ String = "string",
11
+ /** Strict floating-point value (parsed at validation time). */
12
+ Number = "number"
13
+ }
14
+ /**
15
+ * When fallbackCommand is used for missing or unknown top-level tokens.
16
+ * Only the program root may set a non-default mode or a non-nil fallbackCommand.
17
+ */
18
+ export declare enum CliFallbackMode {
19
+ /**
20
+ * If argv has no first subcommand, route to `fallbackCommand`; if the first token is unknown, error.
21
+ */
22
+ MissingOnly = "missingOnly",
23
+ /**
24
+ * If argv has no first subcommand or the first token is not a known child, route to `fallbackCommand`.
25
+ */
26
+ MissingOrUnknown = "missingOrUnknown",
27
+ /**
28
+ * If the first token is present but not a known child, route to `fallbackCommand`.
29
+ * When the first subcommand token is missing (empty argv), do not use fallback (implicit root help).
30
+ */
31
+ UnknownOnly = "unknownOnly"
32
+ }
33
+ /**
34
+ * A named flag or value option (`--long`, `-short`), listed on `CliCommand.options`.
35
+ */
36
+ export interface CliOption {
37
+ /** Option name (e.g., "name", "verbose"). */
38
+ name: string;
39
+ /** Description shown in help. */
40
+ description: string;
41
+ /** Option kind: presence flag, string value, or number value. */
42
+ kind: CliOptionKind;
43
+ /** Short option character (e.g., 'n' for -n). */
44
+ shortName?: string;
45
+ }
46
+ /**
47
+ * An ordered positional argument slot, listed on `CliCommand.positionals`.
48
+ */
49
+ export interface CliPositional {
50
+ /** Positional name (used in help and error messages). */
51
+ name: string;
52
+ /** Description shown in help. */
53
+ description: string;
54
+ /** Value kind for each consumed token. */
55
+ kind: CliOptionKind;
56
+ /**
57
+ * Minimum number of values required (default 1).
58
+ * Use `0` for an optional slot when paired with `argMax: 1`, or a varargs tail with `argMax: 0`.
59
+ */
60
+ argMin?: number;
61
+ /**
62
+ * Maximum number of values (`1` = a single required or optional word; default 1). Use `0` for an
63
+ * unbounded varargs tail (must be the last slot in the command’s `positionals` list).
64
+ */
65
+ argMax?: number;
66
+ }
67
+ /**
68
+ * A command node: routing group (has commands) or leaf (has handler).
69
+ *
70
+ * The value passed to cliRun is the program root: name is the app/binary name,
71
+ * commands are top-level subcommands, options are global flags.
72
+ * The root must not set handler or declare positionals (validated at startup).
73
+ */
74
+ export interface CliCommand {
75
+ /** Program or command key (e.g., "myapp", "stat", "owner"). */
76
+ key: string;
77
+ /** Short description shown in help. */
78
+ description: string;
79
+ /** Additional notes shown in help (supports {app} placeholder). */
80
+ notes?: string;
81
+ /** Global or command-level flags/options. */
82
+ options?: CliOption[];
83
+ /** Positional argument definitions. */
84
+ positionals?: CliPositional[];
85
+ /** Nested subcommands (empty for leaf commands). */
86
+ commands?: CliCommand[];
87
+ /** Handler function for leaf commands. */
88
+ handler?: CliHandler;
89
+ /** Default top-level subcommand when argv omits a command or uses an unknown first token (root only). */
90
+ fallbackCommand?: string;
91
+ /** How fallbackCommand is applied (root only). */
92
+ fallbackMode?: CliFallbackMode;
93
+ }
94
+ /**
95
+ * Handler closure type for leaf commands.
96
+ * Supports both sync and async handlers.
97
+ */
98
+ export type CliHandler = (ctx: CliContext) => void | Promise<void>;
99
+ /**
100
+ * Error thrown when the static CliCommand tree violates ArgsBarg rules.
101
+ */
102
+ export declare class CliSchemaValidationError extends Error {
103
+ /** Creates a schema validation error with a human-readable rule violation. */
104
+ constructor(message: string);
105
+ }
106
+ /**
107
+ * Values passed to a leaf command handler after parsing: app name, routed path, args, and merged options.
108
+ */
109
+ export declare class CliContext {
110
+ readonly appName: string;
111
+ readonly commandPath: string[];
112
+ readonly args: string[];
113
+ readonly schema: CliCommand;
114
+ readonly opts: Record<string, string>;
115
+ /** Captures the merged program root, routed path, positional words, and option map for a leaf handler. */
116
+ constructor(appName: string, commandPath: string[], args: string[], opts: Record<string, string>, schema: CliCommand);
117
+ /** Returns whether a presence flag was set (including implicit "1" for boolean options). */
118
+ hasFlag(name: string): boolean;
119
+ /** Returns the string value for a string-valued option, if present. */
120
+ stringOpt(name: string): string | undefined;
121
+ /** Parses a stored string as a number; returns null if missing or not a strict double string. */
122
+ numberOpt(name: string): number | null;
123
+ /**
124
+ * Generic typed accessor: parses a stored string using the provided parse function.
125
+ * This is the TypeScript-native advantage over the Swift version.
126
+ */
127
+ typedOpt<T>(name: string, parse: (s: string) => T): T | null;
128
+ }
129
+ /**
130
+ * Validates the schema, parses argv, prints help or errors, runs completion or the leaf handler, then exits.
131
+ *
132
+ * @param root The root CliCommand.
133
+ * @param argv Override the default argv (process.argv.slice(2)).
134
+ */
135
+ export declare function cliRun(root: CliCommand, argv?: string[]): Promise<never>;
136
+ /**
137
+ * Prints a red error line and contextual help on stderr, then exits with status 1.
138
+ */
139
+ export declare function cliErrWithHelp(ctx: CliContext, msg: string): never;
140
+
141
+ export {};
package/justfile CHANGED
@@ -27,6 +27,10 @@ lint:
27
27
  test: check-types format lint
28
28
  bun test
29
29
 
30
+ # generate type declarations for the package
31
+ typegen:
32
+ bunx dts-bundle-generator --out-file index.d.ts src/index.ts
33
+
30
34
  # publish to github and npm
31
- release bump: test
35
+ release bump: test typegen
32
36
  bun scripts/release.ts {{bump}}
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "//just": "echo this app uses justfile for development tasks"
7
7
  },
8
8
  "main": "./src/index.ts",
9
9
  "module": "./src/index.ts",
10
- "types": "./src/index.ts",
10
+ "types": "./index.d.ts",
11
11
  "bin": {
12
12
  "argsbarg": "src/index.ts"
13
13
  },
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Release workflow: bump semver; update CHANGELOG; commit, tag, push, GitHub release, npm publish.
3
+ * Release workflow: bump semver; update CHANGELOG (trailing release reference links); commit, tag, push,
4
+ * GitHub release, npm publish.
4
5
  *
5
6
  * **Run `just test` (or the individual `just check-types`, `just lint`, `bun test`) before this
6
7
  * script** — it does not typecheck, lint, or run tests. The `just release` recipe runs checks first.
@@ -65,6 +66,94 @@ function bumpSemver(version: string, part: Bump): string {
65
66
  return `${major}.${minor}.${patch}`;
66
67
  }
67
68
 
69
+ /** Returns stdout from a successful subprocess, trimmed; throws if the command fails. */
70
+ function runCapture(cmd: string[], cwd: string = repoRoot): string {
71
+ const proc = Bun.spawnSync({ cmd, cwd, stdout: "pipe", stderr: "pipe" });
72
+ if (proc.exitCode !== 0) {
73
+ const err = new TextDecoder().decode(proc.stderr);
74
+ throw new Error(`Command failed: ${cmd.join(" ")}\n${err}`);
75
+ }
76
+ return new TextDecoder().decode(proc.stdout).trimEnd();
77
+ }
78
+
79
+ /** Parses `git remote get-url origin` into `https://github.com/owner/repo`, or null if not GitHub. */
80
+ function githubRepoBaseFromOrigin(origin: string): string | null {
81
+ const trimmed = origin.trim();
82
+ let path = trimmed.replace(/^git@[^:]+:/, "").replace(/^https?:\/\/[^/]+\//, "");
83
+ if (path.endsWith(".git")) {
84
+ path = path.slice(0, -4);
85
+ }
86
+ if (!/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(path)) {
87
+ return null;
88
+ }
89
+ return `https://github.com/${path}`;
90
+ }
91
+
92
+ /**
93
+ * Collects released semver headings from changelog body in document order (newest first when
94
+ * headings follow Keep a Changelog order after each release).
95
+ */
96
+ function releasedSemverVersionsFromChangelog(md: string): string[] {
97
+ const re = /^## \[(\d+\.\d+\.\d+)\] /gm;
98
+ const out: string[] = [];
99
+ let m: RegExpExecArray | null;
100
+ while ((m = re.exec(md)) !== null) {
101
+ out.push(m[1]!);
102
+ }
103
+ return out;
104
+ }
105
+
106
+ /**
107
+ * Strips optional legacy `## Links` heading and/or trailing Keep-a-Changelog reference link lines
108
+ * (`[...]: http...`) at the end of the file.
109
+ */
110
+ function stripChangelogLinkDefinitions(md: string): string {
111
+ let s = md.replace(/\r?\n## Links\r?\n/, "\n");
112
+ const lines = s.split(/\r?\n/);
113
+ let i = lines.length;
114
+ while (i > 0 && lines[i - 1] === "") {
115
+ i -= 1;
116
+ }
117
+ const refLine = /^\[[^\]]+\]: .+$/;
118
+ while (i > 0 && refLine.test(lines[i - 1]!)) {
119
+ i -= 1;
120
+ }
121
+ while (i > 0 && lines[i - 1] === "") {
122
+ i -= 1;
123
+ }
124
+ if (i > 0 && lines[i - 1] === "## Links") {
125
+ i -= 1;
126
+ }
127
+ while (i > 0 && lines[i - 1] === "") {
128
+ i -= 1;
129
+ }
130
+ if (i === 0) {
131
+ return "";
132
+ }
133
+ return lines.slice(0, i).join("\n");
134
+ }
135
+
136
+ /**
137
+ * Appends reference link definitions (no visible heading): `[Unreleased]` → compare latest tag to `HEAD`,
138
+ * and each released `[x.y.z]` → release tag URL.
139
+ */
140
+ function appendChangelogLinkDefinitions(md: string, repoBase: string): string {
141
+ const body = stripChangelogLinkDefinitions(md).trimEnd();
142
+ const versions = releasedSemverVersionsFromChangelog(body);
143
+ if (versions.length === 0) {
144
+ return `${body}\n`;
145
+ }
146
+ const newest = versions[0]!;
147
+ const lines = [
148
+ "",
149
+ "",
150
+ `[Unreleased]: ${repoBase}/compare/v${newest}...HEAD`,
151
+ ...versions.map((v) => `[${v}]: ${repoBase}/releases/tag/v${v}`),
152
+ "",
153
+ ];
154
+ return `${body}${lines.join("\n")}`;
155
+ }
156
+
68
157
  /** Runs a subprocess, inheriting stdio, and exits the process on non-zero status. */
69
158
  function run(label: string, cmd: string[], cwd: string = repoRoot): void {
70
159
  console.log(`\n→ ${label}: ${cmd.join(" ")}`);
@@ -129,7 +218,13 @@ await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
129
218
 
130
219
  const changelogPath = joinFile(repoRoot, "CHANGELOG.md");
131
220
  const changelog = await Bun.file(changelogPath).text();
132
- const nextChangelog = promoteChangelog(changelog, nextVersion, releaseDate);
221
+ const promoted = promoteChangelog(changelog, nextVersion, releaseDate);
222
+ const origin = runCapture(["git", "remote", "get-url", "origin"], repoRoot);
223
+ const repoBase = githubRepoBaseFromOrigin(origin);
224
+ if (repoBase === null) {
225
+ throw new Error(`Could not parse GitHub repo URL from origin: ${JSON.stringify(origin)}`);
226
+ }
227
+ const nextChangelog = appendChangelogLinkDefinitions(promoted, repoBase);
133
228
  await Bun.write(changelogPath, nextChangelog);
134
229
 
135
230
  const notesPath = joinFile(repoRoot, ".release-notes.tmp.md");
package/src/help.ts CHANGED
@@ -177,11 +177,12 @@ export function cliOptionLabel(o: CliOption, color: boolean): string {
177
177
 
178
178
  /** Formats a positional slot label (`<n>`, `[n]`, or varargs) for help. */
179
179
  export function cliPositionalLabel(p: CliPositional, color: boolean): string {
180
+ const { argMin = 1, argMax = 1 } = p;
180
181
  let r: string;
181
- if (p.argMax === 1) {
182
- r = p.argMin === 0 ? "[" + p.name + "]" : "<" + p.name + ">";
182
+ if (argMax === 1) {
183
+ r = argMin === 0 ? "[" + p.name + "]" : "<" + p.name + ">";
183
184
  } else {
184
- r = p.argMin === 0 ? "[" + p.name + "...]" : "<" + p.name + "...>";
185
+ r = argMin === 0 ? "[" + p.name + "...]" : "<" + p.name + "...>";
185
186
  }
186
187
  if (!color) return r;
187
188
  return style.aquaBold(r);
package/src/parse.ts CHANGED
@@ -10,9 +10,9 @@ across every entry path.
10
10
  import { CliContext } from "./context.ts";
11
11
  import {
12
12
  CliCommand,
13
+ CliFallbackMode,
13
14
  CliOption,
14
15
  CliOptionKind,
15
- CliFallbackMode,
16
16
  } from "./types.ts";
17
17
  import { fullStringIsDouble } from "./utils.ts";
18
18
 
@@ -234,8 +234,9 @@ function finishLeaf(
234
234
  const args: string[] = [];
235
235
 
236
236
  for (const p of node.positionals ?? []) {
237
- if (p.argMax === 1) {
238
- if (p.argMin >= 1) {
237
+ const { argMin = 1, argMax = 1 } = p;
238
+ if (argMax === 1) {
239
+ if (argMin >= 1) {
239
240
  if (idx >= argv.length) {
240
241
  return errorResult(`Missing positional argument: ${p.name}`);
241
242
  }
@@ -249,21 +250,21 @@ function finishLeaf(
249
250
  }
250
251
 
251
252
  let count = 0;
252
- if (p.argMax === 0) {
253
+ if (argMax === 0) {
253
254
  while (idx < argv.length) {
254
255
  args.push(argv[idx]);
255
256
  idx += 1;
256
257
  count += 1;
257
258
  }
258
259
  } else {
259
- while (count < p.argMax && idx < argv.length) {
260
+ while (count < argMax && idx < argv.length) {
260
261
  args.push(argv[idx]);
261
262
  idx += 1;
262
263
  count += 1;
263
264
  }
264
265
  }
265
- if (count < p.argMin) {
266
- return errorResult(`Expected at least ${p.argMin} argument(s) for ${p.name}, got ${count}`);
266
+ if (count < argMin) {
267
+ return errorResult(`Expected at least ${argMin} argument(s) for ${p.name}, got ${count}`);
267
268
  }
268
269
  }
269
270
 
package/src/types.ts CHANGED
@@ -64,10 +64,16 @@ export interface CliPositional {
64
64
  description: string;
65
65
  /** Value kind for each consumed token. */
66
66
  kind: CliOptionKind;
67
- /** Minimum number of values required. */
68
- argMin: number;
69
- /** Maximum number of values (0 = unlimited, for a varargs tail). */
70
- argMax: number;
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;
71
77
  }
72
78
 
73
79
  /**
package/src/validate.ts CHANGED
@@ -96,15 +96,16 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
96
96
  // Validate positionals
97
97
  const positionals = cmd.positionals ?? [];
98
98
  for (const p of positionals) {
99
- if (p.argMin < 0) {
99
+ if (p.argMin !== undefined && p.argMin < 0) {
100
100
  throw new CliSchemaValidationError(`argMin must be >= 0 for positional ${cmd.key}/${p.name}`);
101
101
  }
102
- if (p.argMax < 0) {
102
+ if (p.argMax !== undefined && p.argMax < 0) {
103
103
  throw new CliSchemaValidationError(
104
104
  `argMax must be >= 0 (use 0 for unlimited) for positional ${cmd.key}/${p.name}`,
105
105
  );
106
106
  }
107
- if (p.argMax > 0 && p.argMin > p.argMax) {
107
+ const { argMin = 1, argMax = 1 } = p;
108
+ if (argMax > 0 && argMin > argMax) {
108
109
  throw new CliSchemaValidationError(
109
110
  `argMin must not exceed argMax for positional ${cmd.key}/${p.name}`,
110
111
  );
@@ -114,7 +115,8 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
114
115
  // Check positional ordering: required before optional
115
116
  let sawOptional = false;
116
117
  for (const p of positionals) {
117
- if (p.argMin === 0) {
118
+ const { argMin = 1 } = p;
119
+ if (argMin === 0) {
118
120
  sawOptional = true;
119
121
  } else if (sawOptional) {
120
122
  throw new CliSchemaValidationError(`Required positional after optional in scope ${cmd.key}`);
@@ -123,7 +125,8 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
123
125
 
124
126
  // Check unlimited positional must be last
125
127
  for (let idx = 0; idx < positionals.length; idx++) {
126
- if (positionals[idx].argMax === 0 && idx + 1 < positionals.length) {
128
+ const { argMax = 1 } = positionals[idx]!;
129
+ if (argMax === 0 && idx + 1 < positionals.length) {
127
130
  throw new CliSchemaValidationError(
128
131
  `Unlimited positional (argMax == 0) must be last in scope ${cmd.key}`,
129
132
  );