import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { asBoolean } from "@paperclipai/adapter-utils/server-utils"; type PreparedOpenCodeRuntimeConfig = { env: Record; notes: string[]; cleanup: () => Promise; }; function resolveXdgConfigHome(env: Record): string { return ( (typeof env.XDG_CONFIG_HOME === "string" && env.XDG_CONFIG_HOME.trim()) || (typeof process.env.XDG_CONFIG_HOME === "string" && process.env.XDG_CONFIG_HOME.trim()) || path.join(os.homedir(), ".config") ); } function isPlainObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } async function readJsonObject(filepath: string): Promise> { try { const raw = await fs.readFile(filepath, "utf8"); const parsed = JSON.parse(raw); return isPlainObject(parsed) ? parsed : {}; } catch { return {}; } } export async function prepareOpenCodeRuntimeConfig(input: { env: Record; config: Record; targetIsRemote?: boolean; }): Promise { const skipPermissions = asBoolean(input.config.dangerouslySkipPermissions, true); if (!skipPermissions) { return { env: input.env, notes: [], cleanup: async () => {}, }; } // For remote execution targets the host XDG_CONFIG_HOME path is meaningless // (and actively harmful — it leaks a macOS-only path into the remote Linux // env). Callers that need to ship a runtime opencode config to the remote // box do that via prepareAdapterExecutionTargetRuntime in execute.ts; this // host-fs helper is local-only. if (input.targetIsRemote) { return { env: input.env, notes: [], cleanup: async () => {}, }; } const sourceConfigDir = path.join(resolveXdgConfigHome(input.env), "opencode"); const runtimeConfigHome = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-config-")); const runtimeConfigDir = path.join(runtimeConfigHome, "opencode"); const runtimeConfigPath = path.join(runtimeConfigDir, "opencode.json"); await fs.mkdir(runtimeConfigDir, { recursive: true }); try { await fs.cp(sourceConfigDir, runtimeConfigDir, { recursive: true, force: true, errorOnExist: false, dereference: false, }); } catch (err) { if ((err as NodeJS.ErrnoException | null)?.code !== "ENOENT") { throw err; } } const existingConfig = await readJsonObject(runtimeConfigPath); const existingPermission = isPlainObject(existingConfig.permission) ? existingConfig.permission : {}; const nextConfig = { ...existingConfig, permission: { ...existingPermission, external_directory: "allow", }, }; await fs.writeFile(runtimeConfigPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8"); return { env: { ...input.env, XDG_CONFIG_HOME: runtimeConfigHome, }, notes: [ "Injected runtime OpenCode config with permission.external_directory=allow to avoid headless approval prompts.", ], cleanup: async () => { await fs.rm(runtimeConfigHome, { recursive: true, force: true }); }, }; }