import fs from "fs/promises" import path from "path" import type { ClaudeHomeConfig } from "../parsers/claude-home" import type { ClaudeMcpServer } from "../types/claude" import { syncGeminiCommands } from "./commands" import { mergeJsonConfigAtKey } from "./json-config" import { syncSkills } from "./skills" type GeminiMcpServer = { command?: string args?: string[] url?: string env?: Record headers?: Record } export async function syncToGemini( config: ClaudeHomeConfig, outputRoot: string, ): Promise { await syncGeminiSkills(config.skills, outputRoot) await syncGeminiCommands(config, outputRoot) if (Object.keys(config.mcpServers).length > 0) { const settingsPath = path.join(outputRoot, "settings.json") const converted = convertMcpForGemini(config.mcpServers) await mergeJsonConfigAtKey({ configPath: settingsPath, key: "mcpServers", incoming: converted, }) } } async function syncGeminiSkills( skills: ClaudeHomeConfig["skills"], outputRoot: string, ): Promise { const skillsDir = path.join(outputRoot, "skills") const sharedSkillsDir = getGeminiSharedSkillsDir(outputRoot) if (!sharedSkillsDir) { await syncSkills(skills, skillsDir) return } const canonicalSharedSkillsDir = await canonicalizePath(sharedSkillsDir) const mirroredSkills: ClaudeHomeConfig["skills"] = [] const directSkills: ClaudeHomeConfig["skills"] = [] for (const skill of skills) { if (await isWithinDir(skill.sourceDir, canonicalSharedSkillsDir)) { mirroredSkills.push(skill) } else { directSkills.push(skill) } } await removeGeminiMirrorConflicts(mirroredSkills, skillsDir, canonicalSharedSkillsDir) await syncSkills(directSkills, skillsDir) } function getGeminiSharedSkillsDir(outputRoot: string): string | null { if (path.basename(outputRoot) !== ".gemini") return null return path.join(path.dirname(outputRoot), ".agents", "skills") } async function canonicalizePath(targetPath: string): Promise { try { return await fs.realpath(targetPath) } catch { return path.resolve(targetPath) } } async function isWithinDir(candidate: string, canonicalParentDir: string): Promise { const resolvedCandidate = await canonicalizePath(candidate) return resolvedCandidate === canonicalParentDir || resolvedCandidate.startsWith(`${canonicalParentDir}${path.sep}`) } async function removeGeminiMirrorConflicts( skills: ClaudeHomeConfig["skills"], skillsDir: string, sharedSkillsDir: string, ): Promise { for (const skill of skills) { const duplicatePath = path.join(skillsDir, skill.name) let stat try { stat = await fs.lstat(duplicatePath) } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") { continue } throw error } if (!stat.isSymbolicLink()) { continue } let resolvedTarget: string try { resolvedTarget = await canonicalizePath(duplicatePath) } catch { continue } if (resolvedTarget === await canonicalizePath(skill.sourceDir) || await isWithinDir(resolvedTarget, sharedSkillsDir)) { await fs.unlink(duplicatePath) } } } function convertMcpForGemini( servers: Record, ): Record { const result: Record = {} for (const [name, server] of Object.entries(servers)) { const entry: GeminiMcpServer = {} if (server.command) { entry.command = server.command if (server.args && server.args.length > 0) entry.args = server.args if (server.env && Object.keys(server.env).length > 0) entry.env = server.env } else if (server.url) { entry.url = server.url if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers } result[name] = entry } return result }