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.
- package/.cursor/rules/code.mdc +1 -2
- package/CHANGELOG.md +14 -0
- package/README.md +6 -6
- package/examples/nested.ts +0 -3
- package/index.d.ts +141 -0
- package/justfile +5 -1
- package/package.json +2 -2
- package/scripts/release.ts +97 -2
- package/src/help.ts +4 -3
- package/src/parse.ts +8 -7
- package/src/types.ts +10 -4
- package/src/validate.ts +8 -5
package/.cursor/rules/code.mdc
CHANGED
|
@@ -3,8 +3,7 @@ description: Code quality and style rules for TypeScript files
|
|
|
3
3
|
globs: **/*.ts
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
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
|
-

|
|
1
|
+

|
|
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
|
[](https://github.com/bdombro/bun-argsbarg)
|
|
5
5
|
[](LICENSE)
|
|
6
|
-
[](https://www.npmjs.com/package/argsbarg)
|
|
7
7
|
[](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
|
-

|
|
20
|
+

|
|
21
21
|
|
|
22
22
|
Sub-level Halps! -->
|
|
23
|
-

|
|
23
|
+

|
|
24
24
|
|
|
25
25
|
Shell completions! -->
|
|
26
|
-

|
|
26
|
+

|
|
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
|
-
|
|
|
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...>` |
|
package/examples/nested.ts
CHANGED
|
@@ -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.
|
|
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": "./
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
11
|
"bin": {
|
|
12
12
|
"argsbarg": "src/index.ts"
|
|
13
13
|
},
|
package/scripts/release.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Release workflow: bump semver; update CHANGELOG; commit, tag, push,
|
|
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
|
|
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 (
|
|
182
|
-
r =
|
|
182
|
+
if (argMax === 1) {
|
|
183
|
+
r = argMin === 0 ? "[" + p.name + "]" : "<" + p.name + ">";
|
|
183
184
|
} else {
|
|
184
|
-
r =
|
|
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
|
-
|
|
238
|
-
|
|
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 (
|
|
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 <
|
|
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 <
|
|
266
|
-
return errorResult(`Expected at least ${
|
|
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
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
);
|