diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f3d0d0b7ad3d..102af5a24fb3 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -321,6 +321,18 @@ export namespace Config { }) } + async function inherited(dir: string) { + if (!Installation.isLocal()) return false + + try { + const req = createRequire(path.join(dir, "package.json")) + const pkg = req.resolve("@opencode-ai/plugin/package.json") + return (await Filesystem.readJson<{ version?: string }>(pkg).catch(() => null))?.version === Installation.VERSION + } catch { + return false + } + } + async function isWritable(dir: string) { try { await fs.access(dir, constants.W_OK) @@ -339,11 +351,13 @@ export namespace Config { return false } + const pkg = path.join(dir, "package.json") + const pkgExists = await Filesystem.exists(pkg) + if (!pkgExists && (await inherited(dir))) return false + const nodeModules = path.join(dir, "node_modules") if (!existsSync(nodeModules)) return true - const pkg = path.join(dir, "package.json") - const pkgExists = await Filesystem.exists(pkg) if (!pkgExists) return true const parsed = await Filesystem.readJson<{ dependencies?: Record }>(pkg).catch(() => null) diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index 3ef3a02304dc..2f93e1d71813 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -234,11 +234,12 @@ export namespace PermissionNext { export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { const merged = merge(...rulesets) - log.info("evaluate", { permission, pattern, ruleset: merged }) - const match = merged.findLast( - (rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern), - ) - return match ?? { action: "ask", permission, pattern: "*" } + const rule = + merged.findLast( + (rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern), + ) ?? { action: "ask", permission, pattern: "*" } + log.info("evaluate", { permission, pattern, rule, rules: merged.length }) + return rule } const EDIT_TOOLS = ["edit", "write", "patch", "multiedit"] diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 90727cf8a083..91b85d25d67f 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -8,6 +8,7 @@ import path from "path" import fs from "fs/promises" import { pathToFileURL } from "url" import { Global } from "../../src/global" +import { Installation } from "../../src/installation" import { ProjectID } from "../../src/project/schema" import { Filesystem } from "../../src/util/filesystem" @@ -763,6 +764,24 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => { } }) +test("skips dependency install when plugin is already available from a parent install", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const cfg = path.join(dir, "project", ".opencode") + const mod = path.join(dir, "project", "node_modules", "@opencode-ai", "plugin") + await fs.mkdir(cfg, { recursive: true }) + await fs.mkdir(mod, { recursive: true }) + await Filesystem.write( + path.join(mod, "package.json"), + JSON.stringify({ name: "@opencode-ai/plugin", version: Installation.VERSION }), + ) + return cfg + }, + }) + + expect(await Config.needsInstall(tmp.extra)).toBe(false) +}) + test("resolves scoped npm plugins in config", async () => { await using tmp = await tmpdir({ init: async (dir) => { diff --git a/packages/opencode/test/permission-next.test.ts b/packages/opencode/test/permission-next.test.ts new file mode 100644 index 000000000000..5cff60a9400c --- /dev/null +++ b/packages/opencode/test/permission-next.test.ts @@ -0,0 +1,32 @@ +import { expect, test } from "bun:test" +import { setTimeout as sleep } from "node:timers/promises" +import { PermissionNext } from "../src/permission/next" +import { Log } from "../src/util/log" + +test("evaluate logs the matched rule without serializing the full ruleset", async () => { + const pattern = `permission-log-${Date.now()}` + PermissionNext.evaluate("task", pattern, [ + { + permission: "task", + pattern: "*", + action: "ask", + }, + { + permission: "task", + pattern, + action: "allow", + }, + ]) + + await sleep(50) + + const line = (await Bun.file(Log.file()).text()) + .trim() + .split("\n") + .findLast((x) => x.includes("service=permission") && x.includes(`pattern=${pattern}`)) + + expect(line).toBeDefined() + expect(line).toContain("rule=") + expect(line).toContain("rules=2") + expect(line).not.toContain("ruleset=") +})