Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 15 additions & 6 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,21 +144,30 @@ export namespace Config {
waitTick?: (input: { dir: string; attempt: number; delay: number; waited: number }) => void | Promise<void>
}

function installRoot(dir: string) {
if (Filesystem.resolve(dir) !== Filesystem.resolve(Global.Path.config)) return dir
return path.join(Global.Path.cache, "config", "deps")
}

export async function installDependencies(dir: string, input?: InstallInput) {
if (!(await isWritable(dir))) return
await using _ = await Flock.acquire(`config-install:${Filesystem.resolve(dir)}`, {
const root = installRoot(dir)
if (!(await isWritable(root))) {
await fsNode.mkdir(root, { recursive: true }).catch(() => undefined)
}
if (!(await isWritable(root))) return
await using _ = await Flock.acquire(`config-install:${Filesystem.resolve(root)}`, {
signal: input?.signal,
onWait: (tick) =>
input?.waitTick?.({
dir,
dir: root,
attempt: tick.attempt,
delay: tick.delay,
waited: tick.waited,
}),
})
input?.signal?.throwIfAborted()

const pkg = path.join(dir, "package.json")
const pkg = path.join(root, "package.json")
const target = Installation.isLocal() ? "*" : Installation.VERSION
const json = await Filesystem.readJson<{ dependencies?: Record<string, string> }>(pkg).catch(() => ({
dependencies: {},
Expand All @@ -169,15 +178,15 @@ export namespace Config {
}
await Filesystem.writeJson(pkg, json)

const gitignore = path.join(dir, ".gitignore")
const gitignore = path.join(root, ".gitignore") //seems like useless patch for package managers.
const ignore = await Filesystem.exists(gitignore)
if (!ignore) {
await Filesystem.write(
gitignore,
["node_modules", "package.json", "package-lock.json", "bun.lock", ".gitignore"].join("\n"),
)
}
await Npm.install(dir)
await Npm.install(root)
}

async function isWritable(dir: string) {
Expand Down
51 changes: 51 additions & 0 deletions packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,57 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
}
})

test("installs global config dependencies in cache instead of config", async () => {
const root = path.join(Global.Path.cache, "config", "deps")
await fs.rm(root, { recursive: true, force: true }).catch(() => {})
await fs.rm(path.join(Global.Path.config, "package.json"), { force: true }).catch(() => {})
await fs.rm(path.join(Global.Path.config, ".gitignore"), { force: true }).catch(() => {})

const run = spyOn(Npm, "install").mockImplementation(async (dir: string) => {
const mod = path.join(dir, "node_modules", "@opencode-ai", "plugin")
await fs.mkdir(mod, { recursive: true })
await Filesystem.write(
path.join(mod, "package.json"),
JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }),
)
})

try {
await Config.installDependencies(Global.Path.config)
expect(run).toHaveBeenCalledWith(root)
expect(await Filesystem.exists(path.join(root, "package.json"))).toBe(true)
expect(await Filesystem.exists(path.join(root, ".gitignore"))).toBe(true)
expect(await Filesystem.exists(path.join(Global.Path.config, "package.json"))).toBe(false)
expect(await Filesystem.exists(path.join(Global.Path.config, ".gitignore"))).toBe(false)
} finally {
run.mockRestore()
}
})

test("installs non-global config dependencies in place", async () => {
await using tmp = await tmpdir()
const dir = path.join(tmp.path, "custom")
await fs.mkdir(dir, { recursive: true })

const run = spyOn(Npm, "install").mockImplementation(async (cwd: string) => {
const mod = path.join(cwd, "node_modules", "@opencode-ai", "plugin")
await fs.mkdir(mod, { recursive: true })
await Filesystem.write(
path.join(mod, "package.json"),
JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }),
)
})

try {
await Config.installDependencies(dir)
expect(run).toHaveBeenCalledWith(dir)
expect(await Filesystem.exists(path.join(dir, "package.json"))).toBe(true)
expect(await Filesystem.exists(path.join(dir, ".gitignore"))).toBe(true)
} finally {
run.mockRestore()
}
})

test("dedupes concurrent config dependency installs for the same dir", async () => {
await using tmp = await tmpdir()
const dir = path.join(tmp.path, "a")
Expand Down
Loading