11import { Log } from "../util/log"
22import path from "path"
3- import { pathToFileURL } from "url"
3+ import { pathToFileURL , fileURLToPath } from "url"
44import { createRequire } from "module"
55import os from "os"
66import z from "zod"
@@ -22,18 +22,22 @@ import {
2222} from "jsonc-parser"
2323import { Instance } from "../project/instance"
2424import { LSPServer } from "../lsp/server"
25+ import { BunProc } from "@/bun"
2526import { Installation } from "@/installation"
2627import { ConfigMarkdown } from "./markdown"
2728import { constants , existsSync } from "fs"
2829import { Bus } from "@/bus"
2930import { GlobalBus } from "@/bus/global"
3031import { Event } from "../server/event"
3132import { Glob } from "../util/glob"
33+ import { PackageRegistry } from "@/bun/registry"
34+ import { proxied } from "@/util/proxied"
3235import { iife } from "@/util/iife"
3336import { Account } from "@/account"
3437import { ConfigPaths } from "./paths"
3538import { Filesystem } from "@/util/filesystem"
36- import { Npm } from "@/npm"
39+ import { Process } from "@/util/process"
40+ import { Lock } from "@/util/lock"
3741
3842export namespace Config {
3943 const ModelId = z . string ( ) . meta ( { $ref : "https://models.dev/model-schema.json#/$defs/Model" } )
@@ -150,7 +154,8 @@ export namespace Config {
150154
151155 deps . push (
152156 iife ( async ( ) => {
153- await installDependencies ( dir )
157+ const shouldInstall = await needsInstall ( dir )
158+ if ( shouldInstall ) await installDependencies ( dir )
154159 } ) ,
155160 )
156161
@@ -266,10 +271,6 @@ export namespace Config {
266271 }
267272
268273 export async function installDependencies ( dir : string ) {
269- if ( ! ( await isWritable ( dir ) ) ) {
270- log . info ( "config dir is not writable, skipping dependency install" , { dir } )
271- return
272- }
273274 const pkg = path . join ( dir , "package.json" )
274275 const targetVersion = Installation . isLocal ( ) ? "*" : Installation . VERSION
275276
@@ -283,15 +284,43 @@ export namespace Config {
283284 await Filesystem . writeJson ( pkg , json )
284285
285286 const gitignore = path . join ( dir , ".gitignore" )
286- if ( ! ( await Filesystem . exists ( gitignore ) ) )
287- await Filesystem . write (
288- gitignore ,
289- [ "node_modules" , "plans" , "package.json" , "bun.lock" , ".gitignore" , "package-lock.json" ] . join ( "\n" ) ,
290- )
287+ const hasGitIgnore = await Filesystem . exists ( gitignore )
288+ if ( ! hasGitIgnore )
289+ await Filesystem . write ( gitignore , [ "node_modules" , "package.json" , "bun.lock" , ".gitignore" ] . join ( "\n" ) )
291290
292291 // Install any additional dependencies defined in the package.json
293292 // This allows local plugins and custom tools to use external packages
294- await Npm . install ( dir )
293+ using _ = await Lock . write ( "bun-install" )
294+ await BunProc . run (
295+ [
296+ "install" ,
297+ // TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
298+ ...( proxied ( ) || process . env . CI ? [ "--no-cache" ] : [ ] ) ,
299+ ] ,
300+ { cwd : dir } ,
301+ ) . catch ( ( err ) => {
302+ if ( err instanceof Process . RunFailedError ) {
303+ const detail = {
304+ dir,
305+ cmd : err . cmd ,
306+ code : err . code ,
307+ stdout : err . stdout . toString ( ) ,
308+ stderr : err . stderr . toString ( ) ,
309+ }
310+ if ( Flag . OPENCODE_STRICT_CONFIG_DEPS ) {
311+ log . error ( "failed to install dependencies" , detail )
312+ throw err
313+ }
314+ log . warn ( "failed to install dependencies" , detail )
315+ return
316+ }
317+
318+ if ( Flag . OPENCODE_STRICT_CONFIG_DEPS ) {
319+ log . error ( "failed to install dependencies" , { dir, error : err } )
320+ throw err
321+ }
322+ log . warn ( "failed to install dependencies" , { dir, error : err } )
323+ } )
295324 }
296325
297326 async function isWritable ( dir : string ) {
@@ -303,6 +332,41 @@ export namespace Config {
303332 }
304333 }
305334
335+ export async function needsInstall ( dir : string ) {
336+ // Some config dirs may be read-only.
337+ // Installing deps there will fail; skip installation in that case.
338+ const writable = await isWritable ( dir )
339+ if ( ! writable ) {
340+ log . debug ( "config dir is not writable, skipping dependency install" , { dir } )
341+ return false
342+ }
343+
344+ const nodeModules = path . join ( dir , "node_modules" )
345+ if ( ! existsSync ( nodeModules ) ) return true
346+
347+ const pkg = path . join ( dir , "package.json" )
348+ const pkgExists = await Filesystem . exists ( pkg )
349+ if ( ! pkgExists ) return true
350+
351+ const parsed = await Filesystem . readJson < { dependencies ?: Record < string , string > } > ( pkg ) . catch ( ( ) => null )
352+ const dependencies = parsed ?. dependencies ?? { }
353+ const depVersion = dependencies [ "@opencode-ai/plugin" ]
354+ if ( ! depVersion ) return true
355+
356+ const targetVersion = Installation . isLocal ( ) ? "latest" : Installation . VERSION
357+ if ( targetVersion === "latest" ) {
358+ const isOutdated = await PackageRegistry . isOutdated ( "@opencode-ai/plugin" , depVersion , dir )
359+ if ( ! isOutdated ) return false
360+ log . info ( "Cached version is outdated, proceeding with install" , {
361+ pkg : "@opencode-ai/plugin" ,
362+ cachedVersion : depVersion ,
363+ } )
364+ return true
365+ }
366+ if ( depVersion === targetVersion ) return false
367+ return true
368+ }
369+
306370 function rel ( item : string , patterns : string [ ] ) {
307371 const normalizedItem = item . replaceAll ( "\\" , "/" )
308372 for ( const pattern of patterns ) {
0 commit comments