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.
@@ -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,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
- ![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)
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "//just": "echo this app uses justfile for development tasks"
@@ -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
  );