Skip to content

Commit c6880aa

Browse files
committed
Stabilize local .opencode install checks on Windows
1 parent 29d77ab commit c6880aa

4 files changed

Lines changed: 84 additions & 0 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { base64Encode } from "@opencode-ai/util/encode"
3+
import { normalizeDirectory } from "./server"
4+
5+
describe("normalizeDirectory", () => {
6+
test("keeps absolute posix directories unchanged", () => {
7+
expect(normalizeDirectory("/tmp/demo")).toBe("/tmp/demo")
8+
})
9+
10+
test("decodes posix route slugs into absolute directories", () => {
11+
expect(normalizeDirectory(base64Encode("/tmp/demo"))).toBe("/tmp/demo")
12+
})
13+
14+
test("decodes windows route slugs into absolute directories", () => {
15+
expect(normalizeDirectory(base64Encode("C:\\Users\\demo\\repo"))).toBe("C:\\Users\\demo\\repo")
16+
})
17+
18+
test("does not rewrite plain relative values", () => {
19+
expect(normalizeDirectory("workspace")).toBe("workspace")
20+
expect(normalizeDirectory(base64Encode("workspace"))).toBe(base64Encode("workspace"))
21+
})
22+
})

packages/app/src/utils/server.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
22
import type { ServerConnection } from "@/context/server"
3+
import { decode64 } from "./base64"
4+
5+
function absolute(dir: string) {
6+
return dir.startsWith("/") || /^[A-Za-z]:[\\/]/.test(dir) || dir.startsWith("\\\\")
7+
}
8+
9+
export function normalizeDirectory(dir?: string) {
10+
if (!dir || absolute(dir)) return dir
11+
const next = decode64(dir)
12+
if (!next || !absolute(next)) return dir
13+
return next
14+
}
315

416
export function createSdkForServer({
517
server,
@@ -16,6 +28,7 @@ export function createSdkForServer({
1628

1729
return createOpencodeClient({
1830
...config,
31+
directory: normalizeDirectory(config.directory),
1932
headers: { ...config.headers, ...auth },
2033
baseUrl: server.url,
2134
})

packages/opencode/src/config/config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,25 @@ export namespace Config {
330330
}
331331
}
332332

333+
async function shared(dir: string) {
334+
if (!Installation.isLocal()) return false
335+
336+
const roots = [Instance.directory, Instance.worktree].filter(Boolean)
337+
for (const root of roots) {
338+
const rel = path.relative(root, dir)
339+
if (path.isAbsolute(rel) || rel.startsWith("..")) continue
340+
if (await Filesystem.exists(path.join(root, "node_modules", "@opencode-ai", "plugin"))) return true
341+
}
342+
343+
return false
344+
}
345+
333346
export async function needsInstall(dir: string) {
347+
if (await shared(dir)) {
348+
log.debug("config dir can use shared local dependencies, skipping dependency install", { dir })
349+
return false
350+
}
351+
334352
// Some config dirs may be read-only.
335353
// Installing deps there will fail; skip installation in that case.
336354
const writable = await isWritable(dir)

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,37 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
763763
}
764764
})
765765

766+
test("skips installs for project .opencode when local deps are available upstream", async () => {
767+
await using tmp = await tmpdir({
768+
init: async (dir) => {
769+
await fs.mkdir(path.join(dir, "node_modules", "@opencode-ai", "plugin"), { recursive: true })
770+
await fs.mkdir(path.join(dir, ".opencode"), { recursive: true })
771+
},
772+
})
773+
774+
await Instance.provide({
775+
directory: tmp.path,
776+
fn: async () => {
777+
expect(await Config.needsInstall(path.join(tmp.path, ".opencode"))).toBe(false)
778+
},
779+
})
780+
})
781+
782+
test("still installs for project .opencode when shared local deps are missing", async () => {
783+
await using tmp = await tmpdir({
784+
init: async (dir) => {
785+
await fs.mkdir(path.join(dir, ".opencode"), { recursive: true })
786+
},
787+
})
788+
789+
await Instance.provide({
790+
directory: tmp.path,
791+
fn: async () => {
792+
expect(await Config.needsInstall(path.join(tmp.path, ".opencode"))).toBe(true)
793+
},
794+
})
795+
})
796+
766797
test("resolves scoped npm plugins in config", async () => {
767798
await using tmp = await tmpdir({
768799
init: async (dir) => {

0 commit comments

Comments
 (0)