Skip to content

Commit 0d38e69

Browse files
authored
fix(core): skip dependency install in read-only config dirs (anomalyco#12128)
1 parent 9679e0c commit 0d38e69

2 files changed

Lines changed: 79 additions & 1 deletion

File tree

packages/opencode/src/config/config.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { LSPServer } from "../lsp/server"
2424
import { BunProc } from "@/bun"
2525
import { Installation } from "@/installation"
2626
import { ConfigMarkdown } from "./markdown"
27-
import { existsSync } from "fs"
27+
import { constants, existsSync } from "fs"
2828
import { Bus } from "@/bus"
2929
import { GlobalBus } from "@/bus/global"
3030
import { Event } from "../server/event"
@@ -273,7 +273,24 @@ export namespace Config {
273273
).catch(() => {})
274274
}
275275

276+
async function isWritable(dir: string) {
277+
try {
278+
await fs.access(dir, constants.W_OK)
279+
return true
280+
} catch {
281+
return false
282+
}
283+
}
284+
276285
async function needsInstall(dir: string) {
286+
// Some config dirs may be read-only.
287+
// Installing deps there will fail; skip installation in that case.
288+
const writable = await isWritable(dir)
289+
if (!writable) {
290+
log.debug("config dir is not writable, skipping dependency install", { dir })
291+
return false
292+
}
293+
277294
const nodeModules = path.join(dir, "node_modules")
278295
if (!existsSync(nodeModules)) return true
279296

packages/opencode/test/config/config.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,67 @@ test("gets config directories", async () => {
566566
})
567567
})
568568

569+
test("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR", async () => {
570+
if (process.platform === "win32") return
571+
572+
await using tmp = await tmpdir<string>({
573+
init: async (dir) => {
574+
const ro = path.join(dir, "readonly")
575+
await fs.mkdir(ro, { recursive: true })
576+
await fs.chmod(ro, 0o555)
577+
return ro
578+
},
579+
dispose: async (dir) => {
580+
const ro = path.join(dir, "readonly")
581+
await fs.chmod(ro, 0o755).catch(() => {})
582+
return ro
583+
},
584+
})
585+
586+
const prev = process.env.OPENCODE_CONFIG_DIR
587+
process.env.OPENCODE_CONFIG_DIR = tmp.extra
588+
589+
try {
590+
await Instance.provide({
591+
directory: tmp.path,
592+
fn: async () => {
593+
await Config.get()
594+
},
595+
})
596+
} finally {
597+
if (prev === undefined) delete process.env.OPENCODE_CONFIG_DIR
598+
else process.env.OPENCODE_CONFIG_DIR = prev
599+
}
600+
})
601+
602+
test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
603+
await using tmp = await tmpdir<string>({
604+
init: async (dir) => {
605+
const cfg = path.join(dir, "configdir")
606+
await fs.mkdir(cfg, { recursive: true })
607+
return cfg
608+
},
609+
})
610+
611+
const prev = process.env.OPENCODE_CONFIG_DIR
612+
process.env.OPENCODE_CONFIG_DIR = tmp.extra
613+
614+
try {
615+
await Instance.provide({
616+
directory: tmp.path,
617+
fn: async () => {
618+
await Config.get()
619+
},
620+
})
621+
622+
expect(await Bun.file(path.join(tmp.extra, "package.json")).exists()).toBe(true)
623+
expect(await Bun.file(path.join(tmp.extra, ".gitignore")).exists()).toBe(true)
624+
} finally {
625+
if (prev === undefined) delete process.env.OPENCODE_CONFIG_DIR
626+
else process.env.OPENCODE_CONFIG_DIR = prev
627+
}
628+
})
629+
569630
test("resolves scoped npm plugins in config", async () => {
570631
await using tmp = await tmpdir({
571632
init: async (dir) => {

0 commit comments

Comments
 (0)