From 111e2481ad5e0d8953f01da12c33bc69cab637b2 Mon Sep 17 00:00:00 2001 From: Jason Dillon Date: Sun, 28 Dec 2025 03:16:54 -0800 Subject: [PATCH 1/4] tidy --- bun.lock | 30 ++++++++++++------------------ lib/cli.ts | 2 +- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/bun.lock b/bun.lock index 29bfd56..31719be 100644 --- a/bun.lock +++ b/bun.lock @@ -1,9 +1,9 @@ { "lockfileVersion": 1, - "configVersion": 0, + "configVersion": 1, "workspaces": { "": { - "name": "forge2", + "name": "@planet57/commando", "dependencies": { "boxen": "^7.1.1", "chalk": "^5.3.0", @@ -55,15 +55,13 @@ "@pnpm/npm-conf": ["@pnpm/npm-conf@2.3.1", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw=="], - "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], "@types/glob": ["@types/glob@7.2.0", "", { "dependencies": { "@types/minimatch": "*", "@types/node": "*" } }, "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA=="], "@types/minimatch": ["@types/minimatch@5.1.2", "", {}, "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA=="], - "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="], - - "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], @@ -71,7 +69,7 @@ "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - "ansi-escapes": ["ansi-escapes@7.1.1", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q=="], + "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -97,7 +95,7 @@ "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], @@ -135,8 +133,6 @@ "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], @@ -177,9 +173,9 @@ "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "figlet": ["figlet@1.9.3", "", { "dependencies": { "commander": "^14.0.0" }, "bin": { "figlet": "bin/index.js" } }, "sha512-majPgOpVtrZN1iyNGbsUP6bOtZ6eaJgg5HHh0vFvm5DJhh8dc+FJpOC4GABvMZ/A7XHAJUuJujhgUY/2jPWgMA=="], + "figlet": ["figlet@1.9.4", "", { "dependencies": { "commander": "^14.0.0" }, "bin": { "figlet": "bin/index.js" } }, "sha512-uN6QE+TrzTAHC1IWTyrc4FfGo2KH/82J8Jl1tyKB7+z5DBit/m3D++Iu5lg91qJMnQQ3vpJrj5gxcK/pk4R9tQ=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -245,13 +241,13 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - "ky": ["ky@1.13.0", "", {}, "sha512-JeNNGs44hVUp2XxO3FY9WV28ymG7LgO4wju4HL/dCq1A8eKDcFgVrdCn1ssn+3Q/5OQilv5aYsL0DMt5mmAV9w=="], + "ky": ["ky@1.14.2", "", {}, "sha512-q3RBbsO5A5zrPhB6CaCS8ZUv+NWCXv6JJT4Em0i264G9W0fdPB8YRfnnEi7Dm7X7omAkBIPojzYJ2D1oHTHqug=="], "latest-version": ["latest-version@9.0.0", "", { "dependencies": { "package-json": "^10.0.0" } }, "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA=="], @@ -387,7 +383,7 @@ "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], - "stubborn-utils": ["stubborn-utils@1.0.1", "", {}, "sha512-bwtct4FpoH1eYdSMFc84fxnYynWwsy2u0joj94K+6caiPnjZIpwTLHT2u7CFAS0GumaBZVB5Y2GkJ46mJS76qg=="], + "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], "thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="], @@ -421,12 +417,10 @@ "yargs-parser": ["yargs-parser@13.1.2", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg=="], - "zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], + "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], - "@types/glob/@types/minimatch": ["@types/minimatch@6.0.0", "", { "dependencies": { "minimatch": "*" } }, "sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA=="], - "boxen/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], diff --git a/lib/cli.ts b/lib/cli.ts index df9b024..04a691d 100644 --- a/lib/cli.ts +++ b/lib/cli.ts @@ -176,7 +176,7 @@ async function buildCLI(config: CommandoConfig): Promise { program .name("cmdo") - .description("Modern CLI framework for deployments") + .description("CommanDO CLI Framework") .version(pkg.version); addTopLevelOptions(program); From 63b3fce6c99db62fc25dd1badec1e5078360a140 Mon Sep 17 00:00:00 2001 From: Jason Dillon Date: Sun, 28 Dec 2025 03:17:07 -0800 Subject: [PATCH 2/4] release commands --- .claude/commands/project-prerelease.md | 91 ++++++++++++++++++++++++++ .claude/commands/project-release.md | 72 ++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 .claude/commands/project-prerelease.md create mode 100644 .claude/commands/project-release.md diff --git a/.claude/commands/project-prerelease.md b/.claude/commands/project-prerelease.md new file mode 100644 index 0000000..b26bd7a --- /dev/null +++ b/.claude/commands/project-prerelease.md @@ -0,0 +1,91 @@ +--- +description: Audit changelog and draft entries for upcoming release +allowed-tools: Bash(git:*), Bash(jq:*), Bash(bd:*), Read, Edit, Grep, AskUserQuestion, Skill(beads) +model: haiku +--- + +Audit the changelog for missing entries since the last release and draft updates. + +## Instructions + +Follow these steps exactly in order. + +### Step 1: Get the last release tag + +```bash +git describe --tags --abbrev=0 2>/dev/null || echo "no tags yet" +``` + +### Step 2: Get commits and bead IDs since the tag + +Replace `` with actual tag from Step 1: + +```bash +git log ..HEAD --oneline --no-merges +git log ..HEAD --format="%B" --no-merges | grep -oE "(commando|forge)-[a-z0-9]+" | sort -u +``` + +Also read CHANGELOG.md `[Unreleased]` section. + +### Step 3: Categorize each commit + +**INCLUDE if:** + +- Commit type is `feat:` or `fix:` +- Change affects CLI users (commands, output, behavior) + +**SKIP if:** + +- Type is: `docs:`, `ci:`, `test:`, `chore:`, `bd:`, `bd sync:`, `refactor:` +- Change is in: `.claude/`, `.github/`, `scripts/`, `docs/`, `.beads/` +- Bead ID already in CHANGELOG.md + +### Step 4: Get bead details + +```bash +bd list --status=closed --limit=20 +``` + +For each bead ID from commits: + +```bash +bd show +``` + +### Step 5: Check for gaps + +Compare closed beads vs beads in commits. Flag user-facing beads missing from commits. + +### Step 6: Draft changelog entries + +Format: + +- One line per entry, max 80 chars +- Start with verb: "Add", "Fix", "Change", "Remove" +- Include bead ID: `(\`commando-xxx\`)` +- Group by: Added, Changed, Fixed, Removed + +### Step 7: Present report + +Show: + +1. **Commits analyzed** - hash, type, INCLUDE/SKIP, reason +2. **Beads referenced** - ID, title, type +3. **Gaps** - missing beads or "None" +4. **Draft entries** - grouped by section + +### Step 8: Ask for confirmation + +Use `AskUserQuestion`: + +- Question: "Update CHANGELOG.md with these entries?" +- Header: "Changelog" +- Options: "Yes, update" / "No, skip" + +### Step 9: Update CHANGELOG.md (if yes) + +1. Find `## [Unreleased]` +2. Insert entries after it, before next version section +3. Merge with existing entries (no duplicates) +4. Do NOT commit +5. Tell user: "CHANGELOG.md updated. Review with `git diff CHANGELOG.md`" diff --git a/.claude/commands/project-release.md b/.claude/commands/project-release.md new file mode 100644 index 0000000..1fa0caf --- /dev/null +++ b/.claude/commands/project-release.md @@ -0,0 +1,72 @@ +--- +description: Prepare and tag a new release +argument-hint: [version] +allowed-tools: Bash(git:*), Bash(jq:*), Read, Edit, AskUserQuestion +model: sonnet +--- + +Prepare a release for commando. + +## Instructions + +### Step 1: Gather release context + +```bash +git branch --show-current +jq -r .version package.json +git describe --tags --abbrev=0 2>/dev/null || echo "no tags yet" +git log $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~20")..HEAD --oneline --no-merges +``` + +### Step 2: Validate branch + +Releases MUST be from `main` branch. + +If on ANY OTHER branch: **STOP** and tell user to merge to main first. + +### Step 3: Compute version + +Default: bump patch (e.g., 0.1.3 → 0.1.4) + +If user provided version ($ARGUMENTS), use that instead. + +Use `AskUserQuestion` to confirm version. + +### Step 4: Audit changelog + +Read CHANGELOG.md `[Unreleased]` section. Compare commits since last tag. + +**Only flag user-facing changes:** +- `feat:` that add/change CLI commands or behavior +- `fix:` that fix bugs users encounter + +**Skip:** +- `docs:`, `ci:`, `test:`, `bd:`, `chore:` commits +- Changes in `.claude/`, `.github/`, `scripts/`, `docs/` +- Commits already in changelog (matching bead ID) + +If user-facing changes missing from changelog: **STOP** and ask user to update. + +### Step 5: Execute release + +Only after version confirmed AND changelog complete: + +1. Validate `[Unreleased]` has content + +2. Update CHANGELOG.md: + - Add empty `## [Unreleased]` at top + - Change old `## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD` + +3. Update package.json version + +4. Commit: `chore: release vX.Y.Z` + +5. Create tag: `vX.Y.Z` + +6. Push: + ```bash + git push origin main + git push origin vX.Y.Z + ``` + +7. Report success: "Release tagged. Run the Release workflow manually at https://github.com/jdillon/commando/actions/workflows/release.yml" From 86071d20f3fb9407f89c9889801b9f688815d49e Mon Sep 17 00:00:00 2001 From: Jason Dillon Date: Sun, 28 Dec 2025 03:17:11 -0800 Subject: [PATCH 3/4] tidy --- sandbox/brew-tap-investigation/README.md | 228 ------------- sandbox/brew-tap-investigation/findings.md | 201 ------------ .../brew-tap-investigation/forge.rb.example | 59 ---- .../implementation-steps.md | 307 ------------------ sandbox/homebrew-automation-plan.md | 137 -------- sandbox/homebrew-xcode-issue.md | 51 --- 6 files changed, 983 deletions(-) delete mode 100644 sandbox/brew-tap-investigation/README.md delete mode 100644 sandbox/brew-tap-investigation/findings.md delete mode 100644 sandbox/brew-tap-investigation/forge.rb.example delete mode 100644 sandbox/brew-tap-investigation/implementation-steps.md delete mode 100644 sandbox/homebrew-automation-plan.md delete mode 100644 sandbox/homebrew-xcode-issue.md diff --git a/sandbox/brew-tap-investigation/README.md b/sandbox/brew-tap-investigation/README.md deleted file mode 100644 index 2cfb3cc..0000000 --- a/sandbox/brew-tap-investigation/README.md +++ /dev/null @@ -1,228 +0,0 @@ -# Homebrew Tap Investigation for Forge - -**Date**: 2025-11-15 -**Investigator**: Claude -**Status**: Complete - -## Goal - -Investigate how to support installing Forge via Homebrew tap (`brew tap jdillon/forge && brew install forge`). - -## Background - -Forge is currently installed via: -- Bun package manager -- Git repository: `git+ssh://git@github.com/jdillon/forge` -- Installed to `~/.forge` -- Requires Bun runtime -- Bootstrap script at `bin/forge` (bash wrapper that runs Bun) - -## Key Findings - -### 1. What is a Homebrew Tap? - -A tap is a third-party repository of Homebrew formulae. It allows distribution without being in Homebrew's core repository. - -**Naming convention**: Repository must be named `homebrew-` (e.g., `homebrew-forge`) - -**Installation flow**: -```bash -brew tap jdillon/forge # Adds tap: clones github.com/jdillon/homebrew-forge -brew install forge # Installs formula from that tap -``` - -### 2. Formula Structure - -Formulae are Ruby files that define: -- Source URL (tarball, git repo, or binary) -- Dependencies -- Installation steps -- Tests - -**Location**: `Formula/forge.rb` in the tap repository - -### 3. Approaches for Forge - -There are three viable approaches, each with trade-offs: - -#### **Approach A: Bun Runtime Dependency** (Recommended) -Install as npm/Bun package with Bun as dependency - -**Pros**: -- Most similar to current install.sh behavior -- Can install directly from GitHub -- Uses existing package.json and bin/forge -- Updates are straightforward (bump version in formula) - -**Cons**: -- Requires Bun to be installed (adds ~90MB dependency) -- Users must have Bun on system -- Bun is less common than Node.js - -**Example formula**: -```ruby -class Forge < Formula - desc "Modern CLI framework for deployments" - homepage "https://github.com/jdillon/forge" - url "https://github.com/jdillon/forge/archive/refs/tags/v1.0.0.tar.gz" - sha256 "..." - - depends_on "bun" - - def install - # Install dependencies - system "bun", "install", "--production" - - # Install to libexec (standard for language-specific packages) - libexec.install Dir["*"] - - # Create wrapper that sets FORGE_HOME - (bin/"forge").write_env_script libexec/"bin/forge", FORGE_HOME: libexec - end - - test do - assert_match version.to_s, shell_output("#{bin}/forge --version") - end -end -``` - -#### **Approach B: Standalone Binary** (Future option) -Compile Forge to standalone executable using `bun build --compile` - -**Pros**: -- No runtime dependency -- Smallest install footprint -- Fastest startup -- Users don't need Bun - -**Cons**: -- Requires build step to create binary -- Larger binary size (~50MB) -- Need separate binaries for macOS Intel, macOS ARM, Linux -- More complex release process - -**Example formula**: -```ruby -class Forge < Formula - desc "Modern CLI framework for deployments" - homepage "https://github.com/jdillon/forge" - - if OS.mac? && Hardware::CPU.arm? - url "https://github.com/jdillon/forge/releases/download/v1.0.0/forge-macos-arm64" - sha256 "..." - elsif OS.mac? && Hardware::CPU.intel? - url "https://github.com/jdillon/forge/releases/download/v1.0.0/forge-macos-x64" - sha256 "..." - elsif OS.linux? - url "https://github.com/jdillon/forge/releases/download/v1.0.0/forge-linux-x64" - sha256 "..." - end - - def install - bin.install "forge-#{OS.mac? ? 'macos' : 'linux'}-#{Hardware::CPU.arch}" => "forge" - end - - test do - assert_match version.to_s, shell_output("#{bin}/forge --version") - end -end -``` - -#### **Approach C: Hybrid - Shell Script Installer** -Formula runs install.sh script - -**Pros**: -- Reuses existing install.sh -- Consistent with manual installation - -**Cons**: -- Not idiomatic for Homebrew -- install.sh expects interactive use -- Harder to uninstall cleanly -- Doesn't integrate well with Homebrew's management - -**Not recommended** - Goes against Homebrew best practices. - -### 4. Repository Structure - -Recommended structure for `homebrew-forge` repository: - -``` -homebrew-forge/ -├── Formula/ -│ └── forge.rb # Main formula -├── README.md # Installation instructions -└── LICENSE -``` - -### 5. Release Process - -For Approach A (Bun dependency): -1. Tag release in main repo: `git tag v1.0.0` -2. Push tag: `git push origin v1.0.0` -3. GitHub auto-creates tarball at: `https://github.com/jdillon/forge/archive/refs/tags/v1.0.0.tar.gz` -4. Download tarball, compute SHA256: `shasum -a 256 forge-1.0.0.tar.gz` -5. Update formula with new URL and SHA256 -6. Push formula to tap repo - -For Approach B (Standalone binary): -1. Build binaries for each platform: `bun build --compile lib/cli.ts --outfile forge-` -2. Create GitHub release with binaries as assets -3. Update formula with new URLs and SHA256s -4. Push formula to tap repo - -### 6. Dependencies Comparison - -| Approach | Runtime Deps | Install Size | User Friction | -|----------|-------------|--------------|---------------| -| A: Bun runtime | Bun (~90MB) | ~5MB + deps | Medium (requires Bun) | -| B: Standalone | None | ~50MB | Low (just works) | -| C: install.sh | Bun (~90MB) | ~5MB + deps | High (not Homebrew-idiomatic) | - -### 7. Similar Tools Analysis - -**Examples of Bun-based formulae**: -- Bun itself: `oven-sh/homebrew-bun` - -**Examples of Node.js CLI tools**: -- Standard pattern: depends_on "node", install with npm, symlink binaries -- Reference: https://docs.brew.sh/Node-for-Formula-Authors - -## Recommendation - -**Start with Approach A** (Bun runtime dependency) because: -1. Easiest to implement - closest to current installation -2. Smallest code change to Forge itself -3. Can migrate to Approach B later if desired -4. Follows established patterns from Node.js ecosystem - -**Future migration to Approach B** when: -- Forge becomes more stable -- Want to reduce user friction -- Ready to add binary build step to CI/CD - -## Next Steps - -If proceeding with implementation: - -1. Create `homebrew-forge` repository at github.com/jdillon/homebrew-forge -2. Create initial formula using Approach A template -3. Test locally: `brew install --build-from-source ./Formula/forge.rb` -4. Tag a release in main forge repo (v1.0.0) -5. Update formula with release tarball URL and SHA256 -6. Test end-to-end: `brew tap jdillon/forge && brew install forge` -7. Document in Forge README.md - -## Open Questions - -1. **Version strategy**: Should we use module-system branch or wait for main merge? -2. **FORGE_HOME**: Current install uses ~/.forge, Homebrew typically uses Cellar - need to reconcile -3. **Updates**: Current install.sh pulls from git, Homebrew pulls from releases - different update mechanisms -4. **Dependencies**: Should user commands also be managed by Homebrew or keep current approach? - -## References - -- [Homebrew Tap Documentation](https://docs.brew.sh/How-to-Create-and-Maintain-a-Tap) -- [Node for Formula Authors](https://docs.brew.sh/Node-for-Formula-Authors) -- [Formula Cookbook](https://docs.brew.sh/Formula-Cookbook) -- [Bun Homebrew Tap](https://github.com/oven-sh/homebrew-bun) diff --git a/sandbox/brew-tap-investigation/findings.md b/sandbox/brew-tap-investigation/findings.md deleted file mode 100644 index 77d9543..0000000 --- a/sandbox/brew-tap-investigation/findings.md +++ /dev/null @@ -1,201 +0,0 @@ -# Findings: Homebrew Tap Support for Forge - -## What Worked - -**Research successful** - Found comprehensive documentation and examples: -- Official Homebrew docs explain tap creation clearly -- Node.js formula patterns applicable to Bun -- Multiple real-world examples (Bun's own tap, Node.js CLIs) - -## What Didn't Work - -**No blockers identified** - All approaches are technically feasible - -## Key Insights - -### 1. Homebrew vs Current Installation - Fundamental Difference - -**Current approach (install.sh)**: -- User-centric: installs to `~/.forge` -- Mutable: can update dependencies, pull from git branches -- Development-friendly: can point to local tarballs - -**Homebrew approach**: -- System-centric: installs to `/opt/homebrew/Cellar/forge/` -- Immutable: each version is isolated -- Release-focused: requires tagged releases, not branches - -**Implication**: These are complementary, not replacements: -- `install.sh` → Development/custom installations -- `brew install` → Production/stable installations - -### 2. FORGE_HOME Conflict - -**Current behavior**: -- Bootstrap script `bin/forge` expects `FORGE_HOME=~/.forge` -- Contains: `node_modules/`, `config/`, `state/`, `cache/`, `logs/` - -**Homebrew behavior**: -- Installs to Cellar (e.g., `/opt/homebrew/Cellar/forge/1.0.0`) -- Symlinks to `/opt/homebrew/bin/forge` - -**Resolution needed**: -Two options: -1. **Make FORGE_HOME configurable** - Best option - - Default to `~/.forge` if not set - - Allow override via environment variable - - Homebrew formula sets `FORGE_HOME=#{var}/forge` - - Keeps user data separate from installed code - -2. **Use XDG directories** - More complex but cleaner - - Config: `~/.config/forge` - - State: `~/.local/state/forge` - - Cache: `~/.cache/forge` - - Logs: `~/.local/state/forge/logs` - -**Recommendation**: Option 1 (configurable FORGE_HOME) - minimal code change - -### 3. Dependency Management Differs - -**Current**: User commands installed to `FORGE_HOME/node_modules` - -**Homebrew**: Dependencies should be in formula's libexec, not user's home - -**Challenge**: How do user-installed plugins/commands work with Homebrew install? - -**Options**: -a) Keep current approach - FORGE_HOME for user data, Cellar for forge core -b) Document that Homebrew install is for core CLI only, use `forge plugin install` for extensions -c) Create separate tap for common plugins - -### 4. Release Process Implications - -**Current**: Can install from any branch (`module-system`) - -**Homebrew**: Requires versioned releases (tarball or git tag) - -**Action required**: -- Need stable release strategy -- Semantic versioning -- GitHub releases with tarballs -- Update formula on each release - -## Architectural Considerations - -### Code Changes Needed - -**Minimal changes for Approach A**: -1. Make FORGE_HOME configurable (if not already) -2. Ensure `bin/forge` works when installed via Homebrew -3. Handle case where `FORGE_HOME` doesn't exist yet (create on first run) - -**Current bin/forge script analysis**: -```bash -FORGE_HOME="${HOME}/.forge" # Hardcoded - needs to be configurable -FORGE_CLI="${node_modules}/@planet57/forge/lib/cli.ts" # Expects FORGE_HOME structure -``` - -**Potential issue**: Homebrew install won't have `@planet57/forge` in `FORGE_HOME/node_modules` - -**Resolution**: -- Homebrew formula installs to libexec -- Wrapper script sets: - - `FORGE_HOME=${FORGE_HOME:-$HOME/.forge}` (user data) - - `FORGE_INSTALL=/opt/homebrew/Cellar/forge/1.0.0` (core installation) - - Script needs to check FORGE_INSTALL first, then fall back to FORGE_HOME - -### Testing Strategy - -**Local testing**: -```bash -# Create formula locally -cd homebrew-forge -brew install --build-from-source ./Formula/forge.rb - -# Verify -forge --version -forge --help - -# Test uninstall -brew uninstall forge -``` - -**Tap testing**: -```bash -# From another machine -brew tap jdillon/forge -brew install forge -``` - -## Recommendations - -### Immediate (if implementing now) - -1. **Choose Approach A** (Bun runtime dependency) - - Fastest to implement - - Most similar to current behavior - - Can iterate later - -2. **Create separate repository**: `github.com/jdillon/homebrew-forge` - - Keep tap maintenance separate from core development - - Easier to manage formula updates - -3. **Fix FORGE_HOME handling**: - - Make configurable in bootstrap script - - Separate "installation directory" from "user data directory" - - Document the distinction - -4. **Create v1.0.0 release**: - - Tag current stable point - - Create GitHub release with tarball - - Test tarball installation - -### Future Enhancements - -1. **Add Approach B** (standalone binary) - - Better user experience (no Bun dependency) - - Add to CI/CD: build binaries on release - - Provide both options (let users choose) - -2. **Cask for GUI** (if Forge gets a GUI) - - Use Homebrew Cask instead of formula - -3. **Auto-update mechanism**: - - `forge self-update` command - - Check GitHub releases - - Respect Homebrew vs manual installation - -## Trade-offs Summary - -| Aspect | install.sh | Homebrew (Approach A) | Homebrew (Approach B) | -|--------|-----------|---------------------|---------------------| -| **User friction** | High (manual) | Medium (need Homebrew) | Low (one command) | -| **Dependencies** | Bun required | Bun via Homebrew | None | -| **Update mechanism** | Git pull | `brew upgrade` | `brew upgrade` | -| **Version flexibility** | Any branch | Tagged releases | Tagged releases | -| **Platform support** | macOS/Linux | macOS/Linux | macOS/Linux | -| **Size** | ~5MB + Bun | ~5MB + Bun | ~50MB | -| **Integration** | Manual PATH | Automatic | Automatic | -| **Uninstall** | Manual script | `brew uninstall` | `brew uninstall` | - -## Conclusion - -**Viable**: Yes, Homebrew tap support is definitely doable - -**Complexity**: Low-to-medium (mostly boilerplate) - -**Value**: High for macOS users who prefer Homebrew - -**Risk**: Low - doesn't affect current installation method - -**Time estimate**: -- Basic formula (Approach A): 2-4 hours -- Testing and refinement: 2-4 hours -- Documentation: 1-2 hours -- **Total**: ~1 day for working implementation - -**Blocker check**: -- ✅ No technical blockers -- ⚠️ Need to decide on release strategy (tags vs branches) -- ⚠️ Need to address FORGE_HOME configurable requirement -- ⚠️ Should wait for module-system to merge to main (or release from branch) diff --git a/sandbox/brew-tap-investigation/forge.rb.example b/sandbox/brew-tap-investigation/forge.rb.example deleted file mode 100644 index ec709e7..0000000 --- a/sandbox/brew-tap-investigation/forge.rb.example +++ /dev/null @@ -1,59 +0,0 @@ -# Example Homebrew Formula for Forge -# This would go in: homebrew-forge/Formula/forge.rb - -class Forge < Formula - desc "Modern CLI framework for deployments" - homepage "https://github.com/jdillon/forge" - url "https://github.com/jdillon/forge/archive/refs/tags/v1.0.0.tar.gz" - sha256 "0000000000000000000000000000000000000000000000000000000000000000" # Replace with actual SHA256 - license "Apache-2.0" - - depends_on "bun" - - def install - # Install dependencies (production only) - system "bun", "install", "--production", "--no-save" - - # Install everything to libexec (standard location for language-specific packages) - libexec.install Dir["*"] - - # Create wrapper script that sets environment correctly - # This handles the FORGE_HOME vs installation directory separation - (bin/"forge").write <<~EOS - #!/usr/bin/env bash - set -euo pipefail - - # Installation directory (where Homebrew installed Forge) - export FORGE_INSTALL="#{libexec}" - - # User data directory (where user's config/state/cache lives) - # Can be overridden by user setting FORGE_HOME before running - export FORGE_HOME="${FORGE_HOME:-${HOME}/.forge}" - - # Create FORGE_HOME structure if it doesn't exist - if [[ ! -d "${FORGE_HOME}" ]]; then - mkdir -p "${FORGE_HOME}"/{config,state,cache,logs} - fi - - # Run the Forge CLI using the installation directory - exec bun run --preserve-symlinks "${FORGE_INSTALL}/lib/cli.ts" "$@" - EOS - end - - def post_install - # Create initial FORGE_HOME structure for the user - forge_home = ENV["FORGE_HOME"] || "#{Dir.home}/.forge" - (forge_home/"config").mkpath - (forge_home/"state").mkpath - (forge_home/"cache").mkpath - (forge_home/"logs").mkpath - end - - test do - # Test that forge runs and reports its version - assert_match version.to_s, shell_output("#{bin}/forge --version") - - # Test that forge can show help - assert_match "Modern CLI framework", shell_output("#{bin}/forge --help") - end -end diff --git a/sandbox/brew-tap-investigation/implementation-steps.md b/sandbox/brew-tap-investigation/implementation-steps.md deleted file mode 100644 index 7bdfb53..0000000 --- a/sandbox/brew-tap-investigation/implementation-steps.md +++ /dev/null @@ -1,307 +0,0 @@ -# Implementation Steps for Homebrew Tap Support - -## Prerequisites - -- [ ] Forge is stable enough for v1.0.0 release -- [ ] module-system branch is ready (or merged to main) -- [ ] FORGE_HOME separation is addressed in code - -## Phase 1: Prepare Forge Repository - -### Step 1: Make FORGE_HOME Configurable - -**File**: `bin/forge` - -**Current**: -```bash -FORGE_HOME="${HOME}/.forge" -FORGE_CLI="${node_modules}/@planet57/forge/lib/cli.ts" -``` - -**Needed**: -```bash -# Support both Homebrew installation and manual installation -if [[ -n "${FORGE_INSTALL:-}" ]]; then - # Homebrew installation - use FORGE_INSTALL for core - FORGE_CLI="${FORGE_INSTALL}/lib/cli.ts" -else - # Manual installation - use FORGE_HOME - FORGE_HOME="${FORGE_HOME:-${HOME}/.forge}" - FORGE_CLI="${FORGE_HOME}/node_modules/@planet57/forge/lib/cli.ts" -fi - -# User data directory (always in FORGE_HOME) -export FORGE_HOME="${FORGE_HOME:-${HOME}/.forge}" -``` - -### Step 2: Create Release - -```bash -# In forge repo -git tag -a v1.0.0 -m "Release v1.0.0" -git push origin v1.0.0 -``` - -GitHub automatically creates release tarball at: -`https://github.com/jdillon/forge/archive/refs/tags/v1.0.0.tar.gz` - -### Step 3: Compute SHA256 - -```bash -# Download tarball -curl -L https://github.com/jdillon/forge/archive/refs/tags/v1.0.0.tar.gz -o forge-1.0.0.tar.gz - -# Compute SHA256 -shasum -a 256 forge-1.0.0.tar.gz -# Output: abc123... forge-1.0.0.tar.gz -``` - -## Phase 2: Create Tap Repository - -### Step 4: Create homebrew-forge Repository - -```bash -# On GitHub, create new repo: jdillon/homebrew-forge -# Clone locally -git clone git@github.com:jdillon/homebrew-forge.git -cd homebrew-forge -``` - -### Step 5: Create Directory Structure - -```bash -mkdir -p Formula -``` - -### Step 6: Create Formula - -**File**: `Formula/forge.rb` - -Copy from `forge.rb.example` in this directory, updating: -- `url` - set to tag tarball URL -- `sha256` - set to computed hash from Step 3 -- `version` - matches tag - -### Step 7: Create README - -**File**: `README.md` - -```markdown -# Homebrew Forge - -Official Homebrew tap for [Forge](https://github.com/jdillon/forge). - -## Installation - -```bash -brew tap jdillon/forge -brew install forge -``` - -## Upgrading - -```bash -brew upgrade forge -``` - -## Uninstallation - -```bash -brew uninstall forge -brew untap jdillon/forge -``` - -## About - -Forge is a modern CLI framework for deployments. -``` - -### Step 8: Commit and Push - -```bash -git add Formula/forge.rb README.md -git commit -m "Add forge formula v1.0.0" -git push origin main -``` - -## Phase 3: Local Testing - -### Step 9: Test Local Installation - -```bash -# In homebrew-forge directory -brew install --build-from-source ./Formula/forge.rb - -# Verify -forge --version -forge --help - -# Check installation location -which forge -# Should be: /opt/homebrew/bin/forge (on Apple Silicon) -# or /usr/local/bin/forge (on Intel) - -# Check that FORGE_HOME is created -ls -la ~/.forge -``` - -### Step 10: Test Uninstallation - -```bash -brew uninstall forge - -# Verify removed -which forge -# Should output nothing - -# Note: FORGE_HOME (~/.forge) is preserved (correct behavior) -``` - -## Phase 4: End-to-End Testing - -### Step 11: Test Via Tap - -```bash -# From clean state (or different machine) -brew tap jdillon/forge -brew install forge - -# Verify -forge --version -``` - -### Step 12: Test Upgrade Path - -```bash -# Create v1.0.1 release in forge repo -cd /path/to/forge -git tag v1.0.1 -git push origin v1.0.1 - -# Update formula in tap repo -cd /path/to/homebrew-forge -# Edit Formula/forge.rb: -# - Update version to 1.0.1 -# - Update url to v1.0.1 tarball -# - Update sha256 (download new tarball, compute hash) -git commit -am "Update forge to v1.0.1" -git push - -# Test upgrade -brew upgrade forge -forge --version # Should show 1.0.1 -``` - -## Phase 5: Documentation - -### Step 13: Update Forge README - -**File**: `README.md` in forge repo - -Add installation section: - -```markdown -## Installation - -### Via Homebrew (macOS/Linux) - -```bash -brew tap jdillon/forge -brew install forge -``` - -### Manual Installation - -See [docs/installation.md](docs/installation.md) for manual installation via install.sh. -``` - -### Step 14: Create Installation Docs - -**File**: `docs/installation.md` - -Document both methods: -- Homebrew (recommended for most users) -- Manual install.sh (for development or custom setups) - -Explain differences: -- Homebrew: stable releases, automatic updates via `brew upgrade` -- Manual: any branch/commit, manual updates via install.sh - -## Phase 6: Automation (Optional) - -### Step 15: Automate Formula Updates - -Create GitHub Action in forge repo to update tap on release: - -**.github/workflows/update-homebrew.yml**: -```yaml -name: Update Homebrew Formula - -on: - release: - types: [published] - -jobs: - update-formula: - runs-on: ubuntu-latest - steps: - - name: Update Homebrew tap - uses: mislav/bump-homebrew-formula-action@v2 - with: - formula-name: forge - homebrew-tap: jdillon/homebrew-forge - env: - COMMITTER_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} -``` - -This automatically updates the formula when you create a GitHub release. - -## Verification Checklist - -After implementation, verify: - -- [ ] `brew tap jdillon/forge` works -- [ ] `brew install forge` works -- [ ] `forge --version` shows correct version -- [ ] `forge --help` works -- [ ] FORGE_HOME is created at ~/.forge -- [ ] User can run forge commands -- [ ] `brew upgrade forge` works when new version released -- [ ] `brew uninstall forge` cleanly removes forge -- [ ] FORGE_HOME persists after uninstall (correct behavior) -- [ ] Installation works on both Intel and Apple Silicon Macs -- [ ] Installation works on Linux (if supported) - -## Rollback Plan - -If issues arise: - -```bash -# User can remove tap -brew untap jdillon/forge - -# User can fall back to manual installation -curl -fsSL https://raw.githubusercontent.com/jdillon/forge/main/bin/install.sh | bash -``` - -## Maintenance - -**On each release**: -1. Tag release in forge repo -2. Compute new SHA256 -3. Update formula in homebrew-forge repo -4. Push formula update -5. Test locally before pushing - -**Automation helps**: Step 15's GitHub Action automates most of this. - -## Estimated Time - -- Phase 1: 1-2 hours (code changes + testing) -- Phase 2: 30 minutes (repo setup) -- Phase 3: 30 minutes (local testing) -- Phase 4: 30 minutes (tap testing) -- Phase 5: 1 hour (documentation) -- Phase 6: 1 hour (optional automation) - -**Total**: 4-6 hours for complete implementation diff --git a/sandbox/homebrew-automation-plan.md b/sandbox/homebrew-automation-plan.md deleted file mode 100644 index a5f8790..0000000 --- a/sandbox/homebrew-automation-plan.md +++ /dev/null @@ -1,137 +0,0 @@ -# Homebrew Automation Plan for Forge - -Based on analysis of beads project's `.github/workflows/`. - -## Current State - -- Forge uses Bun runtime (not standalone binary) -- Formula at `homebrew-planet57/Formula/forge.rb` -- Manual process: rebuild tarball, compute sha256, update formula - -## Beads Approach (Reference) - -1. **release.yml** triggers on tag push `v*` -2. GoReleaser builds platform binaries + checksums.txt -3. **update-homebrew.yml** downloads checksums, generates formula, pushes to tap repo -4. Uses `HOMEBREW_TAP_TOKEN` (PAT with repo scope) to push to tap - -## Proposed Forge Approach - -### Option A: Bun Tarball (Current Approach, Automated) - -Keep current formula structure but automate: - -```yaml -# .github/workflows/release.yml -on: - push: - tags: ['v*'] - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - - name: Build tarball - run: bun run pack - - - name: Create GitHub Release - uses: softprops/action-gh-release@v1 - with: - files: build/*.tgz - generate_release_notes: true - - update-homebrew: - needs: release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Get version - id: version - run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - - - name: Download tarball and compute sha256 - run: | - curl -sL "https://github.com/jdillon/forge/releases/download/v${{ steps.version.outputs.version }}/planet57-forge-${{ steps.version.outputs.version }}.tgz" -o forge.tgz - echo "sha256=$(shasum -a 256 forge.tgz | awk '{print $1}')" >> $GITHUB_OUTPUT - id: checksum - - - name: Update formula - run: | - # Generate formula with version and sha256 substituted - # (template approach or sed replacement) - - - name: Push to homebrew-planet57 - env: - HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} - run: | - git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/jdillon/homebrew-planet57.git" tap - cp Formula/forge.rb tap/Formula/forge.rb - cd tap - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add Formula/forge.rb - git commit -m "Update forge to ${{ steps.version.outputs.version }}" - git push -``` - -### Option B: Standalone Binary (Future) - -Use `bun build --compile` for zero-dependency binary: - -1. Build binaries for darwin-arm64, darwin-x64, linux-x64 -2. Upload as release assets -3. Formula uses platform-specific URLs like beads - -**Pros**: No Bun dependency for users -**Cons**: Larger binaries (~50MB), more complex build - -### Recommendation - -**Phase 1**: Option A (automate current approach) -- Low effort, matches current manual process -- Gets automation working quickly - -**Phase 2**: Option B (standalone binary) -- Better UX for users -- Add when ready for wider distribution - -## Implementation Steps - -1. [ ] Create `.github/workflows/release.yml` -2. [ ] Create formula template with placeholders -3. [ ] Create `HOMEBREW_TAP_TOKEN` secret (PAT with repo scope) -4. [ ] Test with a v0.1.0 tag push -5. [ ] Document release process - -## Secrets Required - -- `HOMEBREW_TAP_TOKEN`: Personal Access Token with `repo` scope for pushing to homebrew-planet57 - -## Formula Template - -```ruby -class Forge < Formula - desc "Modern CLI framework for deployments" - homepage "https://github.com/jdillon/forge" - url "https://github.com/jdillon/forge/releases/download/v{{VERSION}}/planet57-forge-{{VERSION}}.tgz" - sha256 "{{SHA256}}" - license "Apache-2.0" - version "{{VERSION}}" - - depends_on "oven-sh/bun/bun" - - def install - # ... (existing install logic) - end -end -``` - -## Open Questions - -1. Should we keep local tarball testing workflow separate? -2. Do we want a manual dispatch trigger for testing? -3. Should formula live in forge repo or just homebrew-planet57? diff --git a/sandbox/homebrew-xcode-issue.md b/sandbox/homebrew-xcode-issue.md deleted file mode 100644 index a9960b8..0000000 --- a/sandbox/homebrew-xcode-issue.md +++ /dev/null @@ -1,51 +0,0 @@ -# Homebrew Xcode Version Check Issue - -## Problem - -Installing forge via Homebrew on deimos fails: - -``` -Error: Your Xcode (16.4) is too outdated. -Please update to Xcode 26.0 (or delete it). -``` - -## Root Cause - -Homebrew enforces minimum Xcode versions per macOS in [`Library/Homebrew/os/mac/xcode.rb`](https://github.com/Homebrew/brew/blob/master/Library/Homebrew/os/mac/xcode.rb): - -```ruby -def self.minimum_version - case macos - when "26" then "26.0" # macOS 26 requires Xcode 26.0 - when "15" then "16.0" - ... -``` - -The check triggers because the forge formula has **no bottles**. Homebrew assumes "no bottle = needs to build = needs Xcode" even though forge only extracts a tarball. - -## Solution: Add Bottles - -Bottles tell Homebrew "pre-built, no compilation needed" and skip the Xcode check. - -Since forge just extracts a tarball, bottles would be identical content. Options: - -1. **Build bottles in CI** - GitHub Actions can build bottles for each macOS version -2. **Use `bottle :unneeded`** - Tell Homebrew this formula never needs building (may not work for taps) - -## Workaround - -Install latest Xcode via xcodes, then switch back to older version for iOS builds: - -```bash -# Install Xcode 26.x -xcodes install 26.0 - -# After brew install, switch back -sudo xcode-select -s /Applications/Xcode-16.4.0.app -``` - -## References - -- [Homebrew/brew xcode.rb source](https://github.com/Homebrew/brew/blob/master/Library/Homebrew/os/mac/xcode.rb) -- [Discussion #3822 - "requires nonexistent xcode version"](https://github.com/orgs/Homebrew/discussions/3822) -- [Issue #18736 - Homebrew requires Xcode when CLT is sufficient](https://github.com/Homebrew/brew/issues/18736) From a27b9e5a41aa01af849866f0650e9b9dd57b7324 Mon Sep 17 00:00:00 2001 From: Jason Dillon Date: Sun, 28 Dec 2025 20:22:52 -0800 Subject: [PATCH 4/4] update tests --- lib/logging/bootstrap.ts | 2 +- tests/cli-color.test.ts | 18 +++++++++--------- tests/cli-help.test.ts | 20 ++++++++++---------- tests/cli-log-format.test.ts | 16 ++++++++-------- tests/context.test.ts | 8 ++++---- tests/lib/runner.ts | 8 ++++---- tests/lib/utils.ts | 2 +- tests/subcommand-options.test.ts | 10 +++++----- 8 files changed, 42 insertions(+), 42 deletions(-) diff --git a/lib/logging/bootstrap.ts b/lib/logging/bootstrap.ts index fc60d60..0cd1cba 100644 --- a/lib/logging/bootstrap.ts +++ b/lib/logging/bootstrap.ts @@ -42,7 +42,7 @@ function parseLogLevel(): string { } // Fallback to environment variable - if (process.env.FORGE_DEBUG) { + if (process.env.COMMANDO_DEBUG) { return 'debug'; } diff --git a/tests/cli-color.test.ts b/tests/cli-color.test.ts index ba0f87f..241327a 100644 --- a/tests/cli-color.test.ts +++ b/tests/cli-color.test.ts @@ -20,7 +20,7 @@ import { describe, test } from './lib/testx'; import { expect } from 'bun:test'; import { setupTestLogs, TEST_DIRS } from './lib/utils'; -import { runForge } from './lib/runner'; +import { runCommando } from './lib/runner'; import { join } from 'path'; describe('CLI Color Detection', () => { @@ -30,7 +30,7 @@ describe('CLI Color Detection', () => { test.skipIf(!!process.env.CI)('should use auto mode by default', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--help'], env: { NO_COLOR: '' }, // Clear NO_COLOR for this test logDir: logs.logDir, @@ -44,7 +44,7 @@ describe('CLI Color Detection', () => { test('should disable colors with --color=never', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--color', 'never', '--help'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -53,13 +53,13 @@ describe('CLI Color Detection', () => { expect(result.exitCode).toBe(0); // Help should still work - check stdout const output = await Bun.file(result.stdoutLog).text(); - expect(output).toContain('Modern CLI framework'); + expect(output).toContain('CommanDO CLI Framework'); }); test('should enable colors with --color=always', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--color', 'always', '--help'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -67,13 +67,13 @@ describe('CLI Color Detection', () => { expect(result.exitCode).toBe(0); const output = await Bun.file(result.stdoutLog).text(); - expect(output).toContain('Modern CLI framework'); + expect(output).toContain('CommanDO CLI Framework'); }); test('should disable colors with NO_COLOR env var', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--help'], env: { NO_COLOR: '1' }, logDir: logs.logDir, @@ -82,13 +82,13 @@ describe('CLI Color Detection', () => { expect(result.exitCode).toBe(0); const output = await Bun.file(result.stdoutLog).text(); // Help goes to stdout - expect(output).toContain('Modern CLI framework'); + expect(output).toContain('CommanDO CLI Framework'); }); test('should prioritize NO_COLOR env over --color flag', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--color', 'always', '--help'], env: { NO_COLOR: '1' }, // Should override --color=always logDir: logs.logDir, diff --git a/tests/cli-help.test.ts b/tests/cli-help.test.ts index 11edc84..b603c2c 100644 --- a/tests/cli-help.test.ts +++ b/tests/cli-help.test.ts @@ -20,7 +20,7 @@ import { describe, test } from './lib/testx'; import { expect } from 'bun:test'; import { setupTestLogs, TEST_DIRS } from './lib/utils'; -import { runForge } from './lib/runner'; +import { runCommando } from './lib/runner'; import { join } from 'path'; describe('CLI Help Output', () => { @@ -29,7 +29,7 @@ describe('CLI Help Output', () => { test('should display help with --help', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--help'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -38,7 +38,7 @@ describe('CLI Help Output', () => { expect(result.exitCode).toBe(0); const output = await Bun.file(result.stdoutLog).text(); expect(output).toContain('Usage: cmdo'); - expect(output).toContain('Modern CLI framework'); + expect(output).toContain('CommanDO CLI Framework'); expect(output).toContain('Options:'); expect(output).toContain('Commands:'); }); @@ -46,7 +46,7 @@ describe('CLI Help Output', () => { test('should display help with -h', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '-h'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -60,7 +60,7 @@ describe('CLI Help Output', () => { test('should show all core options', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--help'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -79,7 +79,7 @@ describe('CLI Help Output', () => { test('should show version with --version', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--version'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -93,7 +93,7 @@ describe('CLI Help Output', () => { test('should show version with -V', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['-V'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -107,7 +107,7 @@ describe('CLI Help Output', () => { test('should show terse error for unknown options', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--invalid-option'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -125,7 +125,7 @@ describe('CLI Help Output', () => { test('should list commands in help output', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--help'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -138,7 +138,7 @@ describe('CLI Help Output', () => { test('should sort options alphabetically', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--help'], logDir: logs.logDir, logBaseName: logs.logBaseName, diff --git a/tests/cli-log-format.test.ts b/tests/cli-log-format.test.ts index b4a3a59..d618f49 100644 --- a/tests/cli-log-format.test.ts +++ b/tests/cli-log-format.test.ts @@ -20,7 +20,7 @@ import { describe, test } from './lib/testx'; import { expect } from 'bun:test'; import { setupTestLogs, TEST_DIRS } from './lib/utils'; -import { runForge } from './lib/runner'; +import { runCommando } from './lib/runner'; import { join } from 'path'; describe('CLI --log-format Validation', () => { @@ -29,7 +29,7 @@ describe('CLI --log-format Validation', () => { test('should accept "json" format', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--log-format', 'json', 'test', 'greet'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -44,7 +44,7 @@ describe('CLI --log-format Validation', () => { test('should accept "pretty" format', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--log-format', 'pretty', 'test', 'greet'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -57,7 +57,7 @@ describe('CLI --log-format Validation', () => { test('should reject invalid format "plain"', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--log-format', 'plain', 'test', 'greet'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -73,7 +73,7 @@ describe('CLI --log-format Validation', () => { test('should reject invalid format "xyz"', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--log-format', 'xyz', 'test', 'greet'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -89,7 +89,7 @@ describe('CLI --log-format Validation', () => { test('should reject numeric format "123"', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--log-format', '123', 'test', 'greet'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -104,7 +104,7 @@ describe('CLI --log-format Validation', () => { test('should show valid formats in error message', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, '--log-format', 'invalid', 'test', 'greet'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -119,7 +119,7 @@ describe('CLI --log-format Validation', () => { test('should use pretty format by default', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, 'test', 'greet'], logDir: logs.logDir, logBaseName: logs.logBaseName, diff --git a/tests/context.test.ts b/tests/context.test.ts index d84617e..558f03c 100644 --- a/tests/context.test.ts +++ b/tests/context.test.ts @@ -20,7 +20,7 @@ import { describe, test } from './lib/testx'; import { expect } from 'bun:test'; import { setupTestLogs, TEST_DIRS } from './lib/utils'; -import { runForge } from './lib/runner'; +import { runCommando } from './lib/runner'; import { join } from 'path'; describe('CommandoContext', () => { @@ -30,7 +30,7 @@ describe('CommandoContext', () => { const logs = await setupTestLogs(ctx); const outputFile = join(logs.logDir, 'context-output.json'); - const result = await runForge({ + const result = await runCommando({ args: ['--root', fixtureRoot, '--log-level', 'debug', '--log-format', 'json', '--color', 'never', 'test', 'context', outputFile], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -57,7 +57,7 @@ describe('CommandoContext', () => { const logs = await setupTestLogs(ctx); const outputFile = join(logs.logDir, 'context-output.json'); - const result = await runForge({ + const result = await runCommando({ args: ['--root', fixtureRoot, '--log-format', 'json', 'test', 'context', outputFile], env: { NO_COLOR: '' }, // Unset NO_COLOR to test actual auto-detection logDir: logs.logDir, @@ -75,7 +75,7 @@ describe('CommandoContext', () => { const logs = await setupTestLogs(ctx); const outputFile = join(logs.logDir, 'context-output.json'); - const result = await runForge({ + const result = await runCommando({ args: ['--root', fixtureRoot, '--log-format', 'json', 'test', 'context', outputFile], env: { NO_COLOR: '1' }, logDir: logs.logDir, diff --git a/tests/lib/runner.ts b/tests/lib/runner.ts index 8e069a2..2bf2c2f 100644 --- a/tests/lib/runner.ts +++ b/tests/lib/runner.ts @@ -33,7 +33,7 @@ const PROJECT_ROOT = resolve(import.meta.dir, '../..'); /** * Configuration for running commando in tests */ -export interface RunForgeConfig { +export interface RunCommandoConfig { /** CLI arguments to pass to commando */ args: string[]; /** Environment variables (merged with process.env) */ @@ -53,7 +53,7 @@ export interface RunForgeConfig { /** * Result from running commando */ -export interface RunForgeResult { +export interface RunCommandoResult { /** Exit code from the command */ exitCode: number; /** Path to stdout log file */ @@ -67,7 +67,7 @@ export interface RunForgeResult { * * @example * ```typescript - * const result = await runForge({ + * const result = await runCommando({ * args: ['--root', fixtureRoot, 'test', 'context', outputFile], * env: { COMMANDO_DEBUG: '1' }, * logDir: logs.logDir, @@ -78,7 +78,7 @@ export interface RunForgeResult { * const stdout = await Bun.file(result.stdoutLog).text(); * ``` */ -export async function runForge(config: RunForgeConfig): Promise { +export async function runCommando(config: RunCommandoConfig): Promise { const { args, env = {}, diff --git a/tests/lib/utils.ts b/tests/lib/utils.ts index dca9978..872774d 100644 --- a/tests/lib/utils.ts +++ b/tests/lib/utils.ts @@ -192,7 +192,7 @@ export interface RunCommandResult { * const result = await runCommandWithLogs({ * command: "bash", * args: ["install.sh"], - * env: { HOME: testHome, FORGE_REPO: "..." }, + * env: { HOME: testHome, COMMANDO_HOME: "..." }, * logDir: testHome, * logBaseName: "install", * }); diff --git a/tests/subcommand-options.test.ts b/tests/subcommand-options.test.ts index 59f2d38..06150cc 100644 --- a/tests/subcommand-options.test.ts +++ b/tests/subcommand-options.test.ts @@ -23,7 +23,7 @@ import { describe, test } from './lib/testx'; import { expect } from 'bun:test'; import { setupTestLogs, TEST_DIRS } from './lib/utils'; -import { runForge } from './lib/runner'; +import { runCommando } from './lib/runner'; import { join } from 'path'; describe('Subcommand Options', () => { @@ -32,7 +32,7 @@ describe('Subcommand Options', () => { test('should parse subcommand flags (--loud)', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, 'test', 'greet', 'Alice', '--loud'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -46,7 +46,7 @@ describe('Subcommand Options', () => { test('should parse subcommand short flags (-l)', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, 'test', 'greet', 'Bob', '-l'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -60,7 +60,7 @@ describe('Subcommand Options', () => { test('should work without flags', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, 'test', 'greet', 'Charlie'], logDir: logs.logDir, logBaseName: logs.logBaseName, @@ -74,7 +74,7 @@ describe('Subcommand Options', () => { test('should work with no arguments', async (ctx) => { const logs = await setupTestLogs(ctx); - const result = await runForge({ + const result = await runCommando({ args: ['--root', projectRoot, 'test', 'greet'], logDir: logs.logDir, logBaseName: logs.logBaseName,