Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,9 @@ jobs:
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
GH_REPO: ${{ needs.version.outputs.repo }}
# PAT with write access to bolt-builder/homebrew-tap; the publish script
# skips the tap update with a warning when unset.
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
NPM_CONFIG_PROVENANCE: false
LATEST_YML_DIR: /tmp/latest-yml
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ curl -fsSL https://raw.githubusercontent.com/bolt-builder/bolt-cli/dev/install |

# Or with npm / bun / pnpm / yarn
npm i -g @bolt-builder/bolt-cli

# Or with Homebrew (macOS / Linux)
brew install bolt-builder/tap/bolt-cli

# Or try it without installing
npx @bolt-builder/bolt-cli
```

Then, from any project:
Expand Down
86 changes: 81 additions & 5 deletions packages/opencode/script/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,87 @@ const tags = [`${image}:${version}`, `${image}:${Script.channel}`]
const tagFlags = tags.flatMap((t) => ["-t", t])

// registries
// AUR and Homebrew publishing was removed: the old blocks targeted upstream's
// `opencode-bin` AUR package and `anomalyco/homebrew-tap`, which Bolt neither owns
// nor has credentials for, and their sha256 inputs (dist archives) are not
// downloaded into the publish job. Restore from git history once Bolt has its
// own tap and AUR package.
// AUR publishing was removed: the old block targeted upstream's `opencode-bin`
// AUR package, which Bolt does not own. Restore from git history once Bolt has
// its own AUR package.
if (!Script.preview) {
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
await brew()
}

// Regenerates the Homebrew formula in bolt-builder/homebrew-tap so
// `brew install bolt-builder/tap/bolt-cli` tracks the latest release. The
// sha256 inputs are the exact release assets users download, fetched from the
// (still draft) GitHub release rather than rebuilt locally.
async function brew() {
const token = process.env.HOMEBREW_TAP_TOKEN
if (!token) {
console.warn("HOMEBREW_TAP_TOKEN is not set; skipping homebrew tap update")
return
}

const assets = [
"bolt-cli-darwin-arm64.zip",
"bolt-cli-darwin-x64.zip",
"bolt-cli-linux-arm64.tar.gz",
"bolt-cli-linux-x64.tar.gz",
]
await $`rm -rf ./dist/brew`
await $`mkdir -p ./dist/brew`
await $`gh release download v${Script.version} --repo bolt-builder/bolt-cli --dir ./dist/brew ${assets.flatMap((a) => ["-p", a])}`
const sha = async (asset: string) => (await $`sha256sum ./dist/brew/${asset} | cut -d' ' -f1`.text()).trim()

const url = (asset: string) =>
`https://github.com/bolt-builder/bolt-cli/releases/download/v${Script.version}/${asset}`
const block = async (asset: string) =>
[
` url "${url(asset)}"`,
` sha256 "${await sha(asset)}"`,
"",
" def install",
' bin.install "bolt"',
" end",
].join("\n")

const formula = [
"# typed: false",
"# frozen_string_literal: true",
"",
"# This file is generated by the bolt-cli publish script. DO NOT EDIT.",
"class BoltCli < Formula",
' desc "Bolt: the AI coding agent built for the terminal."',
' homepage "https://github.com/bolt-builder/bolt-cli"',
` version "${Script.version.split("-")[0]}"`,
"",
" on_macos do",
" if Hardware::CPU.arm?",
await block("bolt-cli-darwin-arm64.zip"),
" end",
" if Hardware::CPU.intel?",
await block("bolt-cli-darwin-x64.zip"),
" end",
" end",
"",
" on_linux do",
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
await block("bolt-cli-linux-arm64.tar.gz"),
" end",
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
await block("bolt-cli-linux-x64.tar.gz"),
" end",
" end",
"end",
"",
].join("\n")

const tap = `https://x-access-token:${token}@github.com/bolt-builder/homebrew-tap.git`
await $`rm -rf ./dist/homebrew-tap`
await $`git clone ${tap} ./dist/homebrew-tap`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
await $`mkdir -p ./dist/homebrew-tap/Formula`
await Bun.file("./dist/homebrew-tap/Formula/bolt-cli.rb").write(formula)
await $`git add Formula/bolt-cli.rb`.cwd("./dist/homebrew-tap")
if ((await $`git diff --cached --quiet`.cwd("./dist/homebrew-tap").nothrow()).exitCode !== 0) {
await $`git commit -m "bolt-cli v${Script.version}"`.cwd("./dist/homebrew-tap")
await $`git push`.cwd("./dist/homebrew-tap")
}
}
Comment on lines +102 to 175

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: publish.ts is an ES module, so the function declaration is module-scoped rather than global, and it matches the file's existing top-level function style (published, publish).

Comment on lines +102 to 175

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive: publish.ts is an ES module, so the function declaration is module-scoped rather than global, consistent with the file's existing top-level functions (published, publish).

18 changes: 9 additions & 9 deletions packages/opencode/src/installation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Serv
)

const getBrewFormula = Effect.fnUntraced(function* () {
const tapFormula = yield* text(["brew", "list", "--formula", "anomalyco/tap/opencode"])
if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode"
const coreFormula = yield* text(["brew", "list", "--formula", "opencode"])
if (coreFormula.includes("opencode")) return "opencode"
return "opencode"
const tapFormula = yield* text(["brew", "list", "--formula", "bolt-builder/tap/bolt-cli"])
if (tapFormula.includes("bolt-cli")) return "bolt-builder/tap/bolt-cli"
const coreFormula = yield* text(["brew", "list", "--formula", "bolt-cli"])
if (coreFormula.includes("bolt-cli")) return "bolt-cli"
return "bolt-cli"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

const upgradeFailure = (method: Method, result?: { code: number; stdout: string; stderr: string }) => {
Expand Down Expand Up @@ -187,7 +187,7 @@ const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Serv
{ name: "yarn", command: () => text(["yarn", "global", "list"]) },
{ name: "pnpm", command: () => text(["pnpm", "list", "-g", "--depth=0"]) },
{ name: "bun", command: () => text(["bun", "pm", "ls", "-g"]) },
{ name: "brew", command: () => text(["brew", "list", "--formula", "bolt"]) },
{ name: "brew", command: () => text(["brew", "list", "--formula", "bolt-cli"]) },
{ name: "scoop", command: () => text(["scoop", "list", "bolt"]) },
{ name: "choco", command: () => text(["choco", "list", "--limit-output", "bolt"]) },
]
Expand Down Expand Up @@ -224,7 +224,7 @@ const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Serv
return info.formulae[0].versions.stable
}
const response = yield* httpOk.execute(
HttpClientRequest.get("https://formulae.brew.sh/api/formula/opencode.json").pipe(
HttpClientRequest.get("https://formulae.brew.sh/api/formula/bolt-cli.json").pipe(
HttpClientRequest.acceptJson,
),
)
Expand Down Expand Up @@ -289,12 +289,12 @@ const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Serv
const formula = yield* getBrewFormula()
const env = { HOMEBREW_NO_AUTO_UPDATE: "1" }
if (formula.includes("/")) {
const tap = yield* run(["brew", "tap", "anomalyco/tap"], { env })
const tap = yield* run(["brew", "tap", "bolt-builder/tap"], { env })
if (tap.code !== 0) {
upgradeResult = tap
break
}
const repo = yield* text(["brew", "--repo", "anomalyco/tap"])
const repo = yield* text(["brew", "--repo", "bolt-builder/tap"])
Comment on lines +290 to +295

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Refresh the tap before the target-version check.

For a stale bolt-builder/tap checkout, latest() reads the old local formula before the git pull at Line 300. packages/opencode/src/cli/cmd/upgrade.ts then compares that stale target with InstallationVersion and returns before Installation.upgrade() runs. The first self-upgrade after a tap release is skipped until the user refreshes Homebrew manually. Homebrew updates installed taps through brew update. (docs.brew.sh)

Resolve the target from release metadata for tap installs, or refresh the tap before the caller performs its equality guard. Add a regression test for stale tap metadata.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/installation/index.ts` around lines 292 - 297, Refresh
the Homebrew tap before the target-version equality check can run, ensuring
stale local formula metadata is not returned by latest(). Update the tap-install
flow around run(["brew", "tap", "bolt-builder/tap"]) to invoke the appropriate
Homebrew update/refresh operation before resolving the target version, while
preserving failure handling through upgradeResult. Add a regression test
covering stale tap metadata and confirming the upgrade is not skipped.

const dir = repo.trim()
if (dir) {
const pull = yield* run(["git", "pull", "--ff-only"], { cwd: dir, env })
Expand Down
13 changes: 7 additions & 6 deletions packages/opencode/test/installation/installation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ describe("installation", () => {
Effect.gen(function* () {
const result = yield* Installation.use.latest("npm")
expect(result).toBe("1.5.0")
expect(npmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`)
expect(npmCalls).toContain(`https://registry.npmjs.org/@bolt-builder/bolt-cli/${InstallationChannel}`)
}),
)

Expand All @@ -110,7 +110,7 @@ describe("installation", () => {
Effect.gen(function* () {
const result = yield* Installation.use.latest("bun")
expect(result).toBe("1.6.0")
expect(bunCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`)
expect(bunCalls).toContain(`https://registry.npmjs.org/@bolt-builder/bolt-cli/${InstallationChannel}`)
}),
)

Expand All @@ -124,7 +124,7 @@ describe("installation", () => {
Effect.gen(function* () {
const result = yield* Installation.use.latest("pnpm")
expect(result).toBe("1.7.0")
expect(pnpmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`)
expect(pnpmCalls).toContain(`https://registry.npmjs.org/@bolt-builder/bolt-cli/${InstallationChannel}`)
}),
)

Expand All @@ -149,8 +149,8 @@ describe("installation", () => {
() => jsonResponse({ versions: { stable: "2.0.0" } }),
(cmd, args) => {
// getBrewFormula: return core formula (no tap)
if (cmd === "brew" && args.includes("--formula") && args.includes("anomalyco/tap/opencode")) return ""
if (cmd === "brew" && args.includes("--formula") && args.includes("opencode")) return "opencode"
if (cmd === "brew" && args.includes("--formula") && args.includes("bolt-builder/tap/bolt-cli")) return ""
if (cmd === "brew" && args.includes("--formula") && args.includes("bolt-cli")) return "bolt-cli"
return ""
},
),
Expand All @@ -168,7 +168,8 @@ describe("installation", () => {
testLayer(
() => jsonResponse({}), // HTTP not used for tap formula
(cmd, args) => {
if (cmd === "brew" && args.includes("anomalyco/tap/opencode") && args.includes("--formula")) return "opencode"
if (cmd === "brew" && args.includes("bolt-builder/tap/bolt-cli") && args.includes("--formula"))
return "bolt-cli"
if (cmd === "brew" && args.includes("--json=v2")) return brewInfoJson
return ""
},
Expand Down
Loading