Skip to content

Commit 0ae35e0

Browse files
Merge branch 'dev' into fix-env-caching-12698
2 parents 45b50db + 4781564 commit 0ae35e0

4 files changed

Lines changed: 76 additions & 3 deletions

File tree

packages/core/src/npm.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,17 @@ export const layer = Layer.effect(
120120
}
121121
})()
122122

123-
if (yield* afs.existsSafe(dir)) {
123+
if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) {
124124
return resolveEntryPoint(name, path.join(dir, "node_modules", name))
125125
}
126126

127127
const tree = yield* reify({ dir, add: [pkg] })
128128
const first = tree.edgesOut.values().next().value?.to
129-
if (!first) return yield* new InstallFailedError({ add: [pkg], dir })
129+
if (!first) {
130+
const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
131+
if (Option.isSome(result.entrypoint)) return result
132+
return yield* new InstallFailedError({ add: [pkg], dir })
133+
}
130134
return resolveEntryPoint(first.name, first.path)
131135
}, Effect.scoped)
132136

packages/core/test/global.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, expect, test } from "bun:test"
2+
import fs from "fs/promises"
3+
import os from "os"
4+
import path from "path"
5+
import { Global } from "@opencode-ai/core/global"
6+
7+
describe("global paths", () => {
8+
test("tmp path is under the system temp directory", () => {
9+
expect(Global.Path.tmp).toBe(path.join(os.tmpdir(), "opencode"))
10+
expect(Global.make().tmp).toBe(Global.Path.tmp)
11+
})
12+
13+
test("tmp path is created on module load", async () => {
14+
expect((await fs.stat(Global.Path.tmp)).isDirectory()).toBe(true)
15+
})
16+
})

packages/core/test/npm.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import fs from "fs/promises"
22
import path from "path"
33
import { describe, expect, test } from "bun:test"
4+
import { NodeFileSystem } from "@effect/platform-node"
5+
import { Effect, Layer, Option } from "effect"
6+
import { AppFileSystem } from "@opencode-ai/core/filesystem"
7+
import { Global } from "@opencode-ai/core/global"
48
import { Npm } from "@opencode-ai/core/npm"
9+
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
510
import { tmpdir } from "./fixture/tmpdir"
611

712
const win = process.platform === "win32"
@@ -15,6 +20,14 @@ const writePackage = (dir: string, pkg: Record<string, unknown>) =>
1520
}),
1621
)
1722

23+
const npmLayer = (cache: string) =>
24+
Npm.layer.pipe(
25+
Layer.provide(EffectFlock.layer),
26+
Layer.provide(AppFileSystem.layer),
27+
Layer.provide(Global.layerWith({ cache, state: path.join(cache, "state") })),
28+
Layer.provide(NodeFileSystem.layer),
29+
)
30+
1831
describe("Npm.sanitize", () => {
1932
test("keeps normal scoped package specs unchanged", () => {
2033
expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme")
@@ -29,6 +42,28 @@ describe("Npm.sanitize", () => {
2942
})
3043
})
3144

45+
describe("Npm.add", () => {
46+
test("reifies when package cache directory exists without the package installed", async () => {
47+
await using tmp = await tmpdir()
48+
await fs.mkdir(path.join(tmp.path, "fixture-provider"))
49+
await writePackage(path.join(tmp.path, "fixture-provider"), {
50+
name: "fixture-provider",
51+
main: "index.js",
52+
})
53+
await Bun.write(path.join(tmp.path, "fixture-provider", "index.js"), "export const fixture = true\n")
54+
55+
const spec = `fixture-provider@file:${path.join(tmp.path, "fixture-provider")}`
56+
await fs.mkdir(path.join(tmp.path, "cache", "packages", Npm.sanitize(spec)), { recursive: true })
57+
58+
const entry = await Effect.gen(function* () {
59+
const npm = yield* Npm.Service
60+
return yield* npm.add(spec)
61+
}).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
62+
63+
expect(Option.isSome(entry.entrypoint)).toBe(true)
64+
})
65+
})
66+
3267
describe("Npm.install", () => {
3368
test("respects omit from project .npmrc", async () => {
3469
await using tmp = await tmpdir()

packages/opencode/test/agent/agent.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { provideInstance, tmpdir } from "../fixture/fixture"
55
import { Instance } from "../../src/project/instance"
66
import { Agent } from "../../src/agent/agent"
77
import { Permission } from "../../src/permission"
8+
import { Global } from "@opencode-ai/core/global"
89

910
// Helper to evaluate permission for a tool with wildcard pattern
1011
function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined {
@@ -83,7 +84,7 @@ test("explore agent denies edit and write", async () => {
8384
})
8485
})
8586

86-
test("explore agent asks for external directories and allows Truncate.GLOB", async () => {
87+
test("explore agent asks for external directories and allows whitelisted external paths", async () => {
8788
const { Truncate } = await import("../../src/tool/truncate")
8889
await using tmp = await tmpdir()
8990
await Instance.provide({
@@ -93,6 +94,9 @@ test("explore agent asks for external directories and allows Truncate.GLOB", asy
9394
expect(explore).toBeDefined()
9495
expect(Permission.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask")
9596
expect(Permission.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow")
97+
expect(
98+
Permission.evaluate("external_directory", path.join(Global.Path.tmp, "agent-work"), explore!.permission).action,
99+
).toBe("allow")
96100
},
97101
})
98102
})
@@ -515,6 +519,20 @@ test("Truncate.GLOB is allowed even when user denies external_directory globally
515519
})
516520
})
517521

522+
test("global tmp directory children are allowed for external_directory", async () => {
523+
await using tmp = await tmpdir()
524+
await Instance.provide({
525+
directory: tmp.path,
526+
fn: async () => {
527+
const build = await load(tmp.path, (svc) => svc.get("build"))
528+
expect(
529+
Permission.evaluate("external_directory", path.join(Global.Path.tmp, "scratch"), build!.permission).action,
530+
).toBe("allow")
531+
expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("ask")
532+
},
533+
})
534+
})
535+
518536
test("Truncate.GLOB is allowed even when user denies external_directory per-agent", async () => {
519537
const { Truncate } = await import("../../src/tool/truncate")
520538
await using tmp = await tmpdir({

0 commit comments

Comments
 (0)