import { existsSync, realpathSync } from "node:fs" import { homedir } from "node:os" import { join, resolve, win32 } from "node:path" import { CONFIG_BASENAME } from "./plugin-identity" import type { OpenCodeBinaryType, OpenCodeConfigDirOptions, OpenCodeConfigPaths, } from "./opencode-config-dir-types" export type { OpenCodeBinaryType, OpenCodeConfigDirOptions, OpenCodeConfigPaths, } from "./opencode-config-dir-types" export const TAURI_APP_IDENTIFIER = "ai.opencode.desktop" export const TAURI_APP_IDENTIFIER_DEV = "ai.opencode.desktop.dev" export function isDevBuild(version: string | null | undefined): boolean { if (!version) return false return version.includes("-dev") || version.includes(".dev") } function getTauriConfigDir(identifier: string): string { const platform = process.platform switch (platform) { case "darwin": return join(homedir(), "Library", "Application Support", identifier) case "win32": { const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming") return win32.join(appData, identifier) } case "linux": default: { const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config") return join(xdgConfig, identifier) } } } function resolveConfigPath(pathValue: string): string { const resolvedPath = resolve(pathValue) if (!existsSync(resolvedPath)) return resolvedPath try { return realpathSync(resolvedPath) } catch { return resolvedPath } } function getCliDefaultConfigDir(): string { const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config") return resolveConfigPath(join(xdgConfig, "opencode")) } function getCliCustomConfigDir(): string | null { const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim() if (!envConfigDir) { return null } return resolveConfigPath(envConfigDir) } function getCliConfigDir(): string { return getCliCustomConfigDir() ?? getCliDefaultConfigDir() } export function getOpenCodeConfigDirs(options: OpenCodeConfigDirOptions): string[] { if (options.binary !== "opencode") { return [getOpenCodeConfigDir(options)] } return Array.from( new Set([ ...(getCliCustomConfigDir() ? [getCliCustomConfigDir()!] : []), getCliDefaultConfigDir(), ]), ) } export function getOpenCodeConfigDir(options: OpenCodeConfigDirOptions): string { const { binary, version, checkExisting = true } = options if (binary === "opencode") { return getCliConfigDir() } const identifier = isDevBuild(version) ? TAURI_APP_IDENTIFIER_DEV : TAURI_APP_IDENTIFIER const tauriDirBase = getTauriConfigDir(identifier) const tauriDir = process.platform === "win32" ? (win32.isAbsolute(tauriDirBase) ? win32.normalize(tauriDirBase) : win32.resolve(tauriDirBase)) : resolveConfigPath(tauriDirBase) if (checkExisting) { const legacyDir = getCliConfigDir() const legacyConfig = join(legacyDir, "opencode.json") const legacyConfigC = join(legacyDir, "opencode.jsonc") if (existsSync(legacyConfig) || existsSync(legacyConfigC)) { return legacyDir } } return tauriDir } export function getOpenCodeConfigPaths(options: OpenCodeConfigDirOptions): OpenCodeConfigPaths { const configDir = getOpenCodeConfigDir(options) return { configDir, configJson: join(configDir, "opencode.json"), configJsonc: join(configDir, "opencode.jsonc"), packageJson: join(configDir, "package.json"), omoConfig: join(configDir, `${CONFIG_BASENAME}.json`), } } export function detectExistingConfigDir(binary: OpenCodeBinaryType, version?: string | null): string | null { const locations: string[] = [] const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim() if (envConfigDir) { locations.push(resolveConfigPath(envConfigDir)) } if (binary === "opencode-desktop") { const identifier = isDevBuild(version) ? TAURI_APP_IDENTIFIER_DEV : TAURI_APP_IDENTIFIER locations.push(getTauriConfigDir(identifier)) if (isDevBuild(version)) { locations.push(getTauriConfigDir(TAURI_APP_IDENTIFIER)) } } locations.push(getCliConfigDir()) for (const dir of locations) { const configJson = join(dir, "opencode.json") const configJsonc = join(dir, "opencode.jsonc") if (existsSync(configJson) || existsSync(configJsonc)) { return dir } } return null }