| 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<string, string>; |
| notes: string[]; |
| cleanup: () => Promise<void>; |
| }; |
|
|
| function resolveXdgConfigHome(env: Record<string, string>): 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<string, unknown> { |
| return typeof value === "object" && value !== null && !Array.isArray(value); |
| } |
|
|
| async function readJsonObject(filepath: string): Promise<Record<string, unknown>> { |
| 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<string, string>; |
| config: Record<string, unknown>; |
| }): Promise<PreparedOpenCodeRuntimeConfig> { |
| const skipPermissions = asBoolean(input.config.dangerouslySkipPermissions, true); |
| if (!skipPermissions) { |
| 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 }); |
| }, |
| }; |
| } |
|
|