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 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 the release formula (`scripts/with-dev-formula.ts`).
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** — export `HOMEBREW_GITHUB_API_TOKEN` from `gh auth token` at top of justfile; removed `scripts/ensure-brew-github-token.sh` and `HOMEBREW_NO_*` suppress vars.
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` / `HOMEBREW_GITHUB_API_TOKEN`); `just install-production` and `just test-release` export the token (with `gh auth token` fallback). Documented in `docs/distribution-homebrew.md`.
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.5...HEAD
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 need `HOMEBREW_GITHUB_API_TOKEN` on install — see [docs/distribution-homebrew.md](docs/distribution-homebrew.md#end-user-install).
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
- HOMEBREW_GITHUB_API_TOKEN="$(gh auth token)" brew install <tap>/myapp
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/gen-dev-formula.ts`, `Formula/`, `justfile` |
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
 
@@ -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 `HOMEBREW_GITHUB_API_TOKEN` on `brew install` and `brew upgrade`. See [distribution-homebrew.md](distribution-homebrew.md#end-user-install) for token setup (`gh auth token` or a personal access token with **Contents** read).
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
- HOMEBREW_GITHUB_API_TOKEN="$(gh auth token)" brew install <tap>/<key>
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 `HOMEBREW_GITHUB_API_TOKEN="$(gh auth token)" 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).
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; gen-dev-formula uses file:// URL (`just install` is an alias)
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**, prepend `HOMEBREW_GITHUB_API_TOKEN` on `brew install` and `brew upgrade` (release formulae download via the GitHub API).
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 tap <org>/<repo> git@github.com:<org>/<repo>.git
23
- HOMEBREW_GITHUB_API_TOKEN="$(gh auth token)" brew install <tap>/{key}
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
- Without `gh`, create a personal access token at [github.com/settings/personal-access-tokens/new](https://github.com/settings/personal-access-tokens/new):
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
- HOMEBREW_GITHUB_API_TOKEN=YOUR_TOKEN brew install <tap>/{key}
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
- HOMEBREW_GITHUB_API_TOKEN="$(gh auth token)" brew upgrade {key}
36
+ brew upgrade {key}
41
37
  ```
42
38
 
43
- Local dev installs (`just install-local`) use a `file://` staging URL and do not need the token.
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 (not app config)
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` (backed by `HOMEBREW_GITHUB_API_TOKEN`, `gh auth`, or other Homebrew GitHub credential sources):
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 `HOMEBREW_GITHUB_API_TOKEN` on `brew install` and `brew upgrade`.
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 tap bdombro/bun-argsbarg git@github.com:bdombro/bun-argsbarg.git
31
- HOMEBREW_GITHUB_API_TOKEN="$(gh auth token)" brew install bdombro/bun-argsbarg/full-example
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
- Without `gh`, create a personal access token at [github.com/settings/personal-access-tokens/new](https://github.com/settings/personal-access-tokens/new):
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
- HOMEBREW_GITHUB_API_TOKEN=YOUR_TOKEN brew install bdombro/bun-argsbarg/full-example
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
- HOMEBREW_GITHUB_API_TOKEN="$(gh auth token)" brew upgrade full-example
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 (uses gh auth token)
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 ensure-release-formula
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 (private release assets need gh auth token)
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 { backupReleaseFormula, releaseFormulaPath, restoreReleaseFormula, writeDevFormula } from "./dev-formula.ts";
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 ${releaseFormulaPath()}`);
73
+ console.log(`Restored ${formulaPath}`);
35
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "5.1.5",
3
+ "version": "5.1.7",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3"
@@ -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");
@@ -178,7 +178,8 @@ function configSetLeaf(program: CliProgram, mcpSetEnabled: boolean): CliLeaf {
178
178
  },
179
179
  {
180
180
  name: "value",
181
- description: "Value to store (use --json for objects/arrays).",
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`, "");
@@ -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
- return { value: input, userTyped: true };
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(key, entry, undefined, false, fromSchema, hostEnv);
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(key, entry, current, true, fromSchema, hostEnv);
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('{"ttl":1}', { type: "object" }, rootSchema, false)).toThrow(
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
  });
@@ -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
- const parsed = JSON.parse(raw) as unknown;
259
- const errors: string[] = [];
260
- if (propertySchema) {
261
- validateValue(parsed, propertySchema, rootSchema, "$", errors);
262
- if (errors.length > 0) {
263
- throw new Error(errors[0] ?? "Invalid config value");
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
- const lower = raw.trim().toLowerCase();
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(raw);
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("object") || types.includes("array")) {
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()}`);