import { existsSync, readFileSync } from "fs" import { join } from "path" import { homedir } from "os" import { BUILTIN_SERVERS, EXT_TO_LANG } from "./constants" export interface ResolvedServer { id: string command: string[] extensions: string[] priority: number env?: Record initialization?: Record } interface LspEntry { disabled?: boolean command?: string[] extensions?: string[] priority?: number env?: Record initialization?: Record } interface ConfigJson { lsp?: Record } type ConfigSource = "project" | "user" | "opencode" interface ServerWithSource extends ResolvedServer { source: ConfigSource } function loadJsonFile(path: string): T | null { if (!existsSync(path)) return null try { return JSON.parse(readFileSync(path, "utf-8")) as T } catch { return null } } function getConfigPaths(): { project: string; user: string; opencode: string } { const cwd = process.cwd() return { project: join(cwd, ".opencode", "oh-my-opencode.json"), user: join(homedir(), ".config", "opencode", "oh-my-opencode.json"), opencode: join(homedir(), ".config", "opencode", "opencode.json"), } } function loadAllConfigs(): Map { const paths = getConfigPaths() const configs = new Map() const project = loadJsonFile(paths.project) if (project) configs.set("project", project) const user = loadJsonFile(paths.user) if (user) configs.set("user", user) const opencode = loadJsonFile(paths.opencode) if (opencode) configs.set("opencode", opencode) return configs } function getMergedServers(): ServerWithSource[] { const configs = loadAllConfigs() const servers: ServerWithSource[] = [] const disabled = new Set() const seen = new Set() const sources: ConfigSource[] = ["project", "user", "opencode"] for (const source of sources) { const config = configs.get(source) if (!config?.lsp) continue for (const [id, entry] of Object.entries(config.lsp)) { if (entry.disabled) { disabled.add(id) continue } if (seen.has(id)) continue if (!entry.command || !entry.extensions) continue servers.push({ id, command: entry.command, extensions: entry.extensions, priority: entry.priority ?? 0, env: entry.env, initialization: entry.initialization, source, }) seen.add(id) } } for (const [id, config] of Object.entries(BUILTIN_SERVERS)) { if (disabled.has(id) || seen.has(id)) continue servers.push({ id, command: config.command, extensions: config.extensions, priority: -100, source: "opencode", }) } return servers.sort((a, b) => { if (a.source !== b.source) { const order: Record = { project: 0, user: 1, opencode: 2 } return order[a.source] - order[b.source] } return b.priority - a.priority }) } export function findServerForExtension(ext: string): ResolvedServer | null { const servers = getMergedServers() for (const server of servers) { if (server.extensions.includes(ext) && isServerInstalled(server.command)) { return { id: server.id, command: server.command, extensions: server.extensions, priority: server.priority, env: server.env, initialization: server.initialization, } } } return null } export function getLanguageId(ext: string): string { return EXT_TO_LANG[ext] || "plaintext" } export function isServerInstalled(command: string[]): boolean { if (command.length === 0) return false const cmd = command[0] const isWindows = process.platform === "win32" const ext = isWindows ? ".exe" : "" const pathEnv = process.env.PATH || "" const pathSeparator = isWindows ? ";" : ":" const paths = pathEnv.split(pathSeparator) for (const p of paths) { if (existsSync(join(p, cmd)) || existsSync(join(p, cmd + ext))) { return true } } const cwd = process.cwd() const additionalPaths = [ join(cwd, "node_modules", ".bin", cmd), join(cwd, "node_modules", ".bin", cmd + ext), join(homedir(), ".config", "opencode", "bin", cmd), join(homedir(), ".config", "opencode", "bin", cmd + ext), join(homedir(), ".config", "opencode", "node_modules", ".bin", cmd), join(homedir(), ".config", "opencode", "node_modules", ".bin", cmd + ext), ] for (const p of additionalPaths) { if (existsSync(p)) { return true } } return false } export function getAllServers(): Array<{ id: string installed: boolean extensions: string[] disabled: boolean source: string priority: number }> { const configs = loadAllConfigs() const servers = getMergedServers() const disabled = new Set() for (const config of configs.values()) { if (!config.lsp) continue for (const [id, entry] of Object.entries(config.lsp)) { if (entry.disabled) disabled.add(id) } } const result: Array<{ id: string installed: boolean extensions: string[] disabled: boolean source: string priority: number }> = [] const seen = new Set() for (const server of servers) { if (seen.has(server.id)) continue result.push({ id: server.id, installed: isServerInstalled(server.command), extensions: server.extensions, disabled: false, source: server.source, priority: server.priority, }) seen.add(server.id) } for (const id of disabled) { if (seen.has(id)) continue const builtin = BUILTIN_SERVERS[id] result.push({ id, installed: builtin ? isServerInstalled(builtin.command) : false, extensions: builtin?.extensions || [], disabled: true, source: "disabled", priority: 0, }) } return result } export function getConfigPaths_(): { project: string; user: string; opencode: string } { return getConfigPaths() }