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
18 changes: 16 additions & 2 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<string, string> }>(pkg).catch(() => null)
Expand Down
11 changes: 6 additions & 5 deletions packages/opencode/src/permission/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
19 changes: 19 additions & 0 deletions packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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<string>({
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) => {
Expand Down
32 changes: 32 additions & 0 deletions packages/opencode/test/permission-next.test.ts
Original file line number Diff line number Diff line change
@@ -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=")
})
Loading