argsbarg 6.1.9 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +187 -93
  3. package/docs/README.md +3 -2
  4. package/docs/ai-skills.md +36 -23
  5. package/docs/cli-program.md +12 -11
  6. package/docs/config-schema.md +3 -3
  7. package/docs/configure.md +12 -16
  8. package/docs/decisions.md +74 -37
  9. package/docs/developing.md +6 -6
  10. package/docs/distribution-homebrew.md +117 -104
  11. package/docs/mcp.md +21 -57
  12. package/docs/output-schema.md +2 -2
  13. package/examples/formats.ts +5 -6
  14. package/examples/full-example/README.md +7 -69
  15. package/examples/full-example/docs/cli-schema.json +1 -1659
  16. package/examples/full-example/docs/cli.md +2 -1538
  17. package/examples/full-example/docs/http.md +3 -8
  18. package/examples/full-example/docs/mcp.md +23 -59
  19. package/examples/full-example/docs/openapi.json +2 -782
  20. package/examples/full-example/docs/skill.md +18 -14
  21. package/examples/full-example/justfile +22 -14
  22. package/examples/full-example/scripts/create-identity.ts +2 -1
  23. package/examples/full-example/src/commands/status/command.test.ts +2 -2
  24. package/examples/full-example/src/commands/status/command.ts +7 -6
  25. package/examples/full-example/src/program.ts +4 -10
  26. package/examples/full-example-json/Formula/.gitkeep +0 -0
  27. package/examples/full-example-json/Formula/full-example-json.rb +35 -0
  28. package/examples/full-example-json/README.md +27 -0
  29. package/examples/full-example-json/biome.json +22 -0
  30. package/examples/full-example-json/bun.lock +48 -0
  31. package/examples/full-example-json/docs/README.md +27 -0
  32. package/examples/full-example-json/docs/cli-schema.json +2145 -0
  33. package/examples/full-example-json/docs/cli.md +1990 -0
  34. package/examples/full-example-json/docs/http.md +92 -0
  35. package/examples/full-example-json/docs/mcp.md +116 -0
  36. package/examples/full-example-json/docs/openapi.json +1246 -0
  37. package/examples/full-example-json/docs/skill.md +57 -0
  38. package/examples/full-example-json/justfile +171 -0
  39. package/examples/full-example-json/package.json +22 -0
  40. package/examples/full-example-json/scripts/create-identity.ts +12 -0
  41. package/examples/full-example-json/scripts/dev-formula.ts +97 -0
  42. package/examples/full-example-json/scripts/formula-shared.test.ts +68 -0
  43. package/examples/full-example-json/scripts/formula-shared.ts +170 -0
  44. package/examples/full-example-json/scripts/print-identity.ts +28 -0
  45. package/examples/full-example-json/scripts/release.ts +212 -0
  46. package/examples/full-example-json/src/commands/echo/command.ts +26 -0
  47. package/examples/full-example-json/src/commands/status/command.test.ts +10 -0
  48. package/examples/full-example-json/src/commands/status/command.ts +28 -0
  49. package/examples/full-example-json/src/index.ts +10 -0
  50. package/examples/full-example-json/src/program.ts +33 -0
  51. package/examples/full-example-json/src/types/md.d.ts +4 -0
  52. package/examples/full-example-json/tsconfig.json +17 -0
  53. package/examples/minimal.ts +17 -17
  54. package/examples/nested.ts +10 -10
  55. package/examples/option-required.ts +13 -13
  56. package/examples/servers.ts +10 -10
  57. package/index.d.ts +17 -43
  58. package/package.json +1 -1
  59. package/src/cli-tool/create.test.ts +44 -68
  60. package/src/cli-tool/create.ts +81 -17
  61. package/src/cli-tool/full-example-capabilities.test.ts +33 -18
  62. package/src/cli-tool/post-create.ts +31 -17
  63. package/src/cli-tool/program.ts +16 -7
  64. package/src/cli-tool/prompt.ts +27 -0
  65. package/src/cli-tool/run-create.ts +19 -7
  66. package/src/cli-tool/schemagen/schemagen.test.ts +3 -3
  67. package/src/configure/artifacts/install-validate.test.ts +20 -33
  68. package/src/configure/artifacts/paths.ts +9 -53
  69. package/src/configure/artifacts/status.test.ts +13 -16
  70. package/src/configure/artifacts/status.ts +5 -22
  71. package/src/configure/artifacts/target-base.ts +6 -15
  72. package/src/configure/artifacts/target-effective.ts +16 -54
  73. package/src/configure/artifacts/target-mcp-json.ts +2 -5
  74. package/src/configure/artifacts/target-registry.ts +0 -7
  75. package/src/configure/artifacts/target-scope.ts +7 -17
  76. package/src/configure/artifacts/target-skill.ts +6 -15
  77. package/src/configure/artifacts/target-types.ts +6 -54
  78. package/src/configure/artifacts/targets/agents-mcp.ts +11 -0
  79. package/src/configure/artifacts/targets/configure.ts +1 -5
  80. package/src/configure/artifacts/targets/index.ts +4 -44
  81. package/src/configure/artifacts/targets/skill.ts +12 -0
  82. package/src/configure/artifacts/targets.test.ts +21 -59
  83. package/src/configure/configure.test.ts +35 -46
  84. package/src/configure/index.ts +19 -19
  85. package/src/configure/prompt.ts +2 -12
  86. package/src/core/parse.test.ts +21 -32
  87. package/src/core/types.ts +18 -44
  88. package/src/core/validate.ts +28 -45
  89. package/src/docs/docs.test.ts +4 -4
  90. package/src/docs/http-guide.ts +1 -1
  91. package/src/docs/mcp-guide.ts +41 -71
  92. package/src/docs/resolve.ts +1 -1
  93. package/src/docs/save.ts +1 -1
  94. package/src/exports/cli.ts +1 -1
  95. package/src/index.ts +1 -1
  96. package/src/skill/generate.ts +26 -45
  97. package/src/skill/install.ts +18 -38
  98. package/src/skill/naming.ts +3 -27
  99. package/src/test/integration/config.test.ts +3 -3
  100. package/src/test/integration/mcp.test.ts +4 -4
  101. package/{examples/mcp-test.ts → src/test/mcp-integration-fixture.ts} +20 -22
  102. package/src/configure/artifacts/target-mcp-cli.ts +0 -127
  103. package/src/configure/artifacts/targets/chatgpt-mcp.ts +0 -12
  104. package/src/configure/artifacts/targets/claude-code-mcp.ts +0 -15
  105. package/src/configure/artifacts/targets/claude-desktop-mcp.ts +0 -12
  106. package/src/configure/artifacts/targets/claude-skill.ts +0 -16
  107. package/src/configure/artifacts/targets/codex-mcp.ts +0 -25
  108. package/src/configure/artifacts/targets/codex-skill.ts +0 -14
  109. package/src/configure/artifacts/targets/cursor-mcp.ts +0 -15
  110. package/src/configure/artifacts/targets/cursor-skill.ts +0 -16
  111. package/src/configure/artifacts/targets/openclaw-mcp.ts +0 -25
  112. package/src/configure/artifacts/targets/openclaw-skill.ts +0 -17
  113. package/src/configure/artifacts/targets/opencode-mcp.ts +0 -96
  114. package/src/configure/artifacts/targets/opencode-skill.ts +0 -15
  115. /package/examples/{full-example → full-example-json}/src/commands/render-json/__generated__/RenderJsonInputSchema.json +0 -0
  116. /package/examples/{full-example → full-example-json}/src/commands/render-json/__generated__/index.ts +0 -0
  117. /package/examples/{full-example → full-example-json}/src/commands/render-json/command.test.ts +0 -0
  118. /package/examples/{full-example → full-example-json}/src/commands/render-json/command.ts +0 -0
  119. /package/examples/{full-example → full-example-json}/src/commands/render-json/types.ts +0 -0
  120. /package/examples/{full-example → full-example-json}/src/commands/status/__generated__/StatusJsonOutputSchema.json +0 -0
  121. /package/examples/{full-example → full-example-json}/src/commands/status/__generated__/index.ts +0 -0
  122. /package/examples/{full-example → full-example-json}/src/commands/status/types.ts +0 -0
  123. /package/examples/{full-example → full-example-json}/src/commands/workspaces/__generated__/WorkspaceNameInputSchema.json +0 -0
  124. /package/examples/{full-example → full-example-json}/src/commands/workspaces/__generated__/index.ts +0 -0
  125. /package/examples/{full-example → full-example-json}/src/commands/workspaces/command.test.ts +0 -0
  126. /package/examples/{full-example → full-example-json}/src/commands/workspaces/command.ts +0 -0
  127. /package/examples/{full-example → full-example-json}/src/commands/workspaces/types.ts +0 -0
  128. /package/examples/{full-example → full-example-json}/src/db/index.test.ts +0 -0
  129. /package/examples/{full-example → full-example-json}/src/db/index.ts +0 -0
  130. /package/examples/{full-example → full-example-json}/src/db/migrate.test.ts +0 -0
  131. /package/examples/{full-example → full-example-json}/src/db/migrate.ts +0 -0
  132. /package/examples/{full-example → full-example-json}/src/db/migrations/001_workspaces.sql +0 -0
  133. /package/examples/{full-example → full-example-json}/src/db/tables/workspaces.ts +0 -0
  134. /package/examples/{full-example → full-example-json}/src/types/argsbarg.d.ts +0 -0
@@ -0,0 +1,57 @@
1
+ ---
2
+ id: full-example-json
3
+ name: full-example-json
4
+ description: Operates the full-example-json CLI (echo, render-json, status, workspaces get, workspaces post, and 4 more). Use when the user mentions full-example-json, echo, render-json, status, or related tasks.
5
+ enabled: true
6
+ ---
7
+ <!-- Generated by full-example-json docs skill --save; do not edit. -->
8
+
9
+
10
+ # full-example-json
11
+
12
+ Argsbarg schema-first copy template (@sg schemagen, JSON schemas, REST CRUD)
13
+
14
+ ## Execution
15
+
16
+ Invoke via shell:
17
+
18
+ ```bash
19
+ full-example-json <subcommand> [options] [args]
20
+ ```
21
+
22
+ ## Commands
23
+
24
+ - **`full-example-json echo`** — Echo a message (MCP-friendly leaf).
25
+ - **`full-example-json render-json`** — Echo a JSON message (schema-first JSON leaf demo).
26
+ - **`full-example-json status`** — Show app version. (flags: --json)
27
+ - **`full-example-json workspaces get`** — List workspaces.
28
+ - **`full-example-json workspaces post`** — Create a workspace.
29
+ - **`full-example-json workspaces :id get`** — Get one workspace.
30
+ - **`full-example-json workspaces :id put`** — Replace a workspace.
31
+ - **`full-example-json workspaces :id patch`** — Patch a workspace name.
32
+ - **`full-example-json workspaces :id delete`** — Delete a workspace.
33
+
34
+ ## Pitfalls
35
+
36
+ - Pass `--` before arguments that look like flags.
37
+
38
+ ## Reference
39
+
40
+ For full detail, open `reference.md` in this skill directory (same as `full-example-json docs cli`).
41
+
42
+ ## Install location
43
+
44
+ Install follows the [.agents protocol](https://dotagentsprotocol.com/):
45
+
46
+ - Auto-install: `full-example-json configure --sync --yes` when `skill.enabled` → `~/.agents/skills/full-example-json/`
47
+ - Cursor and most coding agents read `~/.agents/skills/` natively
48
+
49
+ **Claude Code (manual):** symlink or copy into Claude's skill directory:
50
+
51
+ ```bash
52
+ mkdir -p ~/.claude/skills
53
+ ln -sf ~/.agents/skills/full-example-json ~/.claude/skills/full-example-json
54
+ ```
55
+
56
+ Project override (optional): `.agents/skills/full-example-json/`
57
+
@@ -0,0 +1,171 @@
1
+ set shell := ["bash", "-eu", "-o", "pipefail", "-c"]
2
+
3
+ export PATH := justfile_directory() + "/node_modules/.bin:" + env_var("PATH")
4
+
5
+ cli_key := `bun scripts/print-identity.ts key`
6
+ tap_org := `bun scripts/print-identity.ts tapOrg`
7
+ tap_repo := `bun scripts/print-identity.ts tapRepo`
8
+ tap := `bun scripts/print-identity.ts tap`
9
+ release_repo := `bun scripts/print-identity.ts releaseRepo`
10
+ tap_git_url := "git@github.com:" + release_repo + ".git"
11
+ brew_prefix := `brew --prefix`
12
+ tap_parent := brew_prefix + "/Library/Taps/" + tap_org
13
+ tap_path := tap_parent + "/homebrew-" + tap_repo
14
+
15
+ # List available recipes (default)
16
+ _:
17
+ @just --list
18
+
19
+ # Compile the CLI binary to dist/full-example
20
+ build:
21
+ bun build ./src/index.ts --compile --outfile=dist/{{cli_key}}
22
+ @rm -f .*.bun-build
23
+
24
+ # Apply SQL migrations in src/db/migrations/ to a SQLite database file
25
+ migrate DB="./workspaces.db":
26
+ #!/usr/bin/env bash
27
+ set -euo pipefail
28
+ mkdir -p "$(dirname '{{DB}}')"
29
+ for f in $(ls src/db/migrations/*.sql | sort); do
30
+ echo "==> $f"
31
+ sqlite3 '{{DB}}' < "$f"
32
+ done
33
+
34
+ # Run schemagen, typecheck, and format
35
+ check: schemagen format typecheck
36
+
37
+ # demo the HTTP API
38
+ demo-http:
39
+ #!/usr/bin/env bash
40
+ set -euo pipefail
41
+ bun ./src/index.ts http --port 13000 &
42
+ trap 'kill $! 2>/dev/null || true' EXIT
43
+ until curl -sf http://127.0.0.1:13000/health/liveness >/dev/null; do sleep 0.1; done
44
+
45
+ # demo a CLI command
46
+ demo-cli:
47
+ @just run {{cli_key}} status
48
+
49
+ # demo a CLI command
50
+ demo-help:
51
+ @just run {{cli_key}} --help
52
+
53
+ # Run the CLI from source with optional args; restarts on file changes
54
+ dev *ARGS:
55
+ bun --watch ./src/index.ts {{ARGS}}
56
+
57
+ # Regenerate consumer docs under ./docs/
58
+ docgen: schemagen
59
+ @just run docs cli-schema --save
60
+ @just run docs cli --save
61
+ @just run docs skill --save
62
+ @just run docs mcp --save
63
+ @just run docs http --save
64
+ @just run docs openapi --save
65
+
66
+ alias fmt := format
67
+
68
+ # Format and lint sources (auto-fix)
69
+ format:
70
+ bun run biome check ./src ./scripts --write --unsafe
71
+
72
+ # Run the HTTP server
73
+ http:
74
+ @just run http
75
+
76
+ # Alias for backward compatibility
77
+ install: install-local
78
+
79
+ # Refresh skills and MCP without reinstalling the formula
80
+ sync-artifacts:
81
+ just run configure --sync --yes
82
+
83
+ # Dev install: build, stage dev formula, brew install, restore release formula
84
+ install-local: build
85
+ @brew untap {{tap}} 2>/dev/null || true
86
+ mkdir -p {{tap_parent}}
87
+ ln -sfn '{{justfile_directory()}}' {{tap_path}}
88
+ bun scripts/dev-formula.ts install
89
+ brew install --formula {{tap}}/{{cli_key}}
90
+ bun scripts/dev-formula.ts reset
91
+ @echo ""
92
+ @echo "Next: {{cli_key}} configure"
93
+
94
+ # Remove local dev install, then install from GitHub tap (requires gh auth login)
95
+ install-production: uninstall
96
+ brew tap {{release_repo}} {{tap_git_url}}
97
+ brew install --formula {{release_repo}}/{{cli_key}}
98
+ @echo ""
99
+ @echo "Next: {{cli_key}} configure"
100
+
101
+ # Alias for backward compatibility
102
+ reinstall: reinstall-local
103
+
104
+ # Rebuild binary and swap into Cellar (run install-local first)
105
+ reinstall-local: build
106
+ install -m 755 dist/{{cli_key}} "$(brew --prefix {{cli_key}})/bin/{{cli_key}}"
107
+
108
+ # Lint sources without writing
109
+ lint:
110
+ bun run biome check ./src ./scripts
111
+
112
+ # Run the CLI from source once
113
+ run *ARGS:
114
+ bun ./src/index.ts {{ARGS}}
115
+
116
+ # Generate JSON Schema artifacts from TypeScript types
117
+ schemagen:
118
+ argsbarg schemagen
119
+
120
+ # Install bun/npm dependencies
121
+ # Install bun/npm dependencies and generate schemas
122
+ setup:
123
+ bun install
124
+ just schemagen
125
+
126
+ # Run unit tests (after check)
127
+ test: check
128
+ bun test .
129
+
130
+ # Bump version, build, publish; or pass --purge to delete stale GitHub releases
131
+ release *ARGS:
132
+ bun scripts/release.ts {{ARGS}}
133
+
134
+ # Install release formula from tap and run formula test
135
+ test-release:
136
+ brew untap {{tap}} 2>/dev/null || true
137
+ mkdir -p {{tap_parent}}
138
+ ln -sfn '{{justfile_directory()}}' {{tap_path}}
139
+ brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
140
+ brew install --formula {{tap}}/{{cli_key}}
141
+ brew test {{cli_key}}
142
+
143
+ # Typecheck without emitting build artifacts
144
+ typecheck:
145
+ bun run tsc --noEmit
146
+
147
+ # Undo just install-local: agent artifacts first, then formula
148
+ uninstall: uninstall-artifacts uninstall-formula
149
+
150
+ # Remove agent artifacts only (skills, MCP)
151
+ uninstall-artifacts:
152
+ just run configure --remove-all --yes
153
+
154
+ # Remove app config file only
155
+ uninstall-config:
156
+ just run configure --remove-config --yes
157
+
158
+ # Remove formula and untap
159
+ uninstall-formula:
160
+ @brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
161
+ @brew uninstall --formula {{tap}}/{{cli_key}}-local 2>/dev/null || true
162
+ @brew untap {{tap}} 2>/dev/null || true
163
+
164
+ # Remove release formula (does not untap)
165
+ uninstall-release:
166
+ @brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
167
+
168
+ # Remove release formula and untap
169
+ uninstall-release-tap:
170
+ @brew uninstall --formula {{tap}}/{{cli_key}} 2>/dev/null || true
171
+ @brew untap {{tap}} 2>/dev/null || true
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "full-example-json",
3
+ "private": true,
4
+ "type": "module",
5
+ "module": "src/index.ts",
6
+ "description": "Argsbarg schema-first copy template (@sg schemagen; not published to npm).",
7
+ "engines": {
8
+ "bun": ">=1.3"
9
+ },
10
+ "scripts": {
11
+ "biome": "biome",
12
+ "start": "bun run src/index.ts"
13
+ },
14
+ "dependencies": {
15
+ "argsbarg": "file:../.."
16
+ },
17
+ "devDependencies": {
18
+ "@biomejs/biome": "^2.5.0",
19
+ "@types/bun": "^1.3.12",
20
+ "typescript": "^5.9.3"
21
+ }
22
+ }
@@ -0,0 +1,12 @@
1
+ /** CLI identity — substituted by `argsbarg create`. */
2
+
3
+ export const createIdentity = {
4
+ key: "full-example-json",
5
+ className: "FullExampleJson",
6
+ tap: "bdombro/bun-argsbarg",
7
+ homepage: "https://github.com/bdombro/bun-argsbarg",
8
+ releaseRepo: "bdombro/bun-argsbarg",
9
+ desc: "Argsbarg schema-first copy template (@sg schemagen, JSON schemas, REST CRUD)",
10
+ envPrefix: "FULL_EXAMPLE_JSON",
11
+ template: "json",
12
+ } as const;
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Stage or restore the dev Homebrew formula for `just install-local`.
4
+ * `install` — back up release `Formula/full-example.rb` and write a `file://` dev formula.
5
+ * `reset` — restore the release formula from backup.
6
+ */
7
+
8
+ import { createHash } from "node:crypto";
9
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { createIdentity } from "./create-identity.ts";
12
+ import { renderDevFormula } from "./formula-shared.ts";
13
+
14
+ const { key } = createIdentity;
15
+ const root = join(import.meta.dir, "..");
16
+ const stagingDir = join(root, "Formula", ".staging");
17
+ const stagingPath = join(stagingDir, key);
18
+ const formulaPath = join(root, "Formula", `${key}.rb`);
19
+ const backupPath = join(stagingDir, `${key}.rb.bak`);
20
+ const programPath = join(root, "src/program.ts");
21
+
22
+ type Command = "install" | "reset";
23
+
24
+ function usage(): never {
25
+ process.stderr.write("Usage: bun scripts/dev-formula.ts <install|reset>\n");
26
+ process.exit(1);
27
+ }
28
+
29
+ function parseCommand(argv: string[]): Command {
30
+ const cmd = argv[2];
31
+ if (cmd === "install" || cmd === "reset") {
32
+ return cmd;
33
+ }
34
+ usage();
35
+ }
36
+
37
+ function backupReleaseFormula(): void {
38
+ if (!existsSync(formulaPath)) {
39
+ return;
40
+ }
41
+ mkdirSync(stagingDir, { recursive: true });
42
+ copyFileSync(formulaPath, backupPath);
43
+ }
44
+
45
+ function restoreReleaseFormula(): void {
46
+ if (!existsSync(backupPath)) {
47
+ return;
48
+ }
49
+ copyFileSync(backupPath, formulaPath);
50
+ unlinkSync(backupPath);
51
+ }
52
+
53
+ function readVersion(): string {
54
+ const content = readFileSync(programPath, "utf8");
55
+ const match = /version:\s*"([^"]+)"/.exec(content);
56
+ if (!match?.[1]) {
57
+ process.stderr.write(`Could not read version from ${programPath}\n`);
58
+ process.exit(1);
59
+ }
60
+ return match[1];
61
+ }
62
+
63
+ function stageDevFormula(): void {
64
+ const distPath = join(root, "dist", key);
65
+ if (!existsSync(distPath)) {
66
+ process.stderr.write(`Missing binary: ${distPath} (run just build first)\n`);
67
+ process.exit(1);
68
+ }
69
+ mkdirSync(stagingDir, { recursive: true });
70
+ copyFileSync(distPath, stagingPath);
71
+ chmodSync(stagingPath, 0o755);
72
+
73
+ const sha256 = createHash("sha256").update(readFileSync(stagingPath)).digest("hex");
74
+ writeFileSync(formulaPath, renderDevFormula(stagingPath, readVersion(), sha256), "utf8");
75
+ }
76
+
77
+ function install(): void {
78
+ backupReleaseFormula();
79
+ stageDevFormula();
80
+ console.log(`Wrote dev formula: ${formulaPath}`);
81
+ }
82
+
83
+ function reset(): void {
84
+ if (!existsSync(backupPath)) {
85
+ console.log("No dev formula backup to restore.");
86
+ return;
87
+ }
88
+ restoreReleaseFormula();
89
+ console.log(`Restored ${formulaPath}`);
90
+ }
91
+
92
+ const command = parseCommand(process.argv);
93
+ if (command === "install") {
94
+ install();
95
+ } else {
96
+ reset();
97
+ }
@@ -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-json.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-json.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-json");
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-json");
67
+ });
68
+ });
@@ -0,0 +1,170 @@
1
+ /** Shared Ruby fragments embedded in Homebrew formulae. */
2
+
3
+ import { createHash } from "node:crypto";
4
+ import { readFileSync } from "node:fs";
5
+ import { $ } from "bun";
6
+ import { createIdentity } from "./create-identity.ts";
7
+
8
+ const { key, className, desc, homepage, releaseRepo } = createIdentity;
9
+
10
+ /** GitHub release row used by {@link selectStaleReleaseTags}. */
11
+ export interface ReleaseTag {
12
+ tagName: string;
13
+ publishedAt: string;
14
+ }
15
+
16
+ export const formulaInstallRuby = `def install
17
+ bin.install "${key}"
18
+ chmod 0755, bin/"${key}"
19
+ generate_completions_from_executable(bin/"${key}", "completion", base_name: "${key}")
20
+ end`;
21
+
22
+ export const formulaPostInstallRuby = `def post_install
23
+ system bin/"${key}", "configure", "--sync", "--yes"
24
+ end`;
25
+
26
+ export const formulaUninstallRuby = `def uninstall
27
+ system bin/"${key}", "configure", "--remove-all", "--yes"
28
+ end`;
29
+
30
+ export const formulaCaveatsRuby = `def caveats
31
+ <<~EOS
32
+ Run \`${key} configure\` to set up agent artifacts and app config (interactive).
33
+ Restart MCP chat apps (Cursor, Claude Desktop, etc.) after install or upgrade so they load the updated server.
34
+ EOS
35
+ end`;
36
+
37
+ export const formulaTestRuby = `test do
38
+ assert_match version.to_s, shell_output("#{bin}/${key} version")
39
+ assert_predicate bash_completion/"${key}", :exist?
40
+ assert_predicate zsh_completion/"_${key}", :exist?
41
+ assert_predicate fish_completion/"${key}.fish", :exist?
42
+ end`;
43
+
44
+ export interface FormulaCoords {
45
+ url: string;
46
+ urlStanza: string;
47
+ version: string;
48
+ /** SHA-256 hex digest of the release archive at `url` (zip for GitHub releases). */
49
+ sha256: string;
50
+ /** When true, embed {@link githubPrivateReleaseDownloadStrategyRuby} in the formula class. */
51
+ privateRelease?: boolean;
52
+ }
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
+
82
+ /** Resolves private GitHub release assets via the API at download time. */
83
+ export const githubPrivateReleaseDownloadStrategyRuby = ` # Private/internal releases: default CurlDownloadStrategy cannot fetch non-public
84
+ # GitHub release assets. Resolve the asset via the API and authenticate with
85
+ # GitHub::API.credentials (set up via \`gh auth login\` or HOMEBREW_GITHUB_API_TOKEN).
86
+ class GitHubPrivateReleaseDownloadStrategy < CurlDownloadStrategy
87
+ def initialize(url, name, version, **meta)
88
+ super
89
+ pattern = %r{https://github\\.com/([^/]+)/([^/]+)/releases/download/([^/]+)/(\\S+)}
90
+ match = url.match(pattern)
91
+ raise CurlDownloadStrategyError, "Invalid GitHub release URL: #{url}" unless match
92
+ @owner, @repo, @tag, @filename = match.captures
93
+ end
94
+
95
+ def _fetch(url:, resolved_url: resolved_download_url, timeout:)
96
+ curl_download resolved_download_url,
97
+ "--header", "Accept: application/octet-stream",
98
+ "--header", "Authorization: Bearer #{GitHub::API.credentials}",
99
+ to: temporary_path
100
+ end
101
+
102
+ private
103
+
104
+ def resolved_download_url
105
+ @resolved_download_url ||= begin
106
+ asset = GitHub.get_release(@owner, @repo, @tag).fetch("assets")
107
+ .find { |a| a["name"] == @filename }
108
+ raise CurlDownloadStrategyError, "Release asset not found: #{@filename}" unless asset
109
+ asset.fetch("url")
110
+ end
111
+ end
112
+ end`;
113
+
114
+ /** Homebrew `url` for local dev staging paths. */
115
+ export function devUrlStanza(url: string): string {
116
+ return `url "${url}"`;
117
+ }
118
+
119
+ /** Homebrew `url` for GitHub release assets (uses {@link githubPrivateReleaseDownloadStrategyRuby}). */
120
+ export function releaseUrlStanza(url: string): string {
121
+ return `url "${url}",
122
+ using: GitHubPrivateReleaseDownloadStrategy`;
123
+ }
124
+
125
+ export function releaseFormulaUrl(version: string): string {
126
+ return `https://github.com/${releaseRepo}/releases/download/v${version}/${releaseArchiveName()}`;
127
+ }
128
+
129
+ export function renderFormula(coords: FormulaCoords): string {
130
+ const strategyBlock = coords.privateRelease ? `\n${githubPrivateReleaseDownloadStrategyRuby}\n\n ` : "";
131
+ return `class ${className} < Formula
132
+ desc "${desc}"
133
+ homepage "${homepage}"
134
+ version "${coords.version}"
135
+ sha256 "${coords.sha256}"
136
+
137
+ ${formulaInstallRuby}
138
+
139
+ ${formulaPostInstallRuby}
140
+
141
+ ${formulaUninstallRuby}
142
+
143
+ ${formulaCaveatsRuby}
144
+
145
+ ${formulaTestRuby}
146
+ ${strategyBlock}${coords.urlStanza}
147
+ end
148
+ `;
149
+ }
150
+
151
+ export function renderReleaseFormula(version: string, sha256: string): string {
152
+ const url = releaseFormulaUrl(version);
153
+ return renderFormula({
154
+ url,
155
+ urlStanza: releaseUrlStanza(url),
156
+ version,
157
+ sha256,
158
+ privateRelease: true,
159
+ });
160
+ }
161
+
162
+ export function renderDevFormula(stagingPath: string, version: string, sha256: string): string {
163
+ const url = `file://${stagingPath}`;
164
+ return renderFormula({
165
+ url,
166
+ urlStanza: devUrlStanza(url),
167
+ version,
168
+ sha256,
169
+ });
170
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bun
2
+ /** Print a field from scripts/create-identity.ts for justfile backticks. */
3
+
4
+ import { createIdentity } from "./create-identity.ts";
5
+
6
+ const field = process.argv[2];
7
+ if (!field) {
8
+ console.error("Usage: bun scripts/print-identity.ts <key|className|tap|tapOrg|tapRepo|releaseRepo|envPrefix>");
9
+ process.exit(1);
10
+ }
11
+
12
+ const [tapOrg, tapRepo] = createIdentity.tap.split("/");
13
+ const values: Record<string, string> = {
14
+ key: createIdentity.key,
15
+ className: createIdentity.className,
16
+ tap: createIdentity.tap,
17
+ tapOrg: tapOrg ?? "",
18
+ tapRepo: tapRepo ?? "",
19
+ envPrefix: createIdentity.envPrefix,
20
+ releaseRepo: createIdentity.releaseRepo,
21
+ };
22
+
23
+ const value = values[field];
24
+ if (value === undefined) {
25
+ console.error(`Unknown field: ${field}`);
26
+ process.exit(1);
27
+ }
28
+ process.stdout.write(value);