argsbarg 5.1.8 → 5.1.10
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/CHANGELOG.md +29 -1
- package/docs/cli-program.md +3 -1
- package/docs/config-schema.md +5 -3
- package/docs/configure.md +13 -7
- package/docs/distribution-homebrew.md +17 -3
- package/examples/full-example/justfile +6 -5
- package/examples/full-example/scripts/formula-shared.test.ts +68 -0
- package/examples/full-example/scripts/formula-shared.ts +39 -1
- package/examples/full-example/scripts/release.ts +212 -0
- package/index.d.ts +10 -1
- package/package.json +2 -1
- package/src/builtins/builtins.test.ts +1 -3
- package/src/builtins/completion-bash.ts +6 -27
- package/src/builtins/completion-group.ts +1 -2
- package/src/builtins/completion-simulate-shared.ts +2 -10
- package/src/builtins/completion-zsh.ts +7 -33
- package/src/builtins/config.test.ts +28 -0
- package/src/builtins/config.ts +40 -10
- package/src/builtins/configure-copy.ts +3 -6
- package/src/builtins/dispatch.ts +2 -10
- package/src/builtins/export.ts +1 -3
- package/src/builtins/presentation.ts +2 -7
- package/src/builtins/registry.ts +1 -5
- package/src/cli-tool/create.test.ts +3 -6
- package/src/cli-tool/create.ts +5 -22
- package/src/cli-tool/post-create.ts +1 -3
- package/src/cli-tool/program.ts +1 -2
- package/src/cli-tool/run-create.ts +2 -8
- package/src/cli.ts +6 -27
- package/src/config/bindings.test.ts +72 -0
- package/src/config/bindings.ts +115 -0
- package/src/config/bootstrap.ts +80 -65
- package/src/config/context.test.ts +45 -0
- package/src/config/context.ts +74 -14
- package/src/config/entry.ts +1 -3
- package/src/config/file.test.ts +46 -3
- package/src/config/file.ts +59 -19
- package/src/config/resolve.ts +1 -4
- package/src/config/schema.ts +1 -3
- package/src/config/validate.test.ts +3 -11
- package/src/config/validate.ts +30 -30
- package/src/config.integration.test.ts +1 -5
- package/src/configure/configure.test.ts +19 -5
- package/src/configure/index.ts +17 -15
- package/src/docs/api-guide.ts +1 -6
- package/src/docs/builtin.ts +1 -5
- package/src/docs/docs.test.ts +2 -6
- package/src/docs/mcp-guide.ts +3 -13
- package/src/docs/mcp-resources.test.ts +1 -3
- package/src/docs/mcp-resources.ts +1 -6
- package/src/docs/save.ts +2 -10
- package/src/formats.test.ts +1 -7
- package/src/formats.ts +1 -5
- package/src/headless.test.ts +5 -17
- package/src/help.ts +9 -51
- package/src/hidden-mcpb.test.ts +3 -12
- package/src/hidden.ts +1 -3
- package/src/install/binary-placement.test.ts +2 -6
- package/src/install/binary-placement.ts +1 -4
- package/src/install/gh-release-update.ts +5 -22
- package/src/install/mcp-codex.ts +1 -2
- package/src/install/mcp-config.ts +2 -12
- package/src/install/mcp-opencode.test.ts +1 -3
- package/src/install/mcp-opencode.ts +3 -15
- package/src/install/plan.ts +2 -10
- package/src/install/status.ts +3 -5
- package/src/install/target-base.ts +2 -11
- package/src/install/target-detect.ts +1 -5
- package/src/install/target-effective.ts +3 -13
- package/src/install/target-mcp-cli.ts +4 -26
- package/src/install/target-mcp-json.ts +3 -14
- package/src/install/target-plan-build.ts +1 -5
- package/src/install/target-registry.ts +13 -21
- package/src/install/target-scope.ts +8 -25
- package/src/install/target-types.ts +1 -6
- package/src/install/targets/app.ts +2 -2
- package/src/install/targets/opencode-mcp.ts +1 -6
- package/src/install/targets.test.ts +3 -10
- package/src/install/uninstall.ts +2 -9
- package/src/invoke.test.ts +1 -3
- package/src/mcp/bundle.ts +4 -19
- package/src/mcp/claude.test.ts +3 -3
- package/src/mcp/claude.ts +4 -17
- package/src/mcp/env.ts +1 -3
- package/src/mcp/server.ts +3 -12
- package/src/mcp/tools.ts +2 -11
- package/src/mcp.integration.test.ts +4 -10
- package/src/parse.test.ts +9 -34
- package/src/parse.ts +10 -44
- package/src/schema.ts +2 -10
- package/src/skill/hint.ts +1 -5
- package/src/skill/install.ts +1 -5
- package/src/test-fixtures.ts +1 -3
- package/src/validate.ts +29 -90
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.1.10] - 2026-07-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Zip release assets** — Homebrew formulae download `{key}.zip` from GitHub Releases; `buildReleaseArchive` in `formula-shared.ts`; `just release --purge` to delete stale releases.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Release workflow** — `scripts/release.ts` uploads a zip archive (smaller download) instead of a bare Mach-O binary; formula `sha256` pins the archive.
|
|
19
|
+
|
|
20
|
+
## [5.1.9] - 2026-07-05
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`configure --sync` config bootstrap** — creates `~/.local/lib/<key>/config.json` as `{}` when missing (all apps; Homebrew `post_install`).
|
|
25
|
+
- **`_bindings` metadata** — per-key intent (`env` | `file` | `skip`) in config file; wizard persists env/skip choices; `configure set --from-env`.
|
|
26
|
+
- **`ctx.appConfig` unsafe I/O** — `getUnsafe`, `setUnsafe`, `readUnsafe` for raw file access (works without `program.appConfig`).
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **App config detection** — `appConfigInstalled` / `--status` use file existence only (not directory-only).
|
|
31
|
+
- **Configure wizard** — skips addressed keys; Enter at env prompt persists `_bindings`; accurate write messaging.
|
|
32
|
+
- **`configure --status`** — binding hints on required keys (`set (env)`, etc.).
|
|
33
|
+
- **Partial config validation** — single-key / bindings-only writes skip required-property checks.
|
|
34
|
+
- biome lineLength=120
|
|
35
|
+
|
|
10
36
|
## [5.1.8] - 2026-07-05
|
|
11
37
|
|
|
12
38
|
|
|
@@ -630,7 +656,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
|
|
|
630
656
|
- 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`).
|
|
631
657
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
632
658
|
|
|
633
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v5.1.
|
|
659
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v5.1.10...HEAD
|
|
660
|
+
[5.1.10]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.10
|
|
661
|
+
[5.1.9]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.9
|
|
634
662
|
[5.1.8]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.8
|
|
635
663
|
[5.1.7]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.7
|
|
636
664
|
[5.1.6]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.6
|
package/docs/cli-program.md
CHANGED
|
@@ -432,7 +432,9 @@ await cli.run();
|
|
|
432
432
|
|
|
433
433
|
See [config-schema.md](config-schema.md) for codegen, [configure.md](configure.md) (`configure.targets`), and [mcp.md](mcp.md).
|
|
434
434
|
|
|
435
|
-
**Handler access (`ctx.appConfig`):** `get`, `require`, `set`, `read`, `
|
|
435
|
+
**Handler access (`ctx.appConfig`):** `get`, `require`, `set`, `read` (schema-aware, resolved values); `getUnsafe`, `setUnsafe`, `readUnsafe` (raw file, works without `program.appConfig`); `path`, `dir`. Prefer `get`/`set` when `appConfig` is declared. Env export remains for subprocess inheritance. `path` is `~/.local/lib/<key>/config.json`; `dir` is its parent.
|
|
436
|
+
|
|
437
|
+
**`_bindings`:** reserved metadata for per-key intent (`env`, `file`, `skip`). Set by the wizard, `configure set --from-env`, or `ctx.appConfig.set`. Does not change resolve order (env still wins when set).
|
|
436
438
|
|
|
437
439
|
## Reserved names
|
|
438
440
|
|
package/docs/config-schema.md
CHANGED
|
@@ -42,11 +42,13 @@ await cli.run();
|
|
|
42
42
|
| Interactive `configure` / `--status` | Auto-runs config wizard when `entries` is non-empty; `--status` for read-only inventory |
|
|
43
43
|
| Built-in `configure get` / `configure set` | Read/write resolved values (opt-out via `commands: false`) |
|
|
44
44
|
| MCP bundle / Claude plugin | `userConfig` for entries with `env` set |
|
|
45
|
-
| `ctx.appConfig` in handlers | `get`, `require`, `set`, `read`, `path`, `dir` — prefer
|
|
45
|
+
| `ctx.appConfig` in handlers | `get`, `require`, `set`, `read`, `getUnsafe`, `setUnsafe`, `readUnsafe`, `path`, `dir` — prefer `get`/`set` when `appConfig` is set |
|
|
46
46
|
|
|
47
|
-
**
|
|
47
|
+
**Handler access** — with `program.appConfig`: `get` / `set` / `require` use schema validation and resolved values. `getUnsafe` / `setUnsafe` / `readUnsafe` read and write the raw file (for `_bindings` and ad-hoc keys). Without `program.appConfig`, only `path`, `dir`, and the `*Unsafe` methods work.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
**`_bindings`** — reserved top-level metadata: `{ "_bindings": { "apiToken": "env" } }`. Set via wizard (Enter to use env), `configure set --from-env`, or `ctx.appConfig.set` (marks `file`). Optional keys can be bound to `skip`.
|
|
50
|
+
|
|
51
|
+
**Validation at runtime** — argsbarg validates the config file and `configure set` / `ctx.appConfig.set` against the effective JSON Schema. Partial writes (bindings only, single-key updates) skip required-property checks.
|
|
50
52
|
|
|
51
53
|
See [cli-program.md — Configuration](cli-program.md#configuration-programappconfig) for resolution order, bootstrap timing, and `configure get`/`set`.
|
|
52
54
|
|
package/docs/configure.md
CHANGED
|
@@ -54,7 +54,7 @@ Dev flow matches release: formula `install` copies the binary and generates comp
|
|
|
54
54
|
|
|
55
55
|
# Read or write app config (non-interactive; when program.appConfig is set)
|
|
56
56
|
<key> configure get [key] [--json] [--pretty]
|
|
57
|
-
<key> configure set <key> <value> [--json]
|
|
57
|
+
<key> configure set <key> <value> [--json] [--from-env]
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
Non-interactive / CI: pass **`--yes`** (or **`--json`**, **`--sync`**, **`--remove-all`**, **`--remove-config`**) — see [Confirmation](#confirmation).
|
|
@@ -69,14 +69,14 @@ Non-interactive / CI: pass **`--yes`** (or **`--json`**, **`--sync`**, **`--remo
|
|
|
69
69
|
| Claude skill | Y/n prompt | `~/.claude/skills/<dir>/` when `~/.claude` exists |
|
|
70
70
|
| Codex / OpenCode / OpenClaw skills | Y/n prompt | Agent-specific dirs when available |
|
|
71
71
|
| MCP config | Y/n prompt when `mcpServer.enabled` | Cursor, Claude Code/Desktop, OpenCode, Codex, OpenClaw, ChatGPT desktop |
|
|
72
|
-
| App config | auto-runs wizard | Interactive wizard
|
|
72
|
+
| App config | auto-runs wizard | Interactive wizard may update `~/.local/lib/<key>/config.json` when values change; `--sync` bootstraps an empty file on install |
|
|
73
73
|
|
|
74
74
|
### Externally managed binary (Homebrew)
|
|
75
75
|
|
|
76
76
|
When **`PATH`** resolves the program key to the **running executable** (e.g. after `brew install`):
|
|
77
77
|
|
|
78
78
|
- **`configure --status`** shows `app: system (PATH)`
|
|
79
|
-
- **`--sync`** refreshes skills and MCP
|
|
79
|
+
- **`--sync`** refreshes skills and MCP; also creates `~/.local/lib/<key>/config.json` as `{}` when missing (all apps)
|
|
80
80
|
|
|
81
81
|
MCP config uses the command name on **`PATH`**, not a Cellar path.
|
|
82
82
|
|
|
@@ -110,13 +110,19 @@ Artifact keys: `chatgptMcp`, `claudeCodeMcp`, `claudeDesktopMcp`, `claudeSkill`,
|
|
|
110
110
|
|
|
111
111
|
## App config (`program.appConfig`)
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
Every app gets `~/.local/lib/<sanitized-key>/config.json` on first **`configure --sync`** (Homebrew `post_install`), even without `program.appConfig`.
|
|
114
|
+
|
|
115
|
+
When `program.appConfig` is set, ArgsBarg manages schema-driven values in that file.
|
|
114
116
|
|
|
115
117
|
| Mode | Description |
|
|
116
118
|
| --- | --- |
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
+
| `configure --sync` | Bootstraps `config.json` as `{}` when missing |
|
|
120
|
+
| Interactive `configure` | Config wizard when `entries` is non-empty; writes only when values or `_bindings` change |
|
|
121
|
+
| `--status` | Shows config path, required keys (`set` / `missing`), and binding hints (`env`, `file`, `skip`) |
|
|
119
122
|
| `--remove-config --yes` | Removes the config directory |
|
|
123
|
+
| `configure set --from-env` | Bind a key to its mapped env var (stores `_bindings`, no literal secret) |
|
|
124
|
+
|
|
125
|
+
Per-key intent is stored under the reserved `_bindings` object (e.g. `"apiToken": "env"`). Env still wins at resolve time when set; bindings record user choice and suppress re-prompts.
|
|
120
126
|
|
|
121
127
|
Export helpers from `argsbarg`: `resolveAppConfigPath`, `displayAppConfigPath`.
|
|
122
128
|
|
|
@@ -127,7 +133,7 @@ Export helpers from `argsbarg`: `resolveAppConfigPath`, `displayAppConfigPath`.
|
|
|
127
133
|
| Flag | Description |
|
|
128
134
|
| --- | --- |
|
|
129
135
|
| `--status` | Read-only inventory |
|
|
130
|
-
| `--sync` | Refresh installed agent artifacts (Homebrew `post_install`; greenfield → full sync plan) |
|
|
136
|
+
| `--sync` | Refresh installed agent artifacts; bootstrap `config.json` when missing (Homebrew `post_install`; greenfield → full sync plan) |
|
|
131
137
|
| `--remove-all` | Remove all detected agent artifacts |
|
|
132
138
|
| `--remove-config` | Remove app config directory only |
|
|
133
139
|
|
|
@@ -8,7 +8,8 @@ Argsbarg apps distribute the **binary and shell completions** through Homebrew,
|
|
|
8
8
|
| --- | --- |
|
|
9
9
|
| Binary + completions | Formula `install` block |
|
|
10
10
|
| Skills + MCP | Formula `post_install` → `{key} configure --sync --yes` |
|
|
11
|
-
| App config |
|
|
11
|
+
| App config file | Bootstrapped as `{}` on `post_install` via `--sync` (`~/.local/lib/<key>/config.json`) |
|
|
12
|
+
| App config values | User opt-in: `{key} configure` (interactive wizard when `program.appConfig` has entries) |
|
|
12
13
|
| App config cleanup | Formula `uninstall` → `{key} configure --remove-config --yes` |
|
|
13
14
|
|
|
14
15
|
**Only tap-from-repo** — in-repo `Formula/` or GitHub tap. Not Homebrew core.
|
|
@@ -89,7 +90,7 @@ Local dev formulae (`just install-local`) use a plain `file://` URL and do not n
|
|
|
89
90
|
|
|
90
91
|
Completions require users to configure their shell per [Homebrew Shell Completion](https://docs.brew.sh/Shell-Completion).
|
|
91
92
|
|
|
92
|
-
**Why
|
|
93
|
+
**Why the wizard is separate from `post_install`:** prompting for secrets requires a TTY. `post_install` only bootstraps an empty `config.json` and refreshes skills/MCP. Apps with `appConfig` print a one-line configure hint in formula `caveats` for the interactive wizard.
|
|
93
94
|
|
|
94
95
|
**MCP hosts:** when `mcpServer.enabled` is true, add a caveats line that chat apps (Cursor, Claude Desktop, etc.) must be **restarted** after `brew install` / `brew upgrade` — `post_install` updates MCP config on disk, but hosts typically load it only at startup.
|
|
95
96
|
|
|
@@ -117,9 +118,22 @@ Template source: [`examples/full-example/`](../examples/full-example/) in the ar
|
|
|
117
118
|
## Release workflow
|
|
118
119
|
|
|
119
120
|
1. `just build` → `dist/{key}`
|
|
120
|
-
2. `scripts/release.ts` → writes `Formula/{key}.rb` (GitHub URL + sha256), commits, tags, uploads `dist/{key}` to GitHub Releases
|
|
121
|
+
2. `scripts/release.ts` → zips `dist/{key}` to `dist/{key}.zip`, writes `Formula/{key}.rb` (GitHub zip URL + archive sha256), commits, tags, uploads `dist/{key}.zip` to GitHub Releases
|
|
121
122
|
3. Users `brew upgrade {key}` from the tap
|
|
122
123
|
|
|
124
|
+
Requires the `zip` CLI on the release machine. `just install-local` still stages the bare binary via `file://` (no zip).
|
|
125
|
+
|
|
126
|
+
### Stale release cleanup
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
just release --purge # delete all GitHub releases except the newest (confirm on TTY)
|
|
130
|
+
just release --purge --yes # skip confirmation
|
|
131
|
+
just release --purge --dry-run # list tags that would be deleted
|
|
132
|
+
just release patch --purge # release, then purge older releases
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`--purge` removes GitHub Release records and attached assets only — git tags remain on the remote. Old formula pins that reference deleted release URLs will fail until users upgrade.
|
|
136
|
+
|
|
123
137
|
## Removed (breaking)
|
|
124
138
|
|
|
125
139
|
- Self-install to `~/.local/bin`
|
|
@@ -20,10 +20,8 @@ build:
|
|
|
20
20
|
@rm -f .*.bun-build
|
|
21
21
|
|
|
22
22
|
# Run schemagen, git diff check, typecheck, and format
|
|
23
|
-
check: schemagen
|
|
23
|
+
check: schemagen format typecheck
|
|
24
24
|
git diff --exit-code schemas/
|
|
25
|
-
just typecheck
|
|
26
|
-
just format
|
|
27
25
|
|
|
28
26
|
# Run the CLI from source with optional args; restarts on file changes
|
|
29
27
|
dev *ARGS:
|
|
@@ -38,7 +36,7 @@ alias fmt := format
|
|
|
38
36
|
|
|
39
37
|
# Format and lint sources (auto-fix)
|
|
40
38
|
format:
|
|
41
|
-
bun run biome check ./src ./scripts --write
|
|
39
|
+
bun run biome check ./src ./scripts --write --unsafe
|
|
42
40
|
|
|
43
41
|
# Alias for backward compatibility
|
|
44
42
|
install: install-local
|
|
@@ -81,7 +79,6 @@ run *ARGS:
|
|
|
81
79
|
# Generate JSON Schema artifacts from TypeScript types
|
|
82
80
|
schemagen:
|
|
83
81
|
bun run schemagen
|
|
84
|
-
just format
|
|
85
82
|
|
|
86
83
|
# Install bun/npm dependencies
|
|
87
84
|
setup:
|
|
@@ -91,6 +88,10 @@ setup:
|
|
|
91
88
|
test: check
|
|
92
89
|
bun test .
|
|
93
90
|
|
|
91
|
+
# Bump version, build, publish; or pass --purge to delete stale GitHub releases
|
|
92
|
+
release *ARGS:
|
|
93
|
+
bun scripts/release.ts {{ARGS}}
|
|
94
|
+
|
|
94
95
|
# Install release formula from tap and run formula test
|
|
95
96
|
test-release:
|
|
96
97
|
brew untap {{tap}} 2>/dev/null || true
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
buildReleaseArchive,
|
|
9
|
+
releaseArchiveName,
|
|
10
|
+
releaseFormulaUrl,
|
|
11
|
+
selectStaleReleaseTags,
|
|
12
|
+
} from "./formula-shared.ts";
|
|
13
|
+
|
|
14
|
+
describe("releaseArchiveName", () => {
|
|
15
|
+
test("returns key.zip", () => {
|
|
16
|
+
expect(releaseArchiveName()).toBe("full-example.zip");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("releaseFormulaUrl", () => {
|
|
21
|
+
test("points at zip asset on GitHub releases", () => {
|
|
22
|
+
expect(releaseFormulaUrl("1.2.3")).toBe(
|
|
23
|
+
"https://github.com/bdombro/bun-argsbarg/releases/download/v1.2.3/full-example.zip",
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("selectStaleReleaseTags", () => {
|
|
29
|
+
test("returns empty for zero or one release", () => {
|
|
30
|
+
expect(selectStaleReleaseTags([])).toEqual([]);
|
|
31
|
+
expect(selectStaleReleaseTags([{ tagName: "v1.0.0", publishedAt: "2026-01-01T00:00:00Z" }])).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("keeps newest by publishedAt and returns the rest", () => {
|
|
35
|
+
const releases = [
|
|
36
|
+
{ tagName: "v1.0.0", publishedAt: "2026-01-01T00:00:00Z" },
|
|
37
|
+
{ tagName: "v1.1.0", publishedAt: "2026-02-01T00:00:00Z" },
|
|
38
|
+
{ tagName: "v1.0.1", publishedAt: "2026-01-15T00:00:00Z" },
|
|
39
|
+
];
|
|
40
|
+
expect(selectStaleReleaseTags(releases)).toEqual(["v1.0.1", "v1.0.0"]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("buildReleaseArchive", () => {
|
|
45
|
+
const workDirs: string[] = [];
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
for (const dir of workDirs.splice(0)) {
|
|
49
|
+
rmSync(dir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("creates zip with binary at archive root and matching sha256", async () => {
|
|
54
|
+
const work = mkdtempSync(join(tmpdir(), "argsbarg-release-zip-"));
|
|
55
|
+
workDirs.push(work);
|
|
56
|
+
const binaryPath = join(work, "full-example");
|
|
57
|
+
writeFileSync(binaryPath, "#!/bin/sh\necho hi\n", { mode: 0o755 });
|
|
58
|
+
|
|
59
|
+
const { archivePath, sha256 } = await buildReleaseArchive(binaryPath);
|
|
60
|
+
|
|
61
|
+
expect(archivePath).toBe(`${binaryPath}.zip`);
|
|
62
|
+
const onDisk = createHash("sha256").update(readFileSync(archivePath)).digest("hex");
|
|
63
|
+
expect(sha256).toBe(onDisk);
|
|
64
|
+
|
|
65
|
+
const listing = execFileSync("unzip", ["-l", archivePath], { encoding: "utf8" });
|
|
66
|
+
expect(listing).toContain("full-example");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/** Shared Ruby fragments embedded in Homebrew formulae. */
|
|
2
2
|
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { $ } from "bun";
|
|
3
6
|
import { createIdentity } from "./create-identity.ts";
|
|
4
7
|
|
|
5
8
|
const { key, className, desc, homepage, releaseRepo } = createIdentity;
|
|
6
9
|
|
|
10
|
+
/** GitHub release row used by {@link selectStaleReleaseTags}. */
|
|
11
|
+
export interface ReleaseTag {
|
|
12
|
+
tagName: string;
|
|
13
|
+
publishedAt: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
export const formulaInstallRuby = `def install
|
|
8
17
|
bin.install "${key}"
|
|
9
18
|
chmod 0755, bin/"${key}"
|
|
@@ -36,11 +45,40 @@ export interface FormulaCoords {
|
|
|
36
45
|
url: string;
|
|
37
46
|
urlStanza: string;
|
|
38
47
|
version: string;
|
|
48
|
+
/** SHA-256 hex digest of the release archive at `url` (zip for GitHub releases). */
|
|
39
49
|
sha256: string;
|
|
40
50
|
/** When true, embed {@link githubPrivateReleaseDownloadStrategyRuby} in the formula class. */
|
|
41
51
|
privateRelease?: boolean;
|
|
42
52
|
}
|
|
43
53
|
|
|
54
|
+
/** Release asset filename on GitHub (zip containing the bare binary at archive root). */
|
|
55
|
+
export function releaseArchiveName(): string {
|
|
56
|
+
return `${key}.zip`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Build `{binaryPath}.zip` from a compiled binary; return archive path and sha256 of the zip. */
|
|
60
|
+
export async function buildReleaseArchive(binaryPath: string): Promise<{ archivePath: string; sha256: string }> {
|
|
61
|
+
const archivePath = `${binaryPath}.zip`;
|
|
62
|
+
const result = await $`zip -j -9 ${archivePath} ${binaryPath}`.nothrow();
|
|
63
|
+
if (result.exitCode !== 0) {
|
|
64
|
+
throw new Error(`zip failed: ${result.stderr}`);
|
|
65
|
+
}
|
|
66
|
+
const sha256 = createHash("sha256").update(readFileSync(archivePath)).digest("hex");
|
|
67
|
+
return { archivePath, sha256 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Tags to delete when keeping only the newest release (sorted by `publishedAt` desc). */
|
|
71
|
+
export function selectStaleReleaseTags(releases: ReleaseTag[]): string[] {
|
|
72
|
+
if (releases.length <= 1) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const sorted = [...releases].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt));
|
|
76
|
+
return sorted.slice(1).map((r) => r.tagName);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** GitHub `org/repo` slug for release and purge commands. */
|
|
80
|
+
export const releaseRepoSlug = releaseRepo;
|
|
81
|
+
|
|
44
82
|
/** Resolves private GitHub release assets via the API at download time. */
|
|
45
83
|
export const githubPrivateReleaseDownloadStrategyRuby = ` class GitHubPrivateReleaseDownloadStrategy < CurlDownloadStrategy
|
|
46
84
|
def initialize(url, name, version, **meta)
|
|
@@ -82,7 +120,7 @@ export function releaseUrlStanza(url: string): string {
|
|
|
82
120
|
}
|
|
83
121
|
|
|
84
122
|
export function releaseFormulaUrl(version: string): string {
|
|
85
|
-
return `https://github.com/${releaseRepo}/releases/download/v${version}/${
|
|
123
|
+
return `https://github.com/${releaseRepo}/releases/download/v${version}/${releaseArchiveName()}`;
|
|
86
124
|
}
|
|
87
125
|
|
|
88
126
|
export function renderFormula(coords: FormulaCoords): string {
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Bump version, build, publish zip release; optional `--purge` to drop stale GitHub releases. */
|
|
3
|
+
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
+
import * as readline from "node:readline/promises";
|
|
7
|
+
import { $ } from "bun";
|
|
8
|
+
import { createIdentity } from "./create-identity.ts";
|
|
9
|
+
import {
|
|
10
|
+
buildReleaseArchive,
|
|
11
|
+
type ReleaseTag,
|
|
12
|
+
releaseRepoSlug,
|
|
13
|
+
renderReleaseFormula,
|
|
14
|
+
selectStaleReleaseTags,
|
|
15
|
+
} from "./formula-shared.ts";
|
|
16
|
+
|
|
17
|
+
const { key } = createIdentity;
|
|
18
|
+
const formulaPath = `Formula/${key}.rb`;
|
|
19
|
+
const binaryPath = `dist/${key}`;
|
|
20
|
+
const programPath = "src/program.ts";
|
|
21
|
+
|
|
22
|
+
/** Allowed semver bump kinds for `scripts/release.ts`. */
|
|
23
|
+
type Bump = "major" | "minor" | "patch";
|
|
24
|
+
|
|
25
|
+
interface ReleaseOptions {
|
|
26
|
+
bump?: Bump;
|
|
27
|
+
purge: boolean;
|
|
28
|
+
yes: boolean;
|
|
29
|
+
dryRun: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Entry point: release bump, purge-only, or release then purge. */
|
|
33
|
+
async function main(): Promise<void> {
|
|
34
|
+
const options = parseOptions(process.argv.slice(2));
|
|
35
|
+
if (options.purge && !options.bump) {
|
|
36
|
+
await purgeStaleReleases(options);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!options.bump) {
|
|
40
|
+
usage();
|
|
41
|
+
}
|
|
42
|
+
await runRelease(options.bump, options);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Prints usage and exits. */
|
|
46
|
+
function usage(): never {
|
|
47
|
+
process.stderr.write(
|
|
48
|
+
"Usage:\n" +
|
|
49
|
+
" bun scripts/release.ts <major|minor|patch> [--purge]\n" +
|
|
50
|
+
" bun scripts/release.ts --purge [--yes] [--dry-run]\n",
|
|
51
|
+
);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Parses argv into release options. */
|
|
56
|
+
function parseOptions(argv: string[]): ReleaseOptions {
|
|
57
|
+
const yes = argv.includes("--yes");
|
|
58
|
+
const dryRun = argv.includes("--dry-run");
|
|
59
|
+
const purge = argv.includes("--purge");
|
|
60
|
+
const bump = argv.find((a): a is Bump => a === "major" || a === "minor" || a === "patch");
|
|
61
|
+
for (const arg of argv) {
|
|
62
|
+
if (arg.startsWith("--") && arg !== "--purge" && arg !== "--yes" && arg !== "--dry-run") {
|
|
63
|
+
usage();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!purge && !bump) {
|
|
67
|
+
usage();
|
|
68
|
+
}
|
|
69
|
+
return { bump, purge, yes, dryRun };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Full release pipeline for a semver bump. */
|
|
73
|
+
async function runRelease(bump: Bump, options: ReleaseOptions): Promise<void> {
|
|
74
|
+
const testResult = await $`just test`.nothrow();
|
|
75
|
+
if (testResult.exitCode !== 0) process.exit(testResult.exitCode);
|
|
76
|
+
|
|
77
|
+
const currentVersion = readCurrentVersion();
|
|
78
|
+
const newVersion = applyBump(currentVersion, bump);
|
|
79
|
+
console.log(`Releasing ${currentVersion} → ${newVersion}`);
|
|
80
|
+
|
|
81
|
+
updateVersion(newVersion);
|
|
82
|
+
updateChangelog(newVersion);
|
|
83
|
+
|
|
84
|
+
const buildResult = await $`just build`.nothrow();
|
|
85
|
+
if (buildResult.exitCode !== 0) process.exit(buildResult.exitCode);
|
|
86
|
+
|
|
87
|
+
const archivePath = await updateReleaseFormula(newVersion);
|
|
88
|
+
|
|
89
|
+
const docgenResult = await $`just docgen`.nothrow();
|
|
90
|
+
if (docgenResult.exitCode !== 0) process.exit(docgenResult.exitCode);
|
|
91
|
+
|
|
92
|
+
await commitAndTag(newVersion);
|
|
93
|
+
await createGithubRelease(`v${newVersion}`, archivePath);
|
|
94
|
+
|
|
95
|
+
console.log(`Released v${newVersion}`);
|
|
96
|
+
|
|
97
|
+
if (options.purge) {
|
|
98
|
+
await purgeStaleReleases(options);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Applies a semver bump to `current` and returns the new version string. */
|
|
103
|
+
function applyBump(current: string, bump: Bump): string {
|
|
104
|
+
const [major, minor, patch] = current.split(".").map(Number) as [number, number, number];
|
|
105
|
+
if (bump === "major") return `${major + 1}.0.0`;
|
|
106
|
+
if (bump === "minor") return `${major}.${minor + 1}.0`;
|
|
107
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Commits all staged changes, creates an annotated tag, and pushes both to origin. */
|
|
111
|
+
async function commitAndTag(newVersion: string): Promise<void> {
|
|
112
|
+
await $`git add -A`;
|
|
113
|
+
await $`git commit -m ${`chore: release v${newVersion}`}`;
|
|
114
|
+
await $`git tag v${newVersion}`;
|
|
115
|
+
await $`git push`;
|
|
116
|
+
await $`git push origin v${newVersion}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Creates a GitHub release for `tag` with the zip archive attached. */
|
|
120
|
+
async function createGithubRelease(tag: string, archivePath: string): Promise<void> {
|
|
121
|
+
await $`gh release create ${tag} ${archivePath} --title ${tag} --generate-notes`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Reads the CLI version from `src/program.ts`. */
|
|
125
|
+
function readCurrentVersion(): string {
|
|
126
|
+
const content = fs.readFileSync(programPath, "utf-8");
|
|
127
|
+
const match = /version:\s*"([^"]+)"/.exec(content);
|
|
128
|
+
if (!match) {
|
|
129
|
+
process.stderr.write(`Could not read version from ${programPath}\n`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const version = match[1];
|
|
133
|
+
if (!version) {
|
|
134
|
+
process.stderr.write(`Could not read version from ${programPath}\n`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const parts = version.split(".").map(Number);
|
|
138
|
+
if (parts.length !== 3 || parts.some(Number.isNaN)) {
|
|
139
|
+
process.stderr.write(`Invalid semver in ${programPath}: ${version}\n`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
return version;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Promotes `[Unreleased]` to a dated version section in `CHANGELOG.md`. */
|
|
146
|
+
function updateChangelog(newVersion: string): void {
|
|
147
|
+
const changelogPath = "CHANGELOG.md";
|
|
148
|
+
const content = fs.readFileSync(changelogPath, "utf-8");
|
|
149
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
150
|
+
fs.writeFileSync(
|
|
151
|
+
changelogPath,
|
|
152
|
+
content.replace(/^## \[Unreleased\]/m, `## [Unreleased]\n\n## [${newVersion}] - ${date}`),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Overwrites the version literal in `src/program.ts`. */
|
|
157
|
+
function updateVersion(newVersion: string): void {
|
|
158
|
+
const content = fs.readFileSync(programPath, "utf-8");
|
|
159
|
+
fs.writeFileSync(programPath, content.replace(/version:\s*"[^"]+"/, `version: "${newVersion}"`));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Writes the release formula with zip URL and archive sha256; returns the archive path. */
|
|
163
|
+
async function updateReleaseFormula(version: string): Promise<string> {
|
|
164
|
+
const { archivePath, sha256 } = await buildReleaseArchive(binaryPath);
|
|
165
|
+
fs.writeFileSync(formulaPath, renderReleaseFormula(version, sha256));
|
|
166
|
+
return archivePath;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Deletes all GitHub releases except the most recent. */
|
|
170
|
+
async function purgeStaleReleases(options: ReleaseOptions): Promise<void> {
|
|
171
|
+
const list = await $`gh release list -R ${releaseRepoSlug} --json tagName,publishedAt`.nothrow();
|
|
172
|
+
if (list.exitCode !== 0) process.exit(list.exitCode);
|
|
173
|
+
|
|
174
|
+
const releases = JSON.parse(list.stdout.toString()) as ReleaseTag[];
|
|
175
|
+
const toDelete = selectStaleReleaseTags(releases);
|
|
176
|
+
|
|
177
|
+
if (toDelete.length === 0) {
|
|
178
|
+
console.log("No stale releases to delete.");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`Will delete ${toDelete.length} release(s):`);
|
|
183
|
+
for (const tag of toDelete) {
|
|
184
|
+
console.log(` ${tag}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (options.dryRun) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!options.yes) {
|
|
192
|
+
if (!input.isTTY) {
|
|
193
|
+
process.stderr.write("Not a TTY; pass --yes to confirm purge.\n");
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
const rl = readline.createInterface({ input, output });
|
|
197
|
+
const answer = await rl.question("Delete these releases? [y/N] ");
|
|
198
|
+
rl.close();
|
|
199
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
200
|
+
console.log("Aborted.");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const tag of toDelete) {
|
|
206
|
+
const del = await $`gh release delete ${tag} -R ${releaseRepoSlug} --yes`.nothrow();
|
|
207
|
+
if (del.exitCode !== 0) process.exit(del.exitCode);
|
|
208
|
+
console.log(`Deleted ${tag}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await main();
|
package/index.d.ts
CHANGED
|
@@ -7,11 +7,15 @@ export declare function displayAppConfigPath(program: CliProgram): string;
|
|
|
7
7
|
export type ResolvedConfig = Record<string, unknown>;
|
|
8
8
|
declare class EmptyAppConfigSnapshot {
|
|
9
9
|
private readonly program;
|
|
10
|
-
|
|
10
|
+
private fileData;
|
|
11
|
+
constructor(program: CliProgram, fileData?: Record<string, unknown>);
|
|
11
12
|
get(_key: string): undefined;
|
|
12
13
|
require(key: string): never;
|
|
13
14
|
set(_key: string, _value: unknown): void;
|
|
14
15
|
read(): ResolvedConfig;
|
|
16
|
+
readUnsafe(): Record<string, unknown>;
|
|
17
|
+
getUnsafe(key: string): unknown;
|
|
18
|
+
setUnsafe(key: string, value: unknown): void;
|
|
15
19
|
/** Resolved absolute path to the app JSON config file (OS default from `program.key`). */
|
|
16
20
|
get path(): string;
|
|
17
21
|
/** Resolved absolute directory containing the config file. */
|
|
@@ -26,13 +30,18 @@ declare class AppConfigSnapshot {
|
|
|
26
30
|
require(key: string): unknown;
|
|
27
31
|
set(key: string, value: unknown): void;
|
|
28
32
|
read(): ResolvedConfig;
|
|
33
|
+
readUnsafe(): Record<string, unknown>;
|
|
34
|
+
getUnsafe(key: string): unknown;
|
|
35
|
+
setUnsafe(key: string, value: unknown): void;
|
|
29
36
|
/** Resolved absolute path to the app JSON config file (`~/.local/lib/<key>/config.json`). */
|
|
30
37
|
get path(): string;
|
|
31
38
|
/** Resolved absolute directory containing the config file. */
|
|
32
39
|
get dir(): string;
|
|
33
40
|
/** Replace snapshot after external bootstrap (internal). */
|
|
34
41
|
refresh(fileData: Record<string, unknown>, resolved: ResolvedConfig): void;
|
|
42
|
+
private persistFileData;
|
|
35
43
|
private assertEntryKey;
|
|
44
|
+
private assertUnsafeKey;
|
|
36
45
|
}
|
|
37
46
|
export type AnyAppConfigSnapshot = AppConfigSnapshot | EmptyAppConfigSnapshot;
|
|
38
47
|
/** Coerced leaf inputs keyed by option and positional names. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "argsbarg",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"bun": ">=1.3"
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@biomejs/biome": "^2.5.0",
|
|
34
34
|
"@types/bun": "^1.3.12",
|
|
35
|
+
"dts-bundle-generator": "^9.5.1",
|
|
35
36
|
"typescript": "^5.9.3"
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -52,9 +52,7 @@ describe("builtins help copy", () => {
|
|
|
52
52
|
|
|
53
53
|
test("configure copy omits MCP when mcpServer unset", () => {
|
|
54
54
|
const caps = resolveCapabilities(noMcp);
|
|
55
|
-
expect(configureCommandDescription(noMcp, caps)).toBe(
|
|
56
|
-
"Set up agent skills for this app (binary via Homebrew).",
|
|
57
|
-
);
|
|
55
|
+
expect(configureCommandDescription(noMcp, caps)).toBe("Set up agent skills for this app (binary via Homebrew).");
|
|
58
56
|
expect(configureCommandDescription(noMcp, caps)).not.toContain("MCP");
|
|
59
57
|
expect(configureSyncOptionDescription(noMcp, caps)).not.toContain("MCP");
|
|
60
58
|
const configure = cliBuiltinConfigureCommand(noMcp);
|