Two related-but-separate goals on one feature branch:
- Auto-sync upstream — pull
anomalyco/opencode@devinto this fork on a schedule, opening a PR for review. Already implemented on this branch as.github/workflows/sync-upstream.yml. - Fork self-update — make
opencode upgrade(and the in-TUI "update available" toast) work on this fork. Today the upgrade code is hardcoded againstanomalyco/opencodereleases andopencode-aion npm — neither exists for this fork. We want the same UX as upstream, but pointed atTTK95/opencode's releases.
This document covers item 2.
packages/opencode/src/cli/upgrade.ts runs Installation.latest() then Installation.upgrade(method, target).
packages/opencode/src/installation/index.ts:173 — Installation.method() walks well-known package managers (npm, bun, brew, scoop, choco, …) looking for the install. It also short-circuits to "curl" if process.execPath contains .opencode/bin or .local/bin. Returns "unknown" otherwise.
Installation.latest() (line 207) is hardcoded:
npm/bun/pnpm→https://registry.npmjs.org/opencode-ai/<channel>(line 226)curl/everything else →https://api.github.com/repos/anomalyco/opencode/releases/latest(line 257)
Installation.upgrade() (line 264):
npm→npm install -g opencode-ai@<target>curl→ pipeshttps://opencode.ai/installthroughbashwithVERSION=<target>(line 144)- others → package-manager-native commands
InstallationVersion and InstallationChannel (in packages/core/src/installation/version.ts) are stamped at build time via Bun --define in packages/opencode/script/build.ts:215-220. Script.channel (in packages/script/src/index.ts:60) reads from current git branch (dev, beta, latest, …).
You run from ~/AppData/Roaming/npm/node_modules/opencode-windows-x64/bin/opencode.exe, a symlink created by npm install -g packages/opencode/dist/opencode-windows-x64 (per LOCAL_REINSTALL.md). The npm-installed package name is opencode-windows-x64, not opencode-ai — so today Installation.method() falls through every check and returns "unknown". opencode upgrade already does nothing on this fork.
Three things to add. All gated on OPENCODE_CHANNEL === "dev_ttk" so upstream behavior is untouched when stamped with a normal channel.
packages/opencode/src/installation/index.ts- Read
OPENCODE_REPO(defaultanomalyco/opencode) instead of hardcoding the URL on line 257. Use it everywhere a GitHub repo path is referenced (release URL, install script URL). - Add a new branch in
Installation.method()before the existing checks:if (InstallationChannel === "dev_ttk") return "github-release" as Method
- Add
"github-release"to theMethodunion (line 18). - Add a
case "github-release":branch inInstallation.latest()that hitshttps://api.github.com/repos/${OPENCODE_REPO}/releases/latestand returnstag_name.replace(/^v/, ""). - Add a
case "github-release":branch inInstallation.upgrade()that downloadsopencode-windows-x64.zip(or the platform-appropriate asset) from the matching release tag and extracts it over the running install dir.
- Read
packages/opencode/script/build.ts- Pass
OPENCODE_CHANNEL=dev_ttk(overriding the git-branch default fromScript.channel) when the build is for the fork. Simplest: an env-var override inpackages/script/src/index.tsthat picks upOPENCODE_CHANNELfromprocess.env. The fork's release workflow (andLOCAL_REINSTALL.mdbuild command) sets it.
- Pass
The fork doesn't have signed binaries, MSI installers, or an npm package — just a GitHub release with the windows-x64 binary as a zip asset. The upgrade flow:
GET https://api.github.com/repos/${OPENCODE_REPO}/releases/tags/v${target}→ resolve the asset URL foropencode-windows-x64.zip(oropencode-darwin-arm64.zipetc., based onprocess.platform/process.arch).GET <asset URL>→ write to<install-dir>/opencode-update.zip. Install dir =path.dirname(path.dirname(process.execPath)), e.g.~/AppData/Roaming/npm/node_modules/opencode-windows-x64/.- Extract zip into install dir (overwrite
bin/opencode.exe). On Windows the running.exeis locked — Windows allows replacing a running file viaMoveFileExrename-on-reboot only with admin, so the realistic flow is: writebin/opencode.exe.new, prompt user to relaunch, swap on next start. Or: detectEBUSY, surface a clear message asking the user to exit running TUI sessions and re-run upgrade. - Emit
Installation.Event.Updated.
New .github/workflows/release-fork.yml:
- Triggers:
push: branches: [dev]andworkflow_dispatch. - Reads
packages/opencode/package.jsonversion (already<upstream>-dev_ttkafter the auto-sync workflow lands its PR). - If a release tag
v<version>already exists, exit (no-op — typical case when the push didn't bump the version). - Otherwise:
- Sets up Bun.
- Runs
OPENCODE_VERSION=<version> OPENCODE_CHANNEL=dev_ttk OPENCODE_REPO=TTK95/opencode bun run --cwd packages/opencode build --single— producespackages/opencode/dist/opencode-windows-x64/. - Zips
dist/opencode-windows-x64/bin/*intoopencode-windows-x64.zip. - Creates GitHub release tagged
v<version>with the zip attached. Usegh release createwith defaultGITHUB_TOKEN— no signing certs needed for a personal fork.
- Skips macOS/Linux for now (you only run Windows). Add later if needed.
Trade-off: only one platform → narrower scope, faster CI, no Apple/Azure secrets to wire up. Adding more platforms later is a matrix expansion.
semver.major("1.14.28-dev_ttk") === 1, semver.minor === 14. Existing getReleaseType() (line 37) already uses semver.major/minor, so it correctly classifies 1.14.27-dev_ttk → 1.14.28-dev_ttk as a "patch". No change needed.
InstallationVersion === latest string comparison in upgrade.ts:20 works because both sides will be 1.14.28-dev_ttk (same stamping rules on both build pipelines).
The existing config.autoupdate setting (true | false | "notify") and OPENCODE_DISABLE_AUTOUPDATE flag already gate the upgrade flow. Patches above don't touch any of that — same UX, different remote.
Modify:
packages/opencode/src/installation/index.ts— config repo, addgithub-releasemethodpackages/opencode/script/build.ts(orpackages/script/src/index.ts) — honorOPENCODE_CHANNELenv overrideLOCAL_REINSTALL.md— documentOPENCODE_CHANNEL=dev_ttkin the build command
Create:
.github/workflows/release-fork.yml— build + publish dev_ttk releases
Untouched (intentionally):
Installation.method()checks fornpm/brew/etc. — left in place;dev_ttkshort-circuits before them.- All upstream upgrade methods — unchanged.
-
Local build with new channel:
OPENCODE_VERSION=1.14.28-dev_ttk OPENCODE_CHANNEL=dev_ttk \ bun run --cwd packages/opencode build --single
Then
dist/opencode-windows-x64/bin/opencode --versionshould print1.14.28-dev_ttkanddist/.../bin/opencodeinvokingInstallation.method()returns"github-release". -
Workflow dry run: push a no-op commit to a throwaway branch with
release-fork.ymladapted to that branch, confirm a release with the zip asset is created. -
End-to-end: with the published
v1.14.28-dev_ttkrelease in place, install an older fork build locally (e.g. tag av1.14.26-dev_ttkbuild first), then runopencode upgradeand watch it pull the newer zip and prompt for restart. -
Negative case: when channel is not
dev_ttk(e.g. running an upstream build), every upstream codepath should be unchanged. Runbun test packages/opencode/test/installation/installation.test.tsto check.
- macOS/Linux fork builds (single-platform first; matrix later if needed).
- Code signing for Windows (SmartScreen warning is acceptable for a personal fork; revisit if you ever distribute).
- npm publishing of a fork-named package (
@ttk/opencode-aior similar) — the GitHub release path is enough. - Replacing the running
.exewithout a restart prompt — Windows file-locking makes this not worth the complexity.