From ddda404d99741a8ab68a76fc69bd2131b3685e20 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:15:25 +1000 Subject: [PATCH] fix(desktop): bundle the CLI in production releases (#46705) --- .github/workflows/publish.yml | 3 +-- nix/desktop.nix | 5 ++++ packages/desktop/README.md | 22 +++++++++++++++ .../desktop/electron-builder.config.test.ts | 13 +-------- packages/desktop/electron-builder.config.ts | 27 ++++++++++++------- packages/desktop/electron.vite.config.ts | 3 ++- packages/desktop/scripts/prebuild.ts | 8 +++++- packages/desktop/scripts/utils.ts | 1 + 8 files changed, 56 insertions(+), 26 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f2217eba5fe..fa785a0f472 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -417,7 +417,6 @@ jobs: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - if: github.ref_name == 'beta' with: name: opencode-preview-cli path: packages/cli/dist @@ -480,7 +479,7 @@ jobs: OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} OPENCODE_CLI_TARGET: ${{ matrix.settings.target }} - OPENCODE_CLI_DIST: ${{ (github.ref_name == 'beta' && format('{0}/packages/cli/dist', github.workspace)) || '' }} + OPENCODE_CLI_DIST: ${{ github.workspace }}/packages/cli/dist - name: Build run: bun run build diff --git a/nix/desktop.nix b/nix/desktop.nix index d47fc59635d..b90ba4d9d99 100644 --- a/nix/desktop.nix +++ b/nix/desktop.nix @@ -87,6 +87,11 @@ stdenv.mkDerivation (finalAttrs: { cd packages/desktop + export OPENCODE_CLI_DIST="$TMPDIR/desktop-cli" + cli_package=$(bun -e 'import { getCurrentCli } from "./scripts/utils.ts"; console.log(getCurrentCli().package.replace("@opencode-ai/", ""))') + mkdir -p "$OPENCODE_CLI_DIST/$cli_package/bin" + cp ${lib.getExe opencode} "$OPENCODE_CLI_DIST/$cli_package/bin/opencode2" + bun run build npx electron-builder --dir \ --config electron-builder.config.ts \ diff --git a/packages/desktop/README.md b/packages/desktop/README.md index 6dd9a202ada..4b9a926b868 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -17,3 +17,25 @@ bundle the assets as an application. The resulting app will be in `dist/`. ```bash bun run build && bun run package ``` + +Production builds require a prebuilt V2 CLI distribution. The release workflow supplies the artifact from the same run: + +```bash +OPENCODE_CHANNEL=prod OPENCODE_CLI_DIST=/absolute/path/to/packages/cli/dist bun run build +OPENCODE_CHANNEL=prod bun run package +``` + +Set `OPENCODE_CLI_TARGET` when packaging for a different architecture. The CLI is placed outside `app.asar` in the +application's resources directory, and packaging fails if it is missing. + +CLI preparation uses these channel rules: + +| Channel | Without `OPENCODE_CLI_DIST` | With `OPENCODE_CLI_DIST` | +| -------------------------------------- | ------------------------------ | --------------------------------------------- | +| `dev`, `local`, unset, or unrecognized | Download the dev CLI | Download the dev CLI; ignore the distribution | +| `beta` | Download the beta CLI | Copy the supplied CLI; fail if it is missing | +| `prod`, `latest` | Fail before changing resources | Copy the supplied CLI; fail if it is missing | + +`bun dev` is separate from packaging: it uses local renderer/server mode, the dev app identity, and the CLI source by +default. `bun dev --download-server ` instead downloads that CLI version for local development. Neither path +requires `OPENCODE_CLI_DIST` or runs the production prebuild. diff --git a/packages/desktop/electron-builder.config.test.ts b/packages/desktop/electron-builder.config.test.ts index c1b9a3cb210..b5707146c3f 100644 --- a/packages/desktop/electron-builder.config.test.ts +++ b/packages/desktop/electron-builder.config.test.ts @@ -190,19 +190,8 @@ for (const channel of ["dev", "beta"] as const) { { from: "resources/", to: "", - filter: ["opencode-cli*"], + filter: ["opencode-cli", "opencode-cli.exe"], }, ]) }) } - -test("does not bundle the CLI in prod builds", async () => { - const previous = process.env.OPENCODE_CHANNEL - process.env.OPENCODE_CHANNEL = "prod" - const module = await import("./electron-builder.config.ts?no-cli-resource=prod") - const config = module.default as Configuration - if (previous === undefined) delete process.env.OPENCODE_CHANNEL - else process.env.OPENCODE_CHANNEL = previous - - expect(config.extraResources).toEqual([]) -}) diff --git a/packages/desktop/electron-builder.config.ts b/packages/desktop/electron-builder.config.ts index f92052ce199..2c8db937e5a 100644 --- a/packages/desktop/electron-builder.config.ts +++ b/packages/desktop/electron-builder.config.ts @@ -1,4 +1,5 @@ import { execFile } from "node:child_process" +import { stat } from "node:fs/promises" import path from "node:path" import { fileURLToPath } from "node:url" import { promisify } from "node:util" @@ -45,6 +46,7 @@ export function macSignOptions(options: CustomMacSignOptions): CustomMacSignOpti const channel = (() => { const raw = process.env.OPENCODE_CHANNEL if (raw === "dev" || raw === "beta" || raw === "prod") return raw + if (raw === "latest") return "prod" return "dev" })() @@ -84,16 +86,21 @@ const getBase = (appId: string): Configuration => ({ "!**/node_modules/js-yaml/dist/{js-yaml.js,js-yaml.min.js,*.map}", "!**/node_modules/js-yaml/bin{,/**/*}", ], - extraResources: - channel !== "prod" - ? [ - { - from: "resources/", - to: "", - filter: ["opencode-cli*"], - }, - ] - : [], + extraResources: [ + { + from: "resources/", + to: "", + filter: ["opencode-cli", "opencode-cli.exe"], + }, + ], + afterPack: async (context) => { + const cli = path.join( + context.packager.getResourcesDir(context.appOutDir), + context.electronPlatformName === "win32" ? "opencode-cli.exe" : "opencode-cli", + ) + const file = await stat(cli) + if (!file.isFile() || file.size === 0) throw new Error(`Bundled CLI must be a non-empty file: ${cli}`) + }, mac: { category: "public.app-category.developer-tools", icon: `resources/icons/icon.icns`, diff --git a/packages/desktop/electron.vite.config.ts b/packages/desktop/electron.vite.config.ts index d62fe67e3c5..b20ee1b1def 100644 --- a/packages/desktop/electron.vite.config.ts +++ b/packages/desktop/electron.vite.config.ts @@ -34,7 +34,8 @@ export default defineConfig(({ command }) => ({ dedupe: ["effect"], }, define: { - "import.meta.env.OPENCODE_CHANNEL": JSON.stringify(channel), + // Local renderer/server mode still uses the dev application identity and updater policy. + "import.meta.env.OPENCODE_CHANNEL": JSON.stringify(channel === "local" ? "dev" : channel), }, build: { minify: command === "build", diff --git a/packages/desktop/scripts/prebuild.ts b/packages/desktop/scripts/prebuild.ts index a7a3793a522..274a7afc84e 100644 --- a/packages/desktop/scripts/prebuild.ts +++ b/packages/desktop/scripts/prebuild.ts @@ -4,9 +4,15 @@ import { $ } from "bun" import { copyBuiltCliToResources, downloadCliToResources, resolveChannel } from "./utils" const channel = resolveChannel() +if (channel === "prod" && !Bun.env.OPENCODE_CLI_DIST) { + throw new Error("OPENCODE_CLI_DIST is required for production desktop builds") +} + await $`bun ./scripts/copy-icons.ts ${channel}` await $`bun ./scripts/copy-metainfo.ts ${channel}` if (channel === "dev") await downloadCliToResources() -if (channel === "beta" && Bun.env.OPENCODE_CLI_DIST) await copyBuiltCliToResources(Bun.env.OPENCODE_CLI_DIST) +if ((channel === "beta" || channel === "prod") && Bun.env.OPENCODE_CLI_DIST) { + await copyBuiltCliToResources(Bun.env.OPENCODE_CLI_DIST) +} if (channel === "beta" && !Bun.env.OPENCODE_CLI_DIST) await downloadCliToResources("beta") diff --git a/packages/desktop/scripts/utils.ts b/packages/desktop/scripts/utils.ts index ad356fce50d..8c6b0e72298 100644 --- a/packages/desktop/scripts/utils.ts +++ b/packages/desktop/scripts/utils.ts @@ -10,6 +10,7 @@ export type Channel = "dev" | "beta" | "prod" export function resolveChannel(): Channel { const raw = Bun.env.OPENCODE_CHANNEL if (raw === "dev" || raw === "beta" || raw === "prod") return raw + if (raw === "latest") return "prod" return "dev" }