Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
be40cf4
feat(bun): track provider packages for automatic cleanup
jerome-benoit Jan 23, 2026
244a9e0
fix(bun): address review feedback for provider tracking
jerome-benoit Jan 23, 2026
b194a1f
fix: install new package before removing old to prevent broken state
jerome-benoit Jan 23, 2026
7a750aa
fix: always reinstall when version is latest
jerome-benoit Jan 23, 2026
6f92a5b
test: add edge cases for package.json states
jerome-benoit Jan 23, 2026
1afc404
chore: clean up comments
jerome-benoit Jan 23, 2026
ecc94d6
test(bun): verify exact semver version after install
jerome-benoit Jan 23, 2026
757865e
test(bun): harmonize version checks across all tests
jerome-benoit Jan 23, 2026
29ccd76
refactor: dedupe and harmonize bun tests
jerome-benoit Feb 3, 2026
5347a21
fix(bun): reference counting and decision tree coverage
jerome-benoit Feb 4, 2026
cad34a9
Merge branch 'dev' into feat/provider-package-tracking
jerome-benoit Feb 4, 2026
a00f8ae
Merge branch 'dev' into feat/provider-package-tracking
jerome-benoit Feb 4, 2026
fe6bb4b
Merge branch 'dev' into feat/provider-package-tracking
jerome-benoit Feb 4, 2026
57ca211
Merge branch 'dev' into feat/provider-package-tracking
jerome-benoit Feb 5, 2026
17ccbfe
Merge branch 'dev' into feat/provider-package-tracking
jerome-benoit Feb 5, 2026
82e20eb
test(bun): extend coverage for install edge cases and isOutdated
jerome-benoit Feb 5, 2026
b62ea0a
Merge branch 'dev' into feat/provider-package-tracking
jerome-benoit Feb 5, 2026
b102400
Merge branch 'dev' into feat/provider-package-tracking
jerome-benoit Feb 5, 2026
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
150 changes: 90 additions & 60 deletions packages/opencode/src/bun/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import { proxied } from "@/util/proxied"
export namespace BunProc {
const log = Log.create({ service: "bun" })

interface PackageJson {
dependencies?: Record<string, string>
opencode?: {
providers?: Record<string, string>
}
}

export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
log.info("running", {
cmd: [which(), ...cmd],
Expand Down Expand Up @@ -61,77 +68,100 @@ export namespace BunProc {
}),
)

export async function install(pkg: string, version = "latest") {
// Use lock to ensure only one install at a time
async function readPackageJson(): Promise<PackageJson> {
const file = Bun.file(path.join(Global.Path.cache, "package.json"))
return file.json().catch(() => ({}))
}

async function writePackageJson(parsed: PackageJson) {
const file = Bun.file(path.join(Global.Path.cache, "package.json"))
await Bun.write(file.name!, JSON.stringify(parsed, null, 2))
}

async function track(provider: string, pkg: string) {
const parsed = await readPackageJson()
if (!parsed.opencode) parsed.opencode = {}
if (!parsed.opencode.providers) parsed.opencode.providers = {}
parsed.opencode.providers[provider] = pkg
await writePackageJson(parsed)
}

async function cleanup(provider: string, oldPkg: string) {
const parsed = await readPackageJson()
const providers = parsed.opencode?.providers ?? {}
const used = Object.entries(providers).some(([p, name]) => p !== provider && name === oldPkg)
if (used) return
log.info("removing unused package", { pkg: oldPkg })
await BunProc.run(["remove", "--cwd", Global.Path.cache, oldPkg]).catch(() => {})
}

async function resolveVersion(mod: string, version: string) {
if (version !== "latest") return version
const file = Bun.file(path.join(mod, "package.json"))
const pkg = await file.json().catch(() => null)
return pkg?.version ?? version
}

async function finalize(provider: string | undefined, pkg: string, oldPkg: string | undefined) {
if (provider) await track(provider, pkg)
if (oldPkg && oldPkg !== pkg) await cleanup(provider!, oldPkg)
}

export async function install(pkg: string, version = "latest", provider?: string) {
using _ = await Lock.write("bun-install")

const mod = path.join(Global.Path.cache, "node_modules", pkg)
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
const parsed = await pkgjson.json().catch(async () => {
const result = { dependencies: {} }
await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2))
return result
})
const dependencies = parsed.dependencies ?? {}
if (!parsed.dependencies) parsed.dependencies = dependencies
const state = await readPackageJson()
const cached = state.dependencies?.[pkg]
const oldPkg = provider ? state.opencode?.providers?.[provider] : undefined

// Check if we can skip installation
const modExists = await Filesystem.exists(mod)
const cachedVersion = dependencies[pkg]

if (!modExists || !cachedVersion) {
// continue to install
} else if (version !== "latest" && cachedVersion === version) {
return mod
} else if (version === "latest") {
const isOutdated = await PackageRegistry.isOutdated(pkg, cachedVersion, Global.Path.cache)
if (!isOutdated) return mod
log.info("Cached version is outdated, proceeding with install", { pkg, cachedVersion })
if (version !== "latest") {
if (cached === version && modExists) {
await finalize(provider, pkg, oldPkg)
return mod
}
} else {
const outdated = await PackageRegistry.isOutdated(pkg, cached, Global.Path.cache)
if (!outdated && modExists) {
await finalize(provider, pkg, oldPkg)
return mod
}
if (outdated) log.info("cached version is outdated", { pkg, cached })
}

// Build command arguments
const args = [
"add",
"--force",
"--exact",
// TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
...(proxied() ? ["--no-cache"] : []),
"--cwd",
Global.Path.cache,
pkg + "@" + version,
]

// Let Bun handle registry resolution:
// - If .npmrc files exist, Bun will use them automatically
// - If no .npmrc files exist, Bun will default to https://registry.npmjs.org
// - No need to pass --registry flag
log.info("installing package using Bun's default registry resolution", {
pkg,
version,
})
log.info("installing package", { pkg, version })

await BunProc.run(args, {
cwd: Global.Path.cache,
}).catch((e) => {
throw new InstallFailedError(
{ pkg, version },
{
cause: e,
},
)
await BunProc.run(
[
"add",
"--force",
"--exact",
// TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
...(proxied() ? ["--no-cache"] : []),
"--cwd",
Global.Path.cache,
`${pkg}@${version}`,
],
{ cwd: Global.Path.cache },
).catch((e) => {
throw new InstallFailedError({ pkg, version }, { cause: e })
})

// Resolve actual version from installed package when using "latest"
// This ensures subsequent starts use the cached version until explicitly updated
let resolvedVersion = version
if (version === "latest") {
const installedPkgJson = Bun.file(path.join(mod, "package.json"))
const installedPkg = await installedPkgJson.json().catch(() => null)
if (installedPkg?.version) {
resolvedVersion = installedPkg.version
}
// Persist resolved version and provider tracking
const resolved = await resolveVersion(mod, version)
const updated = await readPackageJson()
if (!updated.dependencies) updated.dependencies = {}
updated.dependencies[pkg] = resolved
if (provider) {
if (!updated.opencode) updated.opencode = {}
if (!updated.opencode.providers) updated.opencode.providers = {}
updated.opencode.providers[provider] = pkg
}
await writePackageJson(updated)

parsed.dependencies[pkg] = resolvedVersion
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
if (oldPkg && oldPkg !== pkg) await cleanup(provider!, oldPkg)
return mod
}
}
4 changes: 3 additions & 1 deletion packages/opencode/src/bun/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export namespace PackageRegistry {
return value
}

export async function isOutdated(pkg: string, cachedVersion: string, cwd?: string): Promise<boolean> {
export async function isOutdated(pkg: string, cachedVersion: string | undefined, cwd?: string): Promise<boolean> {
if (!cachedVersion) return true

const latestVersion = await info(pkg, "version", cwd)
if (!latestVersion) {
log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion })
Expand Down
5 changes: 4 additions & 1 deletion packages/opencode/src/global/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ export namespace Global {
data,
bin: path.join(data, "bin"),
log: path.join(data, "log"),
cache,
// Allow override via OPENCODE_TEST_CACHE for test isolation
get cache() {
return process.env.OPENCODE_TEST_CACHE || cache
},
config,
state,
}
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,7 @@ export namespace Provider {

let installedPath: string
if (!model.api.npm.startsWith("file://")) {
installedPath = await BunProc.install(model.api.npm, "latest")
installedPath = await BunProc.install(model.api.npm, "latest", model.providerID)
} else {
log.info("loading local provider", { pkg: model.api.npm })
installedPath = model.api.npm
Expand Down
Loading
Loading