argsbarg 0.1.0 → 1.0.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 -0
- package/CHANGELOG.md +35 -0
- package/README.md +36 -24
- package/examples/minimal.ts +12 -7
- package/examples/nested.ts +18 -12
- package/justfile +32 -0
- package/package.json +6 -11
- package/plan.md +2 -2
- package/scripts/release.ts +154 -0
- package/src/completion.ts +33 -14
- package/src/context.ts +2 -1
- package/src/help.ts +57 -23
- package/src/index.test.ts +79 -35
- package/src/index.ts +3 -14
- package/src/parse.ts +48 -18
- package/src/runtime.ts +2 -2
- package/src/types.ts +37 -35
- package/src/validate.ts +7 -6
- package/bun.lock +0 -21
package/.cursor/rules/code.mdc
CHANGED
|
@@ -7,3 +7,4 @@ globs: **/*.ts
|
|
|
7
7
|
|
|
8
8
|
- All imports must be ordered alphabetically by their source module path (the `from` clause).
|
|
9
9
|
- 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
|
+
- Types, Interfaces, Functions must have a docstring
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2026-04-22
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `scripts/release.ts` — release automation (`just release <major|minor|patch>`): lint, typecheck, tests, semver bump, CHANGELOG promotion, commit, tag, push, GitHub release, npm publish.
|
|
15
|
+
- `CliPositional` type for entries in `CliCommand.positionals` (name, description, kind, argMin, argMax).
|
|
16
|
+
- `cliPositionalLabel()` for help-style labels of positional slots (exported alongside `cliOptionLabel()`).
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **`CliCommand.children` → `CliCommand.commands`** — nested subcommands are declared under `commands` everywhere (schema, parser, validation, help, completion, runtime).
|
|
21
|
+
- **Development tasks** — former `package.json` scripts live in the repo `justfile` (e.g. `just test`, `just lint`). `package.json` no longer defines a `scripts` block.
|
|
22
|
+
- **Public barrel (`src/index.ts`)** — re-exports are limited to schema types and enums, `CliSchemaValidationError`, `CliContext`, `cliRun`, and `cliErrWithHelp`. Parsing (`parse`, `postParseValidate`, …), completion script helpers, help renderers, `cliValidateRoot`, and `utils` number helpers are no longer re-exported from the package entry (import from `src/*.ts` paths in this repo, or depend on internal modules if you fork).
|
|
23
|
+
|
|
24
|
+
### Removed
|
|
25
|
+
|
|
26
|
+
- **`createOption()`** — options and positionals are plain object literals; there is no factory helper.
|
|
27
|
+
- **`CliOptionDef`** — replaced by distinct types (see below).
|
|
28
|
+
|
|
29
|
+
### Breaking
|
|
30
|
+
|
|
31
|
+
- **`CliOption`** is only for named flags and value options (`options`). It no longer includes `positional`, `argMin`, or `argMax`. Use **`CliPositional`** on `positionals` for ordered arguments and varargs tails.
|
|
32
|
+
- **`CliCommand.positionals`** is now `CliPositional[]`, not `CliOption[]`.
|
|
33
|
+
- 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
|
+
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
35
|
+
|
package/README.md
CHANGED
|
@@ -29,23 +29,28 @@ Shell completions! -->
|
|
|
29
29
|
## Usage
|
|
30
30
|
|
|
31
31
|
```typescript
|
|
32
|
-
import { cliRun, CliCommand,
|
|
32
|
+
import { cliRun, CliCommand, CliOptionKind, CliFallbackMode } from "argsbarg";
|
|
33
33
|
|
|
34
34
|
const cli: CliCommand = {
|
|
35
35
|
key: "helloapp",
|
|
36
36
|
description: "Tiny demo.",
|
|
37
|
-
|
|
37
|
+
commands: [
|
|
38
38
|
{
|
|
39
39
|
key: "hello",
|
|
40
40
|
description: "Say hello.",
|
|
41
41
|
options: [
|
|
42
|
-
|
|
42
|
+
{
|
|
43
|
+
name: "name",
|
|
44
|
+
description: "Who to greet.",
|
|
43
45
|
kind: CliOptionKind.String,
|
|
44
46
|
shortName: "n",
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "verbose",
|
|
50
|
+
description: "Enable extra logging.",
|
|
51
|
+
kind: CliOptionKind.Presence,
|
|
47
52
|
shortName: "v",
|
|
48
|
-
}
|
|
53
|
+
},
|
|
49
54
|
],
|
|
50
55
|
handler: async (ctx) => {
|
|
51
56
|
const name = ctx.stringOpt("name") ?? "world";
|
|
@@ -71,10 +76,10 @@ await cliRun(cli);
|
|
|
71
76
|
|
|
72
77
|
Everything you need for a first-class CLI:
|
|
73
78
|
|
|
74
|
-
- **Nested subcommands** (`CliCommand` with `
|
|
79
|
+
- **Nested subcommands** (`CliCommand` with `commands` for groups, `handler` for leaves)
|
|
75
80
|
- **POSIX-style options** (`-x`, `--long`, `--long=value`)
|
|
76
81
|
- **Bundled presence flags** (`-abc`)
|
|
77
|
-
- **Positional arguments and varargs tails** (`
|
|
82
|
+
- **Positional arguments and varargs tails** (`CliPositional` objects on `positionals`)
|
|
78
83
|
- **Scoped help** at any routing depth (`-h` / `--help`)
|
|
79
84
|
- **Default-command fallback** (`CliFallbackMode`)
|
|
80
85
|
- **Option separator** (`--` to stop option parsing)
|
|
@@ -115,7 +120,7 @@ bun add bun-argsbarg
|
|
|
115
120
|
|
|
116
121
|
## How it works
|
|
117
122
|
|
|
118
|
-
1. Build a **program root** `CliCommand` using pure TypeScript objects: `key` is the app/binary name, `
|
|
123
|
+
1. Build a **program root** `CliCommand` using pure TypeScript objects: `key` is the app/binary name, `commands` are top-level subcommands, `options` are global flags. The root must not set `handler` or declare `positionals` (validated at startup). Use `fallbackCommand` / `fallbackMode` on the root only for default top-level routing.
|
|
119
124
|
2. Call `await cliRun(root)` with that root — validates, parses argv, renders help or errors, invokes the leaf handler, and `process.exit`s with status **0** on success, **1** on implicit help or error (explicit `--help` → **0**).
|
|
120
125
|
3. From a handler, `cliErrWithHelp(ctx, "message")` prints a red error line plus contextual help on stderr and exits **1**.
|
|
121
126
|
|
|
@@ -131,14 +136,14 @@ With `MissingOrUnknown` / `UnknownOnly`, unrecognized **root** flags stop root-f
|
|
|
131
136
|
|
|
132
137
|
### Positionals (help labels)
|
|
133
138
|
|
|
134
|
-
|
|
139
|
+
Add `CliPositional` entries to the command’s `positionals` list (separate from `CliOption` flags). With `argMax: 0`, the tail accepts at least `argMin` tokens and has no upper bound unless you set `argMax` > 0.
|
|
135
140
|
|
|
136
141
|
| Fields | Label |
|
|
137
142
|
| --- | --- |
|
|
138
|
-
|
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
143
|
+
| default `argMin`/`argMax` (single-slot) | `<n>` |
|
|
144
|
+
| `argMin: 0`, `argMax: 1` | `[n]` |
|
|
145
|
+
| `argMin: 0`, `argMax: 0` | `[n...]` |
|
|
146
|
+
| `argMin: 1`, `argMax: 0` | `<n...>` |
|
|
142
147
|
|
|
143
148
|
### Reading values (`CliContext`)
|
|
144
149
|
|
|
@@ -160,24 +165,31 @@ Check the `examples/` directory for full working scripts:
|
|
|
160
165
|
| `ArgsBargNested` | `examples/nested.ts` | Nested `CliCommand` tree, positional tails, async handlers. |
|
|
161
166
|
|
|
162
167
|
```bash
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
export PATH="$PATH:$(pwd)/examples"
|
|
169
|
+
|
|
170
|
+
eval "$(minimal.ts completion zsh)"
|
|
171
|
+
minimal.ts --help
|
|
172
|
+
minimal.ts hello --name world
|
|
173
|
+
|
|
174
|
+
eval "$(nested.ts completion zsh)"
|
|
175
|
+
nested.ts stat owner lookup -u alice ./README.md
|
|
176
|
+
nested.ts read ./README.md
|
|
167
177
|
```
|
|
168
178
|
|
|
169
179
|
|
|
170
180
|
|
|
171
181
|
## Public API overview
|
|
172
182
|
|
|
183
|
+
The package root (`argsbarg` / `src/index.ts`) exports the types and runtime you need to define a schema and run it. Parsing, completion script generation, help rendering, and schema pre-validation live in other modules under `src/` for tests and advanced integrations.
|
|
184
|
+
|
|
173
185
|
| Symbol | Role |
|
|
174
186
|
| --- | --- |
|
|
175
|
-
| `CliCommand`, `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
187
|
+
| `CliCommand`, `CliOption`, `CliPositional`, `CliHandler` | Schema and handler types. |
|
|
188
|
+
| `CliOptionKind`, `CliFallbackMode` | Option kinds and root fallback behavior. |
|
|
189
|
+
| `CliSchemaValidationError` | Thrown when the static command tree violates schema rules. |
|
|
190
|
+
| `CliContext` | Handler context (`ctx.flag`, `ctx.stringOpt`, `ctx.args`, …). |
|
|
191
|
+
| `cliRun(root, [argv])` | Validate, parse argv, dispatch, exit. |
|
|
192
|
+
| `cliErrWithHelp(ctx, msg)` | Print error + scoped help on stderr, exit 1. |
|
|
181
193
|
|
|
182
194
|
Reserved identifier (validated at startup): root command **`completion`**.
|
|
183
195
|
|
package/examples/minimal.ts
CHANGED
|
@@ -7,27 +7,32 @@ readers can copy the pattern into their own scripts quickly.
|
|
|
7
7
|
It demonstrates the minimal Bun integration path.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { cliRun, CliCommand,
|
|
10
|
+
import { cliRun, CliCommand, CliOptionKind, CliFallbackMode } from "../src/index.ts";
|
|
11
11
|
|
|
12
12
|
const cli: CliCommand = {
|
|
13
13
|
key: "minimal.ts",
|
|
14
14
|
description: "Tiny demo.",
|
|
15
|
-
|
|
15
|
+
commands: [
|
|
16
16
|
{
|
|
17
17
|
key: "hello",
|
|
18
18
|
description: "Say hello.",
|
|
19
19
|
options: [
|
|
20
|
-
|
|
20
|
+
{
|
|
21
|
+
name: "name",
|
|
22
|
+
description: "Who to greet.",
|
|
21
23
|
kind: CliOptionKind.String,
|
|
22
24
|
shortName: "n",
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "verbose",
|
|
28
|
+
description: "Enable extra logging.",
|
|
29
|
+
kind: CliOptionKind.Presence,
|
|
25
30
|
shortName: "v",
|
|
26
|
-
}
|
|
31
|
+
},
|
|
27
32
|
],
|
|
28
33
|
handler: (ctx) => {
|
|
29
34
|
const name = ctx.stringOpt("name") ?? "world";
|
|
30
|
-
if (ctx.
|
|
35
|
+
if (ctx.hasFlag("verbose")) {
|
|
31
36
|
console.log("verbose mode");
|
|
32
37
|
}
|
|
33
38
|
console.log(`hello ${name}`);
|
package/examples/nested.ts
CHANGED
|
@@ -7,34 +7,39 @@ and fallback commands fit together in one schema.
|
|
|
7
7
|
It demonstrates how the schema scales beyond one command.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { cliRun, CliCommand,
|
|
10
|
+
import { cliRun, CliCommand, CliOptionKind, CliFallbackMode } from "../src/index.ts";
|
|
11
11
|
|
|
12
12
|
const cli: CliCommand = {
|
|
13
13
|
key: "nested.ts",
|
|
14
14
|
description: "Nested groups demo.",
|
|
15
|
-
|
|
15
|
+
commands: [
|
|
16
16
|
{
|
|
17
17
|
key: "stat",
|
|
18
18
|
description: "File metadata.",
|
|
19
|
-
|
|
19
|
+
commands: [
|
|
20
20
|
{
|
|
21
21
|
key: "owner",
|
|
22
22
|
description: "Ownership helpers.",
|
|
23
|
-
|
|
23
|
+
commands: [
|
|
24
24
|
{
|
|
25
25
|
key: "lookup",
|
|
26
26
|
description: "Resolve owner info.",
|
|
27
27
|
options: [
|
|
28
|
-
|
|
28
|
+
{
|
|
29
|
+
name: "user-name",
|
|
30
|
+
description: "User to look up.",
|
|
29
31
|
kind: CliOptionKind.String,
|
|
30
32
|
shortName: "u",
|
|
31
|
-
}
|
|
33
|
+
},
|
|
32
34
|
],
|
|
33
35
|
positionals: [
|
|
34
|
-
|
|
36
|
+
{
|
|
37
|
+
name: "path",
|
|
38
|
+
description: "File or directory.",
|
|
35
39
|
kind: CliOptionKind.String,
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
argMin: 1,
|
|
41
|
+
argMax: 1,
|
|
42
|
+
},
|
|
38
43
|
],
|
|
39
44
|
handler: (ctx) => {
|
|
40
45
|
const user = ctx.stringOpt("user-name") ?? "?";
|
|
@@ -55,12 +60,13 @@ const cli: CliCommand = {
|
|
|
55
60
|
description: "Print the first line of each file.",
|
|
56
61
|
notes: "Pass one or more file paths. {app} prints the first line of each.",
|
|
57
62
|
positionals: [
|
|
58
|
-
|
|
63
|
+
{
|
|
64
|
+
name: "files",
|
|
65
|
+
description: "Paths to read.",
|
|
59
66
|
kind: CliOptionKind.String,
|
|
60
|
-
positional: true,
|
|
61
67
|
argMin: 1,
|
|
62
68
|
argMax: 0,
|
|
63
|
-
}
|
|
69
|
+
},
|
|
64
70
|
],
|
|
65
71
|
handler: async (ctx) => {
|
|
66
72
|
if (ctx.args.length === 0) {
|
package/justfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# https://github.com/casey/just — run `just` to list recipes.
|
|
2
|
+
|
|
3
|
+
_:
|
|
4
|
+
@just --list
|
|
5
|
+
|
|
6
|
+
# typecheck the codebase
|
|
7
|
+
check-types:
|
|
8
|
+
bun x tsc
|
|
9
|
+
|
|
10
|
+
# run the minimal example
|
|
11
|
+
example:
|
|
12
|
+
bun ./examples/minimal.ts
|
|
13
|
+
|
|
14
|
+
# run the minimal example and watch for changes
|
|
15
|
+
example-watch:
|
|
16
|
+
bun --watch ./examples/minimal.ts
|
|
17
|
+
|
|
18
|
+
# format the codebase
|
|
19
|
+
format:
|
|
20
|
+
bun x biome format ./src ./scripts --write
|
|
21
|
+
|
|
22
|
+
# lint the codebase
|
|
23
|
+
lint:
|
|
24
|
+
bun x biome check ./src ./scripts
|
|
25
|
+
|
|
26
|
+
# Typecheck, lint, then run the test suite.
|
|
27
|
+
test: check-types format lint
|
|
28
|
+
bun test
|
|
29
|
+
|
|
30
|
+
# publish to github and npm
|
|
31
|
+
release bump: test
|
|
32
|
+
bun scripts/release.ts {{bump}}
|
package/package.json
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "argsbarg",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"//just": "echo this app uses justfile for development tasks"
|
|
7
|
+
},
|
|
5
8
|
"main": "./src/index.ts",
|
|
6
9
|
"module": "./src/index.ts",
|
|
7
10
|
"types": "./src/index.ts",
|
|
8
11
|
"bin": {
|
|
9
|
-
"argsbarg": "
|
|
12
|
+
"argsbarg": "src/index.ts"
|
|
10
13
|
},
|
|
11
14
|
"exports": {
|
|
12
15
|
".": "./src/index.ts"
|
|
13
16
|
},
|
|
14
|
-
"scripts": {
|
|
15
|
-
"test": "bun check-types && bun lint && bun test",
|
|
16
|
-
"dev": "bun --watch ./examples/minimal.ts",
|
|
17
|
-
"lint": "bun x biome check ./src",
|
|
18
|
-
"format": "bun x biome format ./src --write",
|
|
19
|
-
"check-types": "bun x tsc",
|
|
20
|
-
"release": "bun x npm publish"
|
|
21
|
-
},
|
|
22
17
|
"devDependencies": {
|
|
23
18
|
"@types/bun": "^1.3.12"
|
|
24
19
|
}
|
|
25
|
-
}
|
|
20
|
+
}
|
package/plan.md
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
- **Phase 1**: Core Schema Types (`src/types.ts`)
|
|
14
14
|
- `CliOptionKind` enum (Presence, String, Number)
|
|
15
15
|
- `CliFallbackMode` enum (MissingOnly, MissingOrUnknown, UnknownOnly)
|
|
16
|
-
- `CliCommand`, `
|
|
16
|
+
- `CliCommand`, `CliOption`, `CliPositional`, `CliHandler`, `CliContext` interfaces
|
|
17
17
|
- `CliSchemaValidationError` class
|
|
18
|
-
-
|
|
18
|
+
- `CliOption` on `options`, `CliPositional` on `positionals`, nested routes under `commands`
|
|
19
19
|
|
|
20
20
|
- **Phase 2**: Argument Parser (`src/parse.ts`)
|
|
21
21
|
- `parse(root, argv)` with long/short options, bundling, equals syntax
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Release workflow: bump semver; update CHANGELOG; commit, tag, push, GitHub release, npm publish.
|
|
4
|
+
*
|
|
5
|
+
* **Run `just test` (or the individual `just check-types`, `just lint`, `bun test`) before this
|
|
6
|
+
* script** — it does not typecheck, lint, or run tests. The `just release` recipe runs checks first.
|
|
7
|
+
*
|
|
8
|
+
* Usage: `bun scripts/release.ts <major|minor|patch>` (usually via `just release <bump>`.)
|
|
9
|
+
*
|
|
10
|
+
* Requires: `git`, `gh` (authenticated), `npm` logged in for publish. The release commit stages **all**
|
|
11
|
+
* repo changes (`git add -A`), not only the version and CHANGELOG edits from this script.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { unlink } from "fs/promises";
|
|
15
|
+
|
|
16
|
+
/** Returns the parent directory of an absolute file path. */
|
|
17
|
+
function parentDir(absolute: string): string {
|
|
18
|
+
const s = absolute.replace(/[/\\]+$/, "");
|
|
19
|
+
const i = Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\"));
|
|
20
|
+
if (i <= 0) {
|
|
21
|
+
return s;
|
|
22
|
+
}
|
|
23
|
+
return s.slice(0, i);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Monorepo root: parent of `scripts/` (directory of this file). */
|
|
27
|
+
const repoRoot = parentDir(import.meta.dir);
|
|
28
|
+
|
|
29
|
+
/** Which semver segment to increment for the next release. */
|
|
30
|
+
type Bump = "major" | "minor" | "patch";
|
|
31
|
+
|
|
32
|
+
/** Prints usage to stderr and exits with status 1. */
|
|
33
|
+
function usage(): never {
|
|
34
|
+
console.error("Usage: bun scripts/release.ts <major|minor|patch>");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Parses the release bump level from argv; exits on invalid input. */
|
|
39
|
+
function parseBump(s: string | undefined): Bump {
|
|
40
|
+
if (s === "major" || s === "minor" || s === "patch") {
|
|
41
|
+
return s;
|
|
42
|
+
}
|
|
43
|
+
usage();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Returns the next `x.y.z` version for the given `major` | `minor` | `patch` segment. */
|
|
47
|
+
function bumpSemver(version: string, part: Bump): string {
|
|
48
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
|
|
49
|
+
if (!m) {
|
|
50
|
+
throw new Error(`package.json version must be semver x.y.z, got: ${JSON.stringify(version)}`);
|
|
51
|
+
}
|
|
52
|
+
let major = Number(m[1]);
|
|
53
|
+
let minor = Number(m[2]);
|
|
54
|
+
let patch = Number(m[3]);
|
|
55
|
+
if (part === "major") {
|
|
56
|
+
major += 1;
|
|
57
|
+
minor = 0;
|
|
58
|
+
patch = 0;
|
|
59
|
+
} else if (part === "minor") {
|
|
60
|
+
minor += 1;
|
|
61
|
+
patch = 0;
|
|
62
|
+
} else {
|
|
63
|
+
patch += 1;
|
|
64
|
+
}
|
|
65
|
+
return `${major}.${minor}.${patch}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Runs a subprocess, inheriting stdio, and exits the process on non-zero status. */
|
|
69
|
+
function run(label: string, cmd: string[], cwd: string = repoRoot): void {
|
|
70
|
+
console.log(`\n→ ${label}: ${cmd.join(" ")}`);
|
|
71
|
+
const proc = Bun.spawnSync({ cmd, cwd, stdout: "inherit", stderr: "inherit" });
|
|
72
|
+
if (proc.exitCode !== 0) {
|
|
73
|
+
console.error(`\n${label} failed (exit ${proc.exitCode})`);
|
|
74
|
+
process.exit(proc.exitCode ?? 1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Moves `[Unreleased]` content under a new `## [version] - date` heading and leaves an empty Unreleased. */
|
|
79
|
+
function promoteChangelog(content: string, version: string, date: string): string {
|
|
80
|
+
const header = "## [Unreleased]";
|
|
81
|
+
const idx = content.indexOf(header);
|
|
82
|
+
if (idx === -1) {
|
|
83
|
+
throw new Error("CHANGELOG.md: missing ## [Unreleased] section");
|
|
84
|
+
}
|
|
85
|
+
const lineEnd = content.indexOf("\n", idx);
|
|
86
|
+
if (lineEnd === -1) {
|
|
87
|
+
throw new Error("CHANGELOG.md: malformed after [Unreleased]");
|
|
88
|
+
}
|
|
89
|
+
const bodyStart = lineEnd + 1;
|
|
90
|
+
const nextIdx = content.indexOf("\n## [", bodyStart);
|
|
91
|
+
const body =
|
|
92
|
+
nextIdx === -1 ? content.slice(bodyStart).trimEnd() : content.slice(bodyStart, nextIdx).trimEnd();
|
|
93
|
+
const tail = nextIdx === -1 ? "" : content.slice(nextIdx + 1);
|
|
94
|
+
const before = content.slice(0, idx);
|
|
95
|
+
const newBlock = `${header}\n\n## [${version}] - ${date}\n${body}\n\n`;
|
|
96
|
+
return before + newBlock + tail;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Returns the markdown body of one version section for `gh release` notes. */
|
|
100
|
+
function extractReleaseNotes(nextChangelog: string, version: string, date: string): string {
|
|
101
|
+
const verHeader = `## [${version}] - ${date}`;
|
|
102
|
+
const ni = nextChangelog.indexOf(verHeader);
|
|
103
|
+
if (ni === -1) {
|
|
104
|
+
throw new Error("internal: promoted version header not found in CHANGELOG");
|
|
105
|
+
}
|
|
106
|
+
const nextHdrAt = nextChangelog.indexOf("\n## [", ni + verHeader.length);
|
|
107
|
+
if (nextHdrAt === -1) {
|
|
108
|
+
return nextChangelog.slice(ni).trimEnd();
|
|
109
|
+
}
|
|
110
|
+
return nextChangelog.slice(ni, nextHdrAt).trimEnd();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Joins a root path and a file segment with a single path separator. */
|
|
114
|
+
function joinFile(root: string, segment: string): string {
|
|
115
|
+
const r = root.replace(/[/\\]+$/, "");
|
|
116
|
+
return `${r}/${segment}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const bump = parseBump(process.argv[2]);
|
|
120
|
+
|
|
121
|
+
const pkgPath = joinFile(repoRoot, "package.json");
|
|
122
|
+
const pkgText = await Bun.file(pkgPath).text();
|
|
123
|
+
const pkg = JSON.parse(pkgText) as { name: string; version: string };
|
|
124
|
+
const nextVersion = bumpSemver(pkg.version, bump);
|
|
125
|
+
const releaseDate = new Date().toISOString().slice(0, 10);
|
|
126
|
+
|
|
127
|
+
pkg.version = nextVersion;
|
|
128
|
+
await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
129
|
+
|
|
130
|
+
const changelogPath = joinFile(repoRoot, "CHANGELOG.md");
|
|
131
|
+
const changelog = await Bun.file(changelogPath).text();
|
|
132
|
+
const nextChangelog = promoteChangelog(changelog, nextVersion, releaseDate);
|
|
133
|
+
await Bun.write(changelogPath, nextChangelog);
|
|
134
|
+
|
|
135
|
+
const notesPath = joinFile(repoRoot, ".release-notes.tmp.md");
|
|
136
|
+
const notesContent = extractReleaseNotes(nextChangelog, nextVersion, releaseDate);
|
|
137
|
+
await Bun.write(notesPath, `${notesContent}\n`);
|
|
138
|
+
|
|
139
|
+
const tag = `v${nextVersion}`;
|
|
140
|
+
const msg = `release ${tag}`;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
run("git add (all)", ["git", "add", "-A"], repoRoot);
|
|
144
|
+
run("git commit", ["git", "commit", "-m", msg], repoRoot);
|
|
145
|
+
run("git tag", ["git", "tag", "-a", tag, "-m", msg], repoRoot);
|
|
146
|
+
run("git push", ["git", "push"], repoRoot);
|
|
147
|
+
run("git push tags", ["git", "push", "--tags"], repoRoot);
|
|
148
|
+
run("gh release", ["gh", "release", "create", tag, "--title", tag, "--notes-file", notesPath], repoRoot);
|
|
149
|
+
run("npm publish", ["npm", "publish"], repoRoot);
|
|
150
|
+
} finally {
|
|
151
|
+
await unlink(notesPath).catch(() => {});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(`\nDone: published ${pkg.name}@${nextVersion} (${tag}).`);
|