perf(desktop): ship the bundled CLI version instead of spawning for it (#49762)

This commit is contained in:
Luke Parker
2026-09-19 09:36:55 +10:00
committed by GitHub
parent 98c6b41573
commit 988a66ca4f
5 changed files with 60 additions and 12 deletions
@@ -202,7 +202,7 @@ for (const channel of ["dev", "beta"] as const) {
{
from: "resources/",
to: "",
filter: ["opencode-cli", "opencode-cli.exe"],
filter: ["opencode-cli", "opencode-cli.exe", "opencode-cli.version"],
},
])
})
+3 -1
View File
@@ -90,7 +90,7 @@ const getBase = (appId: string): Configuration => ({
{
from: "resources/",
to: "",
filter: ["opencode-cli", "opencode-cli.exe"],
filter: ["opencode-cli", "opencode-cli.exe", "opencode-cli.version"],
},
],
afterPack: async (context) => {
@@ -100,6 +100,8 @@ const getBase = (appId: string): Configuration => ({
)
const file = await stat(cli)
if (!file.isFile() || file.size === 0) throw new Error(`Bundled CLI must be a non-empty file: ${cli}`)
const version = path.join(path.dirname(cli), "opencode-cli.version")
if ((await stat(version)).size === 0) throw new Error(`Bundled CLI version must be a non-empty file: ${version}`)
},
mac: {
category: "public.app-category.developer-tools",
+16 -8
View File
@@ -1,7 +1,7 @@
import { $ } from "bun"
import { chmod, copyFile, mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { dirname, join } from "node:path"
const CLI_VERSION = "dev"
@@ -75,10 +75,7 @@ export async function downloadCliToResources(version = CLI_VERSION, dest = windo
const directory = await mkdtemp(join(tmpdir(), "opencode-cli-"))
try {
await $`bun install --no-save --cwd ${directory} ${`${cli.package}@${version}`} ${`--os=${cli.os}`} ${`--cpu=${cli.cpu}`}`
await copyCliToResources(
join(directory, "node_modules", cli.package, "bin", cli.os === "win32" ? "opencode.exe" : "opencode"),
dest,
)
await copyCliToResources(join(directory, "node_modules", cli.package), dest)
} finally {
await rm(directory, { recursive: true, force: true })
}
@@ -89,12 +86,23 @@ export async function downloadCliToResources(version = CLI_VERSION, dest = windo
export async function copyBuiltCliToResources(root: string, dest = windowsify("resources/opencode-cli")) {
const cli = getCurrentCli()
const directory = cli.package.replace("@opencode/", "")
await copyCliToResources(join(root, directory, "bin", cli.os === "win32" ? "opencode.exe" : "opencode"), dest)
await copyCliToResources(join(root, directory), dest)
}
async function copyCliToResources(source: string, dest: string) {
await copyFile(source, dest)
// The package directory is an npm package: its package.json version is the string the executable
// prints for --version. Writing it next to the executable spares the desktop a ~400 ms spawn of the
// 200 MB binary on first launch.
async function copyCliToResources(pkg: string, dest: string) {
const cli = getCurrentCli()
await copyFile(join(pkg, "bin", cli.os === "win32" ? "opencode.exe" : "opencode"), dest)
await prepareCli(dest)
const manifest = (await Bun.file(join(pkg, "package.json")).json()) as { version?: string }
if (!manifest.version) throw new Error(`Bundled CLI package has no version: ${pkg}`)
await Bun.write(versionFile(dest), manifest.version)
}
export function versionFile(cli: string) {
return join(dirname(cli), "opencode-cli.version")
}
async function prepareCli(dest: string) {
@@ -3,9 +3,11 @@ export * as DesktopCli from "./desktop-cli"
import { execFile, spawn } from "node:child_process"
import { promisify } from "node:util"
import { app } from "electron"
import { Context, Effect, FileSystem, Layer, Path } from "effect"
import { Context, Effect, FileSystem, Layer, Option, Path } from "effect"
import installer from "../../../../../install?raw"
import { DesktopPaths } from "../paths"
import { BUNDLED_CLI_VERSION_KEY } from "../storage/keys"
import { getStore } from "../storage/store"
import { parseCliVersion } from "./cli-version"
const execFileAsync = promisify(execFile)
@@ -79,11 +81,46 @@ const resolveBundledCli = Effect.fn("DesktopCli.resolveBundled")(function* (isol
? path.join(process.resourcesPath, executableName())
: path.join(paths.developmentResourcesRoot, isolated ? developmentExecutableName() : executableName())
yield* Effect.logInfo("v2 CLI executable resolved", { bundled, packaged: app.isPackaged })
const version = parseCliVersion(yield* run(bundled, ["--version"]))
const version = yield* bundledVersion(bundled)
const binary = app.isPackaged || isolated ? yield* installCli(bundled, version) : bundled
return { version, binary, command: [binary] }
})
// Spawning the bundled executable for `--version` costs ~400 ms of startup on a 200 MB binary (and
// several seconds on the first launch after an update, while the antivirus scans it). The build
// writes the version next to the executable, so a packaged app never spawns; the per-identity cache
// covers executables that arrived without that file.
const bundledVersion = Effect.fn("DesktopCli.bundledVersion")(function* (bundled: string) {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
const shipped = yield* fs
.readFileString(path.join(path.dirname(bundled), "opencode-cli.version"))
.pipe(Effect.map((text) => text.trim()), Effect.orElseSucceed(() => ""))
if (shipped) {
yield* Effect.logInfo("v2 CLI version bundled", { version: shipped })
return shipped
}
const stat = yield* fs.stat(bundled).pipe(Effect.orElseSucceed(() => undefined))
const identity = stat ? `${stat.size}:${Option.getOrUndefined(stat.mtime)?.getTime() ?? ""}` : undefined
const store = getStore()
const cached = store.get(BUNDLED_CLI_VERSION_KEY)
if (identity && isVersionCache(cached) && cached.path === bundled && cached.identity === identity) {
yield* Effect.logInfo("v2 CLI version reused", { version: cached.version })
return cached.version
}
const version = parseCliVersion(yield* run(bundled, ["--version"]))
if (identity) store.set(BUNDLED_CLI_VERSION_KEY, { path: bundled, identity, version } satisfies VersionCache)
return version
})
type VersionCache = { path: string; identity: string; version: string }
function isVersionCache(value: unknown): value is VersionCache {
if (!value || typeof value !== "object") return false
const cache = value as Record<string, unknown>
return typeof cache.path === "string" && typeof cache.identity === "string" && typeof cache.version === "string"
}
export const cleanStages = Effect.fn("DesktopCli.cleanStages")(function* (binary: string) {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
@@ -5,3 +5,4 @@ export const WSL_SERVERS_KEY = "wslServers"
export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled"
export const BACKGROUND_COLOR_KEY = "backgroundColor"
export const WINDOW_IDS_KEY = "windowIds"
export const BUNDLED_CLI_VERSION_KEY = "bundledCliVersion"