argsbarg 5.1.5 → 5.1.7
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 +17 -5
- package/README.md +5 -3
- package/docs/config-schema.md +1 -1
- package/docs/configure.md +19 -5
- package/docs/distribution-homebrew.md +20 -20
- package/examples/full-example/README.md +10 -15
- package/examples/full-example/justfile +2 -8
- package/examples/full-example/scripts/formula-shared.ts +6 -0
- package/examples/full-example/scripts/with-dev-formula.ts +41 -2
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +0 -1
- package/src/builtins/config.ts +2 -1
- package/src/builtins/configure-copy.ts +2 -0
- package/src/config/bootstrap.ts +30 -7
- package/src/config/validate.test.ts +42 -1
- package/src/config/validate.ts +153 -15
- package/examples/full-example/scripts/dev-formula.ts +0 -54
- package/examples/full-example/scripts/ensure-release-formula.ts +0 -41
- package/examples/full-example/scripts/gen-dev-formula.ts +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -7,18 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.1.7] - 2026-07-05
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Homebrew install docs** — private taps: `brew install gh` and `gh auth login` only; removed legacy token env-var setup from docs and justfiles (template + consumers).
|
|
15
|
+
- **App config array input** — interactive `configure` and `configure set` parse homogeneous primitive arrays from comma-separated values or JSON literals; objects and nested arrays still require `--json`.
|
|
16
|
+
- **Homebrew formula `uninstall`** — generated formulae run `{key} configure --remove-config --yes` before the keg is removed (template + docs).
|
|
17
|
+
|
|
18
|
+
## [5.1.6] - 2026-07-05
|
|
19
|
+
|
|
20
|
+
|
|
10
21
|
## [5.1.5] - 2026-07-05
|
|
11
22
|
|
|
12
23
|
### Changed
|
|
13
24
|
|
|
14
|
-
- **`just install-local`** — back up release `Formula/{key}.rb`, write dev formula for `brew install`, then restore
|
|
15
|
-
- **`just ensure-release-formula`** — normalize release formula layout; restore from install-local backup or fail with `git restore` hint if a dev `file://` formula is left on disk.
|
|
25
|
+
- **`just install-local`** — back up release `Formula/{key}.rb`, write dev formula for `brew install`, then restore (`scripts/with-dev-formula.ts`; dropped `gen-dev-formula.ts` / `dev-formula.ts`).
|
|
16
26
|
|
|
17
27
|
## [5.1.4] - 2026-07-04
|
|
18
28
|
|
|
19
29
|
### Changed
|
|
20
30
|
|
|
21
|
-
- **Template justfile** —
|
|
31
|
+
- **Template justfile** — removed `scripts/ensure-brew-github-token.sh` and `HOMEBREW_NO_*` suppress vars.
|
|
22
32
|
|
|
23
33
|
## [5.1.3] - 2026-07-04
|
|
24
34
|
|
|
@@ -26,7 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
26
36
|
|
|
27
37
|
### Changed
|
|
28
38
|
|
|
29
|
-
- **Private GitHub release installs** — release formulae use `GitHubPrivateReleaseDownloadStrategy` (GitHub API + `GitHub::API.credentials`
|
|
39
|
+
- **Private GitHub release installs** — release formulae use `GitHubPrivateReleaseDownloadStrategy` (GitHub API + `GitHub::API.credentials`); documented in `docs/distribution-homebrew.md`.
|
|
30
40
|
- **Homebrew install docs** — README template and consumer READMEs document private-tap install; `configure` CLI notes stay post-install only.
|
|
31
41
|
|
|
32
42
|
## [5.1.1] - 2026-07-04
|
|
@@ -617,7 +627,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
|
|
|
617
627
|
- 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`).
|
|
618
628
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
619
629
|
|
|
620
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v5.1.
|
|
630
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v5.1.7...HEAD
|
|
631
|
+
[5.1.7]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.7
|
|
632
|
+
[5.1.6]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.6
|
|
621
633
|
[5.1.5]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.5
|
|
622
634
|
[5.1.4]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.4
|
|
623
635
|
[5.1.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.3
|
package/README.md
CHANGED
|
@@ -112,11 +112,13 @@ See **[docs/mcp.md](docs/mcp.md)** for configuration, env bootstrapping, custom
|
|
|
112
112
|
|
|
113
113
|
### Configure CLI
|
|
114
114
|
|
|
115
|
-
Ship via **Homebrew** (tap-from-repo). The formula installs the binary and shell completions; `post_install` runs agent artifact refresh. Private taps
|
|
115
|
+
Ship via **Homebrew** (tap-from-repo). The formula installs the binary and shell completions; `post_install` runs agent artifact refresh. Private taps require `gh auth login` — see [docs/distribution-homebrew.md](docs/distribution-homebrew.md#end-user-install).
|
|
116
116
|
|
|
117
117
|
```bash
|
|
118
|
+
brew install gh
|
|
119
|
+
gh auth login
|
|
118
120
|
brew tap <org>/<repo> git@github.com:<org>/<repo>.git
|
|
119
|
-
|
|
121
|
+
brew install <tap>/myapp
|
|
120
122
|
myapp configure # interactive per-target setup; opt-in app config wizard
|
|
121
123
|
```
|
|
122
124
|
|
|
@@ -264,7 +266,7 @@ To refresh the Cursor rule in an existing consumer: `bun scripts/merge-cli-progr
|
|
|
264
266
|
| Command layout | `src/commands/<name>/command.ts`; registration in `src/program.ts` |
|
|
265
267
|
| MCP doc topics | `docs.topics` auto-exposed as `<key>://docs/<topic>` resources when docs + MCP enabled |
|
|
266
268
|
| Package import | `from "argsbarg"` (not relative to argsbarg `src/`) |
|
|
267
|
-
| Homebrew distribution | `scripts/formula-shared.ts`, `scripts/
|
|
269
|
+
| Homebrew distribution | `scripts/formula-shared.ts`, `scripts/with-dev-formula.ts`, `Formula/`, `justfile` |
|
|
268
270
|
| Dev tooling | Biome (`just format` / `just lint`), TypeScript, colocated tests |
|
|
269
271
|
| Cursor rules | `.cursor/rules/cli-program.mdc`, `.cursor/rules/code.mdc` |
|
|
270
272
|
|
package/docs/config-schema.md
CHANGED
|
@@ -202,7 +202,7 @@ When `program.appConfig` is set and `commands !== false`:
|
|
|
202
202
|
|
|
203
203
|
`configure get`/`set` skip required-config exit and TTY prompts. Sensitive values redact on `get` (`REDACTED` / `{ "set": true }` with `--json`).
|
|
204
204
|
|
|
205
|
-
Object/array/`$ref` properties require `--json` on `configure set`.
|
|
205
|
+
Object/array/`$ref` properties require `--json` on `configure set` when comma-separated or JSON-literal input does not apply (e.g. objects, arrays of objects). Homogeneous primitive arrays (`string[]`, `number[]`, `integer[]`, `boolean[]`, and `string[]` with `format: date` / `date-time` on items) accept comma-separated values or a JSON array in both `configure set` and interactive `configure`.
|
|
206
206
|
|
|
207
207
|
## Example in this repo
|
|
208
208
|
|
package/docs/configure.md
CHANGED
|
@@ -6,15 +6,15 @@ Opt out with `configure: { enabled: false }` on the program root.
|
|
|
6
6
|
|
|
7
7
|
## End-user install (Homebrew)
|
|
8
8
|
|
|
9
|
-
Private GitHub release downloads require
|
|
9
|
+
Private GitHub release downloads require [GitHub CLI](https://cli.github.com/) authentication for `brew install` and `brew upgrade`. See [distribution-homebrew.md](distribution-homebrew.md#end-user-install) (`brew install gh`, then `gh auth login`).
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
brew tap <org>/<repo> git@github.com:<org>/<repo>.git
|
|
13
|
-
|
|
13
|
+
brew install <tap>/<key>
|
|
14
14
|
<key> configure # interactive: per-target prompts; run when app config is required
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Upgrade with `
|
|
17
|
+
Upgrade with `brew upgrade <key>`. Shell completions are installed by Homebrew during `brew install`. Users must configure their shell per [Homebrew Shell Completion](https://docs.brew.sh/Shell-Completion).
|
|
18
18
|
|
|
19
19
|
**Uninstall the binary:** remove agent artifacts first (while the CLI is still on PATH), then `brew uninstall`:
|
|
20
20
|
|
|
@@ -23,11 +23,13 @@ Upgrade with `HOMEBREW_GITHUB_API_TOKEN="$(gh auth token)" brew upgrade <key>`.
|
|
|
23
23
|
brew uninstall <tap>/<key>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
`brew uninstall` runs the formula `uninstall` hook, which removes app config (`configure --remove-config --yes`). Skills and MCP entries are not removed by Homebrew — use `--remove-all` before uninstall when you want those gone too.
|
|
27
|
+
|
|
26
28
|
## Developer install
|
|
27
29
|
|
|
28
30
|
```bash
|
|
29
31
|
just build
|
|
30
|
-
just install-local # same formula as production;
|
|
32
|
+
just install-local # same formula as production; temporary file:// URL during brew install (`just install` is an alias)
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
Dev flow matches release: formula `install` copies the binary and generates completions; `post_install` runs `<key> configure --sync --yes` for skills/MCP. Use `just reinstall-local` to swap the binary into Cellar during tight edit cycles (skips completions and `post_install`). Use `just sync-artifacts` to refresh agent artifacts without touching the binary.
|
|
@@ -67,7 +69,7 @@ Non-interactive / CI: pass **`--yes`** (or **`--json`**, **`--sync`**, **`--remo
|
|
|
67
69
|
| Claude skill | Y/n prompt | `~/.claude/skills/<dir>/` when `~/.claude` exists |
|
|
68
70
|
| Codex / OpenCode / OpenClaw skills | Y/n prompt | Agent-specific dirs when available |
|
|
69
71
|
| MCP config | Y/n prompt when `mcpServer.enabled` | Cursor, Claude Code/Desktop, OpenCode, Codex, OpenClaw, ChatGPT desktop |
|
|
70
|
-
| App config | auto-runs wizard | Interactive wizard writes `~/.local/lib/<key>/config.json` |
|
|
72
|
+
| App config | auto-runs wizard | Interactive wizard writes `~/.local/lib/<key>/config.json` (schema-aware: comma-separated or JSON for primitive arrays) |
|
|
71
73
|
|
|
72
74
|
### Externally managed binary (Homebrew)
|
|
73
75
|
|
|
@@ -163,6 +165,18 @@ end
|
|
|
163
165
|
|
|
164
166
|
This refreshes skills/MCP without running the configure wizard (app config is opt-in via interactive `configure`).
|
|
165
167
|
|
|
168
|
+
## Formula `uninstall`
|
|
169
|
+
|
|
170
|
+
Release formulae should run:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
def uninstall
|
|
174
|
+
system bin/"myapp", "configure", "--remove-config", "--yes"
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Homebrew calls this before removing the keg, so the binary is still on PATH. Safe no-op when app config was never created or `program.appConfig` is unset.
|
|
179
|
+
|
|
166
180
|
## Bootstrapping a new CLI
|
|
167
181
|
|
|
168
182
|
```bash
|
|
@@ -9,38 +9,34 @@ Argsbarg apps distribute the **binary and shell completions** through Homebrew,
|
|
|
9
9
|
| Binary + completions | Formula `install` block |
|
|
10
10
|
| Skills + MCP | Formula `post_install` → `{key} configure --sync --yes` |
|
|
11
11
|
| App config | User opt-in: `{key} configure` (interactive; not run from formula `post_install`) |
|
|
12
|
+
| App config cleanup | Formula `uninstall` → `{key} configure --remove-config --yes` |
|
|
12
13
|
|
|
13
14
|
**Only tap-from-repo** — in-repo `Formula/` or GitHub tap. Not Homebrew core.
|
|
14
15
|
|
|
15
16
|
### End-user install
|
|
16
17
|
|
|
17
|
-
For **private or internal GitHub taps**,
|
|
18
|
-
|
|
19
|
-
If [GitHub CLI](https://cli.github.com/) is installed:
|
|
18
|
+
For **private or internal GitHub taps**, authenticate with GitHub CLI before `brew install` or `brew upgrade` (release formulae download via the GitHub API; Homebrew discovers credentials from `gh auth login`).
|
|
20
19
|
|
|
21
20
|
```bash
|
|
22
|
-
brew
|
|
23
|
-
|
|
24
|
-
{key} configure # when app config is required
|
|
21
|
+
brew install gh # skip if already installed
|
|
22
|
+
gh auth login # skip if already authenticated
|
|
25
23
|
```
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- Resource owner — your org (e.g. sqsp)
|
|
30
|
-
- Repository access — the tap repo or all repositories
|
|
31
|
-
- Permissions — **Contents** (read-only)
|
|
25
|
+
Then:
|
|
32
26
|
|
|
33
27
|
```bash
|
|
34
|
-
|
|
28
|
+
brew tap <org>/<repo> git@github.com:<org>/<repo>.git
|
|
29
|
+
brew install <tap>/{key}
|
|
30
|
+
{key} configure # when app config is required
|
|
35
31
|
```
|
|
36
32
|
|
|
37
33
|
Upgrade:
|
|
38
34
|
|
|
39
35
|
```bash
|
|
40
|
-
|
|
36
|
+
brew upgrade {key}
|
|
41
37
|
```
|
|
42
38
|
|
|
43
|
-
Local dev installs (`just install-local`) use a `file://` staging URL and do not need
|
|
39
|
+
Local dev installs (`just install-local`) use a `file://` staging URL and do not need GitHub authentication.
|
|
44
40
|
|
|
45
41
|
### Developer install
|
|
46
42
|
|
|
@@ -48,7 +44,7 @@ Local dev installs (`just install-local`) use a `file://` staging URL and do not
|
|
|
48
44
|
just build
|
|
49
45
|
just install-local # or `just install` (alias)
|
|
50
46
|
just reinstall-local # fast binary swap (`install -m 755` into Cellar; run install-local first)
|
|
51
|
-
just uninstall # undo formula + agent artifacts (
|
|
47
|
+
just uninstall # undo formula + agent artifacts (app config removed by formula uninstall)
|
|
52
48
|
```
|
|
53
49
|
|
|
54
50
|
Dev and release use the **same formula file** (`Formula/{key}.rb`, class name, install/post_install/test). `just install-local` temporarily overwrites it with a `file://` dev formula (staged under `Formula/.staging/`), runs `brew install`, then **restores** the release formula from backup. Release bumps rewrite the GitHub URL via `scripts/release.ts`.
|
|
@@ -57,13 +53,13 @@ Dev and release use the **same formula file** (`Formula/{key}.rb`, class name, i
|
|
|
57
53
|
|
|
58
54
|
| Recipe | Removes |
|
|
59
55
|
| --- | --- |
|
|
60
|
-
| `just uninstall` | Formula `{key}` + tap symlink + skills/MCP |
|
|
56
|
+
| `just uninstall` | Formula `{key}` + tap symlink + skills/MCP; app config via formula `uninstall` |
|
|
61
57
|
| `just uninstall-config` | App config file only (`configure --remove-config --yes`) |
|
|
62
|
-
| `just uninstall-release` | Release formula from `{tap}` (keeps tap) |
|
|
63
|
-
| `just uninstall-release-tap` | Release formula + `brew untap {tap}` |
|
|
58
|
+
| `just uninstall-release` | Release formula from `{tap}` (keeps tap; app config via formula `uninstall`) |
|
|
59
|
+
| `just uninstall-release-tap` | Release formula + `brew untap {tap}` (app config via formula `uninstall`) |
|
|
64
60
|
| `just test-release` | Install release formula and run formula test |
|
|
65
61
|
|
|
66
|
-
End users: `<key> configure --remove-all --yes` then `brew uninstall <tap>/<key>`.
|
|
62
|
+
End users: `<key> configure --remove-all --yes` then `brew uninstall <tap>/<key>`. `brew uninstall` also removes app config via the formula `uninstall` hook (skills/MCP are not removed automatically).
|
|
67
63
|
|
|
68
64
|
## Formula pattern
|
|
69
65
|
|
|
@@ -76,9 +72,13 @@ end
|
|
|
76
72
|
def post_install
|
|
77
73
|
system bin/"{key}", "configure", "--sync", "--yes"
|
|
78
74
|
end
|
|
75
|
+
|
|
76
|
+
def uninstall
|
|
77
|
+
system bin/"{key}", "configure", "--remove-config", "--yes"
|
|
78
|
+
end
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Release formulae generated by `scripts/formula-shared.ts` embed a `GitHubPrivateReleaseDownloadStrategy` that resolves the release asset through the GitHub API at download time and authenticates with `GitHub::API.credentials` (
|
|
81
|
+
Release formulae generated by `scripts/formula-shared.ts` embed a `GitHubPrivateReleaseDownloadStrategy` that resolves the release asset through the GitHub API at download time and authenticates with `GitHub::API.credentials` (Homebrew discovers `gh auth login` automatically):
|
|
82
82
|
|
|
83
83
|
```ruby
|
|
84
84
|
url "https://github.com/<org>/<repo>/releases/download/vX.Y.Z/{key}",
|
|
@@ -22,30 +22,25 @@ Requires [Homebrew](https://brew.sh).
|
|
|
22
22
|
|
|
23
23
|
### End users
|
|
24
24
|
|
|
25
|
-
Private GitHub release downloads require
|
|
26
|
-
|
|
27
|
-
If [GitHub CLI](https://cli.github.com/) is installed:
|
|
25
|
+
Private GitHub release downloads require [GitHub CLI](https://cli.github.com/) authentication. Run once before `brew install` or `brew upgrade`:
|
|
28
26
|
|
|
29
27
|
```bash
|
|
30
|
-
brew
|
|
31
|
-
|
|
32
|
-
full-example configure
|
|
28
|
+
brew install gh # skip if already installed
|
|
29
|
+
gh auth login # skip if already authenticated
|
|
33
30
|
```
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- Resource owner — your org
|
|
38
|
-
- Repository access — this repo (or all repositories)
|
|
39
|
-
- Permissions — **Contents** (read-only)
|
|
32
|
+
Install:
|
|
40
33
|
|
|
41
34
|
```bash
|
|
42
|
-
|
|
35
|
+
brew tap bdombro/bun-argsbarg git@github.com:bdombro/bun-argsbarg.git
|
|
36
|
+
brew install bdombro/bun-argsbarg/full-example
|
|
37
|
+
full-example configure
|
|
43
38
|
```
|
|
44
39
|
|
|
45
40
|
Upgrade:
|
|
46
41
|
|
|
47
42
|
```bash
|
|
48
|
-
|
|
43
|
+
brew upgrade full-example
|
|
49
44
|
```
|
|
50
45
|
|
|
51
46
|
Shell completions install during `brew install`. See [Homebrew Shell Completion](https://docs.brew.sh/Shell-Completion).
|
|
@@ -59,11 +54,11 @@ brew install just bun
|
|
|
59
54
|
just setup
|
|
60
55
|
just install # build + local dev formula
|
|
61
56
|
just reinstall-local # fast binary swap during development
|
|
62
|
-
just install-production # remote tap install (
|
|
57
|
+
just install-production # remote tap install (requires gh auth login)
|
|
63
58
|
just test-release
|
|
64
59
|
```
|
|
65
60
|
|
|
66
|
-
Undo a local dev install: `just uninstall` (formula + agent artifacts), `just uninstall-config` (app config only).
|
|
61
|
+
Undo a local dev install: `just uninstall` (formula + agent artifacts; app config removed by formula `uninstall`), `just uninstall-config` (app config only, without uninstalling the formula).
|
|
67
62
|
|
|
68
63
|
## Schemagen markers
|
|
69
64
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
set shell := ["bash", "-eu", "-o", "pipefail", "-c"]
|
|
2
2
|
|
|
3
|
-
export HOMEBREW_GITHUB_API_TOKEN := `gh auth token`
|
|
4
|
-
|
|
5
3
|
cli_key := `bun scripts/print-identity.ts key`
|
|
6
4
|
tap_org := `bun scripts/print-identity.ts tapOrg`
|
|
7
5
|
tap_repo := `bun scripts/print-identity.ts tapRepo`
|
|
@@ -49,12 +47,8 @@ install: install-local
|
|
|
49
47
|
sync-artifacts:
|
|
50
48
|
just run configure --sync --yes
|
|
51
49
|
|
|
52
|
-
# Normalize release formula (fix leftover dev formula or stale layout)
|
|
53
|
-
ensure-release-formula:
|
|
54
|
-
bun scripts/ensure-release-formula.ts
|
|
55
|
-
|
|
56
50
|
# Dev install: build, dev formula (restored after brew), symlink tap, brew install
|
|
57
|
-
install-local: build
|
|
51
|
+
install-local: build
|
|
58
52
|
@brew untap {{tap}} 2>/dev/null || true
|
|
59
53
|
mkdir -p {{tap_parent}}
|
|
60
54
|
ln -sfn '{{justfile_directory()}}' {{tap_path}}
|
|
@@ -62,7 +56,7 @@ install-local: build ensure-release-formula
|
|
|
62
56
|
@echo ""
|
|
63
57
|
@echo "Next: {{cli_key}} configure"
|
|
64
58
|
|
|
65
|
-
# Remove local dev install, then install from GitHub tap (
|
|
59
|
+
# Remove local dev install, then install from GitHub tap (requires gh auth login)
|
|
66
60
|
install-production: uninstall
|
|
67
61
|
brew tap {{release_repo}} {{tap_git_url}}
|
|
68
62
|
brew install --formula {{release_repo}}/{{cli_key}}
|
|
@@ -14,6 +14,10 @@ export const formulaPostInstallRuby = `def post_install
|
|
|
14
14
|
system bin/"${key}", "configure", "--sync", "--yes"
|
|
15
15
|
end`;
|
|
16
16
|
|
|
17
|
+
export const formulaUninstallRuby = `def uninstall
|
|
18
|
+
system bin/"${key}", "configure", "--remove-config", "--yes"
|
|
19
|
+
end`;
|
|
20
|
+
|
|
17
21
|
export const formulaCaveatsRuby = `def caveats
|
|
18
22
|
<<~EOS
|
|
19
23
|
Run \`${key} configure\` to set up agent artifacts and app config (interactive).
|
|
@@ -93,6 +97,8 @@ export function renderFormula(coords: FormulaCoords): string {
|
|
|
93
97
|
|
|
94
98
|
${formulaPostInstallRuby}
|
|
95
99
|
|
|
100
|
+
${formulaUninstallRuby}
|
|
101
|
+
|
|
96
102
|
${formulaCaveatsRuby}
|
|
97
103
|
|
|
98
104
|
${formulaTestRuby}
|
|
@@ -1,8 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
|
|
2
3
|
/** Back up release formula, write dev formula, run a command, then restore. Usage: bun scripts/with-dev-formula.ts -- brew install … */
|
|
3
4
|
|
|
4
5
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import {
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { createIdentity } from "./create-identity.ts";
|
|
10
|
+
import { renderDevFormula } from "./formula-shared.ts";
|
|
11
|
+
|
|
12
|
+
const { key } = createIdentity;
|
|
13
|
+
const root = join(import.meta.dir, "..");
|
|
14
|
+
const stagingDir = join(root, "Formula", ".staging");
|
|
15
|
+
const stagingPath = join(stagingDir, key);
|
|
16
|
+
const formulaPath = join(root, "Formula", `${key}.rb`);
|
|
17
|
+
const backupPath = join(stagingDir, `${key}.rb.bak`);
|
|
18
|
+
|
|
19
|
+
function backupReleaseFormula(): void {
|
|
20
|
+
if (!existsSync(formulaPath)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
24
|
+
copyFileSync(formulaPath, backupPath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function restoreReleaseFormula(): void {
|
|
28
|
+
if (!existsSync(backupPath)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
copyFileSync(backupPath, formulaPath);
|
|
32
|
+
unlinkSync(backupPath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeDevFormula(): void {
|
|
36
|
+
const distPath = join(root, "dist", key);
|
|
37
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
38
|
+
copyFileSync(distPath, stagingPath);
|
|
39
|
+
chmodSync(stagingPath, 0o755);
|
|
40
|
+
|
|
41
|
+
const sha256 = createHash("sha256").update(readFileSync(stagingPath)).digest("hex");
|
|
42
|
+
const version = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version as string;
|
|
43
|
+
writeFileSync(formulaPath, renderDevFormula(stagingPath, version, sha256), "utf8");
|
|
44
|
+
}
|
|
6
45
|
|
|
7
46
|
const sep = process.argv.indexOf("--");
|
|
8
47
|
if (sep === -1 || sep === process.argv.length - 1) {
|
|
@@ -31,5 +70,5 @@ try {
|
|
|
31
70
|
}
|
|
32
71
|
|
|
33
72
|
if (process.exitCode === 0) {
|
|
34
|
-
console.log(`Restored ${
|
|
73
|
+
console.log(`Restored ${formulaPath}`);
|
|
35
74
|
}
|
package/package.json
CHANGED
|
@@ -42,7 +42,6 @@ describe("builtins help copy", () => {
|
|
|
42
42
|
expect(configure.description).toContain("agent skills");
|
|
43
43
|
expect(configure.description).toContain("MCP config");
|
|
44
44
|
expect(configure.notes).toContain("brew upgrade");
|
|
45
|
-
expect(configure.notes).not.toContain("HOMEBREW_GITHUB_API_TOKEN");
|
|
46
45
|
const names = configureBuiltinOptions(fixture).map((o) => o.name);
|
|
47
46
|
expect(names).toContain("sync");
|
|
48
47
|
expect(names).toContain("remove-all");
|
package/src/builtins/config.ts
CHANGED
|
@@ -178,7 +178,8 @@ function configSetLeaf(program: CliProgram, mcpSetEnabled: boolean): CliLeaf {
|
|
|
178
178
|
},
|
|
179
179
|
{
|
|
180
180
|
name: "value",
|
|
181
|
-
description:
|
|
181
|
+
description:
|
|
182
|
+
"Value to store (comma-separated or JSON for primitive arrays; --json for objects and nested arrays).",
|
|
182
183
|
kind: CliOptionKind.String,
|
|
183
184
|
argMin: 0,
|
|
184
185
|
argMax: 1,
|
|
@@ -72,6 +72,8 @@ export function configureCommandNotes(program: CliProgram, _caps: CliCapabilitie
|
|
|
72
72
|
` ${app} configure --remove-all --yes`,
|
|
73
73
|
` brew uninstall <tap>/${app}`,
|
|
74
74
|
"",
|
|
75
|
+
"App config is removed automatically by the formula uninstall hook during brew uninstall.",
|
|
76
|
+
"",
|
|
75
77
|
];
|
|
76
78
|
if (program.appConfig) {
|
|
77
79
|
lines.push("Remove app config only:", ` ${app} configure --remove-config --yes`, "");
|
package/src/config/bootstrap.ts
CHANGED
|
@@ -29,7 +29,8 @@ import {
|
|
|
29
29
|
resolveAppConfig,
|
|
30
30
|
stringifyConfigValue,
|
|
31
31
|
} from "./resolve.ts";
|
|
32
|
-
import { effectiveJsonSchema } from "./schema.ts";
|
|
32
|
+
import { configPropertySchema, effectiveJsonSchema } from "./schema.ts";
|
|
33
|
+
import { configValueInputHint, parseConfigSetValue } from "./validate.ts";
|
|
33
34
|
|
|
34
35
|
export { displayAppConfigPath } from "./file.ts";
|
|
35
36
|
|
|
@@ -148,11 +149,15 @@ function promptConfigKey(
|
|
|
148
149
|
configure: boolean,
|
|
149
150
|
jsonSchemaRequired: Set<string> | undefined,
|
|
150
151
|
hostEnv: Record<string, string | undefined>,
|
|
152
|
+
jsonSchema: Record<string, unknown> | undefined,
|
|
151
153
|
): { value: unknown; userTyped: boolean } {
|
|
152
154
|
const baseTitle = entry.title ?? defaultConfigEntryTitle(key);
|
|
153
155
|
const titleWithEnv = entry.env ? `${baseTitle} (${entry.env})` : baseTitle;
|
|
154
156
|
const required = configEntryRequired(key, entry, jsonSchemaRequired);
|
|
155
157
|
const heading = required || !configure ? titleWithEnv : `${titleWithEnv} (optional)`;
|
|
158
|
+
const propSchema = jsonSchema ? configPropertySchema(jsonSchema, key) : undefined;
|
|
159
|
+
const valueHint = jsonSchema ? configValueInputHint(propSchema, jsonSchema) : undefined;
|
|
160
|
+
const valueHintSuffix = valueHint ? ` (${valueHint})` : "";
|
|
156
161
|
process.stderr.write(`${heading}\n`);
|
|
157
162
|
process.stderr.write(` ${entry.description}\n`);
|
|
158
163
|
const hasCurrent = current !== undefined && current !== null && String(current).length > 0;
|
|
@@ -160,18 +165,20 @@ function promptConfigKey(
|
|
|
160
165
|
if (hasCurrent) {
|
|
161
166
|
process.stderr.write(` Current: ${sensitive ? "REDACTED" : stringifyConfigValue(current)}\n`);
|
|
162
167
|
const acceptPrompt = resolvedFromEnv(entry, hostEnv)
|
|
163
|
-
? ` Value (Enter to copy from env): `
|
|
164
|
-
: ` Value (Enter to keep): `;
|
|
168
|
+
? ` Value (Enter to copy from env)${valueHintSuffix}: `
|
|
169
|
+
: ` Value (Enter to keep)${valueHintSuffix}: `;
|
|
165
170
|
process.stderr.write(acceptPrompt);
|
|
166
171
|
} else {
|
|
167
|
-
process.stderr.write(` Value: `);
|
|
172
|
+
process.stderr.write(` Value${valueHintSuffix}: `);
|
|
168
173
|
}
|
|
169
174
|
const input = readPromptLine(sensitive);
|
|
170
175
|
if (input.length === 0 && hasCurrent) {
|
|
171
176
|
return { value: current, userTyped: false };
|
|
172
177
|
}
|
|
173
178
|
if (input.length > 0) {
|
|
174
|
-
|
|
179
|
+
const rootSchema = jsonSchema ?? { type: "object", properties: {} };
|
|
180
|
+
const parsed = parseConfigSetValue(input, propSchema, rootSchema, false);
|
|
181
|
+
return { value: parsed, userTyped: true };
|
|
175
182
|
}
|
|
176
183
|
return { value: undefined, userTyped: false };
|
|
177
184
|
}
|
|
@@ -219,7 +226,15 @@ function promptMissingRequired(program: CliProgram): Record<string, unknown> {
|
|
|
219
226
|
writeConfigureSetupHeading();
|
|
220
227
|
headingWritten = true;
|
|
221
228
|
}
|
|
222
|
-
const { value, userTyped } = promptConfigKey(
|
|
229
|
+
const { value, userTyped } = promptConfigKey(
|
|
230
|
+
key,
|
|
231
|
+
entry,
|
|
232
|
+
undefined,
|
|
233
|
+
false,
|
|
234
|
+
fromSchema,
|
|
235
|
+
hostEnv,
|
|
236
|
+
jsonSchema,
|
|
237
|
+
);
|
|
223
238
|
if (value !== undefined && String(value).length > 0) {
|
|
224
239
|
updates[key] = value;
|
|
225
240
|
}
|
|
@@ -262,7 +277,15 @@ export function runConfigure(
|
|
|
262
277
|
for (const [key, entry] of Object.entries(program.appConfig.entries)) {
|
|
263
278
|
const before = next[key];
|
|
264
279
|
const current = resolved[key];
|
|
265
|
-
const { value, userTyped } = promptConfigKey(
|
|
280
|
+
const { value, userTyped } = promptConfigKey(
|
|
281
|
+
key,
|
|
282
|
+
entry,
|
|
283
|
+
current,
|
|
284
|
+
true,
|
|
285
|
+
fromSchema,
|
|
286
|
+
hostEnv,
|
|
287
|
+
jsonSchema,
|
|
288
|
+
);
|
|
266
289
|
if (value !== undefined && String(value).length > 0) {
|
|
267
290
|
const storedInFile =
|
|
268
291
|
key in existing &&
|
|
@@ -58,11 +58,52 @@ describe("config/validate", () => {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
test("parseConfigSetValue requires --json for objects", () => {
|
|
61
|
-
expect(() => parseConfigSetValue(
|
|
61
|
+
expect(() => parseConfigSetValue("ttl:1", { type: "object" }, rootSchema, false)).toThrow(
|
|
62
62
|
/--json/,
|
|
63
63
|
);
|
|
64
64
|
expect(parseConfigSetValue('{"ttl":1}', { type: "object" }, rootSchema, true)).toEqual({
|
|
65
65
|
ttl: 1,
|
|
66
66
|
});
|
|
67
|
+
expect(parseConfigSetValue('{"ttl":1}', { type: "object" }, rootSchema, false)).toEqual({
|
|
68
|
+
ttl: 1,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("parseConfigSetValue accepts comma-separated string arrays", () => {
|
|
73
|
+
const servicesSchema = {
|
|
74
|
+
type: "array",
|
|
75
|
+
items: { type: "string" },
|
|
76
|
+
};
|
|
77
|
+
expect(parseConfigSetValue("a,b", servicesSchema, rootSchema, false)).toEqual(["a", "b"]);
|
|
78
|
+
expect(parseConfigSetValue('["a","b"]', servicesSchema, rootSchema, false)).toEqual(["a", "b"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("parseConfigSetValue accepts comma-separated number arrays", () => {
|
|
82
|
+
const schema = { type: "array", items: { type: "integer" } };
|
|
83
|
+
expect(parseConfigSetValue("1, 2, 3", schema, rootSchema, false)).toEqual([1, 2, 3]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("parseConfigSetValue accepts comma-separated date arrays", () => {
|
|
87
|
+
const schema = {
|
|
88
|
+
type: "array",
|
|
89
|
+
items: { type: "string", format: "date" },
|
|
90
|
+
};
|
|
91
|
+
expect(parseConfigSetValue("2024-01-01,2024-02-01", schema, rootSchema, false)).toEqual([
|
|
92
|
+
"2024-01-01",
|
|
93
|
+
"2024-02-01",
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("parseConfigSetValue rejects non-primitive arrays without JSON", () => {
|
|
98
|
+
const schema = {
|
|
99
|
+
type: "array",
|
|
100
|
+
items: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: { ttl: { type: "number" } },
|
|
103
|
+
required: ["ttl"],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
expect(() => parseConfigSetValue("a,b", schema, rootSchema, false)).toThrow(/--json/);
|
|
107
|
+
expect(parseConfigSetValue('[{"ttl":1}]', schema, rootSchema, false)).toEqual([{ ttl: 1 }]);
|
|
67
108
|
});
|
|
68
109
|
});
|
package/src/config/validate.ts
CHANGED
|
@@ -3,8 +3,16 @@ Draft-07 JSON Schema subset validator for program.appConfig files.
|
|
|
3
3
|
Aligned with common ts-json-schema-generator output (local $ref, objects, scalars).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { parseCommaList, parseDate, parseDateTime } from "../formats.ts";
|
|
7
|
+
|
|
6
8
|
type JsonSchema = Record<string, unknown>;
|
|
7
9
|
|
|
10
|
+
/** Homogeneous primitive `items` schema for comma-separated array input. */
|
|
11
|
+
interface PrimitiveArrayItems {
|
|
12
|
+
kind: "string" | "integer" | "number" | "boolean";
|
|
13
|
+
format?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
export interface ValidateResult {
|
|
9
17
|
valid: boolean;
|
|
10
18
|
errors: string[];
|
|
@@ -247,6 +255,135 @@ function validateArray(
|
|
|
247
255
|
}
|
|
248
256
|
}
|
|
249
257
|
|
|
258
|
+
function validateParsedConfigValue(
|
|
259
|
+
parsed: unknown,
|
|
260
|
+
propertySchema: JsonSchema | undefined,
|
|
261
|
+
rootSchema: JsonSchema,
|
|
262
|
+
): unknown {
|
|
263
|
+
if (!propertySchema) {
|
|
264
|
+
return parsed;
|
|
265
|
+
}
|
|
266
|
+
const errors: string[] = [];
|
|
267
|
+
validateValue(parsed, propertySchema, rootSchema, "$", errors);
|
|
268
|
+
if (errors.length > 0) {
|
|
269
|
+
throw new Error(errors[0] ?? "Invalid config value");
|
|
270
|
+
}
|
|
271
|
+
return parsed;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function parseJsonLiteral(
|
|
275
|
+
raw: string,
|
|
276
|
+
propertySchema: JsonSchema | undefined,
|
|
277
|
+
rootSchema: JsonSchema,
|
|
278
|
+
): unknown {
|
|
279
|
+
let parsed: unknown;
|
|
280
|
+
try {
|
|
281
|
+
parsed = JSON.parse(raw) as unknown;
|
|
282
|
+
} catch {
|
|
283
|
+
throw new Error("Invalid JSON");
|
|
284
|
+
}
|
|
285
|
+
return validateParsedConfigValue(parsed, propertySchema, rootSchema);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function homogeneousPrimitiveArrayItems(
|
|
289
|
+
arraySchema: JsonSchema,
|
|
290
|
+
rootSchema: JsonSchema,
|
|
291
|
+
): PrimitiveArrayItems | undefined {
|
|
292
|
+
const items = arraySchema.items;
|
|
293
|
+
if (typeof items !== "object" || items === null || Array.isArray(items)) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
const resolved = resolveSchema(items as JsonSchema, rootSchema);
|
|
297
|
+
if (!resolved) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
const types = normalizeTypes(resolved.type);
|
|
301
|
+
if (types.length !== 1) {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
const kind = types[0];
|
|
305
|
+
if (kind === "string" || kind === "integer" || kind === "number" || kind === "boolean") {
|
|
306
|
+
const format = typeof resolved.format === "string" ? resolved.format : undefined;
|
|
307
|
+
return { kind, format };
|
|
308
|
+
}
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parseBooleanToken(raw: string): boolean {
|
|
313
|
+
const lower = raw.trim().toLowerCase();
|
|
314
|
+
if (lower === "true" || lower === "1") return true;
|
|
315
|
+
if (lower === "false" || lower === "0") return false;
|
|
316
|
+
throw new Error("Expected boolean: true, false, 1, or 0");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function parsePrimitiveArraySegment(segment: string, items: PrimitiveArrayItems): unknown {
|
|
320
|
+
switch (items.kind) {
|
|
321
|
+
case "string": {
|
|
322
|
+
if (items.format === "date") {
|
|
323
|
+
return parseDate(segment);
|
|
324
|
+
}
|
|
325
|
+
if (items.format === "date-time") {
|
|
326
|
+
return parseDateTime(segment);
|
|
327
|
+
}
|
|
328
|
+
return segment;
|
|
329
|
+
}
|
|
330
|
+
case "integer": {
|
|
331
|
+
const n = Number(segment);
|
|
332
|
+
if (Number.isNaN(n) || !Number.isInteger(n)) {
|
|
333
|
+
throw new Error(`Expected integer: ${segment}`);
|
|
334
|
+
}
|
|
335
|
+
return n;
|
|
336
|
+
}
|
|
337
|
+
case "number": {
|
|
338
|
+
const n = Number(segment);
|
|
339
|
+
if (Number.isNaN(n)) {
|
|
340
|
+
throw new Error(`Expected number: ${segment}`);
|
|
341
|
+
}
|
|
342
|
+
return n;
|
|
343
|
+
}
|
|
344
|
+
case "boolean":
|
|
345
|
+
return parseBooleanToken(segment);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function parseHomogeneousPrimitiveArray(
|
|
350
|
+
raw: string,
|
|
351
|
+
arraySchema: JsonSchema,
|
|
352
|
+
rootSchema: JsonSchema,
|
|
353
|
+
): unknown[] {
|
|
354
|
+
const items = homogeneousPrimitiveArrayItems(arraySchema, rootSchema);
|
|
355
|
+
if (!items) {
|
|
356
|
+
throw new Error("Use --json for object or array config values");
|
|
357
|
+
}
|
|
358
|
+
const segments = parseCommaList(raw);
|
|
359
|
+
if (segments.length === 0) {
|
|
360
|
+
throw new Error("Comma-separated list must contain at least one value");
|
|
361
|
+
}
|
|
362
|
+
return segments.map((segment) => parsePrimitiveArraySegment(segment, items));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Optional suffix for interactive configure value prompts (schema-aware). */
|
|
366
|
+
export function configValueInputHint(
|
|
367
|
+
propertySchema: JsonSchema | undefined,
|
|
368
|
+
rootSchema: JsonSchema,
|
|
369
|
+
): string | undefined {
|
|
370
|
+
if (!propertySchema) {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
const resolved = resolveSchema(propertySchema, rootSchema);
|
|
374
|
+
if (!resolved) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
const types = normalizeTypes(resolved.type);
|
|
378
|
+
if (types.includes("array") && homogeneousPrimitiveArrayItems(resolved, rootSchema)) {
|
|
379
|
+
return "comma-separated or JSON array";
|
|
380
|
+
}
|
|
381
|
+
if (types.includes("array") || types.includes("object")) {
|
|
382
|
+
return "JSON";
|
|
383
|
+
}
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
|
|
250
387
|
/** Parse a CLI/MCP set value against a property schema. */
|
|
251
388
|
export function parseConfigSetValue(
|
|
252
389
|
raw: string,
|
|
@@ -255,28 +392,22 @@ export function parseConfigSetValue(
|
|
|
255
392
|
useJson: boolean,
|
|
256
393
|
): unknown {
|
|
257
394
|
if (useJson) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return parsed;
|
|
395
|
+
return parseJsonLiteral(raw, propertySchema, rootSchema);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const trimmed = raw.trim();
|
|
399
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
400
|
+
return parseJsonLiteral(trimmed, propertySchema, rootSchema);
|
|
267
401
|
}
|
|
268
402
|
|
|
269
403
|
const resolved = propertySchema ? resolveSchema(propertySchema, rootSchema) : undefined;
|
|
270
404
|
const types = resolved ? normalizeTypes(resolved.type) : ["string"];
|
|
271
405
|
|
|
272
406
|
if (types.includes("boolean")) {
|
|
273
|
-
|
|
274
|
-
if (lower === "true" || lower === "1") return true;
|
|
275
|
-
if (lower === "false" || lower === "0") return false;
|
|
276
|
-
throw new Error("Expected boolean: true, false, 1, or 0");
|
|
407
|
+
return parseBooleanToken(trimmed);
|
|
277
408
|
}
|
|
278
409
|
if (types.includes("number") || types.includes("integer")) {
|
|
279
|
-
const n = Number(
|
|
410
|
+
const n = Number(trimmed);
|
|
280
411
|
if (Number.isNaN(n)) {
|
|
281
412
|
throw new Error("Expected number");
|
|
282
413
|
}
|
|
@@ -285,7 +416,14 @@ export function parseConfigSetValue(
|
|
|
285
416
|
}
|
|
286
417
|
return n;
|
|
287
418
|
}
|
|
288
|
-
if (types.includes("
|
|
419
|
+
if (types.includes("array")) {
|
|
420
|
+
if (!resolved) {
|
|
421
|
+
throw new Error("Use --json for object or array config values");
|
|
422
|
+
}
|
|
423
|
+
const parsed = parseHomogeneousPrimitiveArray(trimmed, resolved, rootSchema);
|
|
424
|
+
return validateParsedConfigValue(parsed, propertySchema, rootSchema);
|
|
425
|
+
}
|
|
426
|
+
if (types.includes("object")) {
|
|
289
427
|
throw new Error("Use --json for object or array config values");
|
|
290
428
|
}
|
|
291
429
|
return raw;
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/** Backup, write, and restore the release formula for local dev Homebrew installs. */
|
|
2
|
-
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { createIdentity } from "./create-identity.ts";
|
|
7
|
-
import { renderDevFormula } from "./formula-shared.ts";
|
|
8
|
-
|
|
9
|
-
const { key } = createIdentity;
|
|
10
|
-
const root = join(import.meta.dir, "..");
|
|
11
|
-
const stagingDir = join(root, "Formula", ".staging");
|
|
12
|
-
const stagingPath = join(stagingDir, key);
|
|
13
|
-
const formulaPath = join(root, "Formula", `${key}.rb`);
|
|
14
|
-
const backupPath = join(stagingDir, `${key}.rb.bak`);
|
|
15
|
-
|
|
16
|
-
/** Path to the committed release formula (`Formula/full-example.rb`). */
|
|
17
|
-
export function releaseFormulaPath(): string {
|
|
18
|
-
return formulaPath;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Gitignored backup path used during `install-local`. */
|
|
22
|
-
export function releaseFormulaBackupPath(): string {
|
|
23
|
-
return backupPath;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Copy the release formula to `.staging` before overwriting with the dev formula. */
|
|
27
|
-
export function backupReleaseFormula(): void {
|
|
28
|
-
if (!existsSync(formulaPath)) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
mkdirSync(stagingDir, { recursive: true });
|
|
32
|
-
copyFileSync(formulaPath, backupPath);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Restore the release formula from backup; no-op when no backup exists. */
|
|
36
|
-
export function restoreReleaseFormula(): void {
|
|
37
|
-
if (!existsSync(backupPath)) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
copyFileSync(backupPath, formulaPath);
|
|
41
|
-
unlinkSync(backupPath);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Stage `dist/full-example` and overwrite `Formula/full-example.rb` with a local `file://` dev formula. */
|
|
45
|
-
export function writeDevFormula(): void {
|
|
46
|
-
const distPath = join(root, "dist", key);
|
|
47
|
-
mkdirSync(stagingDir, { recursive: true });
|
|
48
|
-
copyFileSync(distPath, stagingPath);
|
|
49
|
-
chmodSync(stagingPath, 0o755);
|
|
50
|
-
|
|
51
|
-
const sha256 = createHash("sha256").update(readFileSync(stagingPath)).digest("hex");
|
|
52
|
-
const version = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version as string;
|
|
53
|
-
writeFileSync(formulaPath, renderDevFormula(stagingPath, version, sha256), "utf8");
|
|
54
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/** Normalize `Formula/full-example.rb` to the release layout; recover from a leftover dev formula. */
|
|
3
|
-
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import {
|
|
6
|
-
releaseFormulaPath,
|
|
7
|
-
restoreReleaseFormula,
|
|
8
|
-
releaseFormulaBackupPath,
|
|
9
|
-
} from "./dev-formula.ts";
|
|
10
|
-
import { renderReleaseFormula } from "./formula-shared.ts";
|
|
11
|
-
|
|
12
|
-
const formulaPath = releaseFormulaPath();
|
|
13
|
-
const backupPath = releaseFormulaBackupPath();
|
|
14
|
-
|
|
15
|
-
if (existsSync(backupPath)) {
|
|
16
|
-
restoreReleaseFormula();
|
|
17
|
-
console.log(`Restored release formula from ${backupPath}`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
let content = readFileSync(formulaPath, "utf8");
|
|
21
|
-
if (content.includes("file://")) {
|
|
22
|
-
process.stderr.write(
|
|
23
|
-
`Dev formula still present at ${formulaPath}. Run: git restore ${formulaPath}\n`,
|
|
24
|
-
);
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const version = content.match(/^\s*version\s+"([^"]+)"/m)?.[1];
|
|
29
|
-
const sha256 = content.match(/^\s*sha256\s+"([^"]+)"/m)?.[1];
|
|
30
|
-
if (!version || !sha256) {
|
|
31
|
-
process.stderr.write(`Could not parse version/sha256 from ${formulaPath}\n`);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const normalized = renderReleaseFormula(version, sha256);
|
|
36
|
-
if (content !== normalized) {
|
|
37
|
-
writeFileSync(formulaPath, normalized, "utf8");
|
|
38
|
-
console.log(`Normalized ${formulaPath} (v${version})`);
|
|
39
|
-
} else {
|
|
40
|
-
console.log(`Release formula OK: ${formulaPath} (v${version})`);
|
|
41
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/** Generate Formula/full-example.rb for local dev install (file:// URL). Prefer `with-dev-formula.ts` via `just install-local`. */
|
|
3
|
-
|
|
4
|
-
import { releaseFormulaPath, writeDevFormula } from "./dev-formula.ts";
|
|
5
|
-
|
|
6
|
-
writeDevFormula();
|
|
7
|
-
console.log(`Wrote ${releaseFormulaPath()}`);
|