argsbarg 1.0.0 → 1.0.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/.cursor/rules/code.mdc +1 -2
- package/CHANGELOG.md +10 -0
- package/README.md +5 -5
- package/examples/nested.ts +0 -3
- package/package.json +1 -1
- 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,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.1] - 2026-04-22
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`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.
|
|
15
|
+
- **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`.
|
|
16
|
+
|
|
10
17
|
## [1.0.0] - 2026-04-22
|
|
11
18
|
|
|
12
19
|
### Added
|
|
@@ -33,3 +40,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
33
40
|
- 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
41
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
35
42
|
|
|
43
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.0.1...HEAD
|
|
44
|
+
[1.0.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.0.1
|
|
45
|
+
[1.0.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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)
|
|
@@ -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/package.json
CHANGED
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
|
);
|