import fs from "fs/promises" import path from "path" import type { ClaudeHomeConfig } from "../parsers/claude-home" import type { ClaudeMcpServer } from "../types/claude" import { forceSymlink, isValidSkillName } from "../utils/symlink" type CursorMcpServer = { command?: string args?: string[] url?: string env?: Record headers?: Record } type CursorMcpConfig = { mcpServers: Record } export async function syncToCursor( config: ClaudeHomeConfig, outputRoot: string, ): Promise { const skillsDir = path.join(outputRoot, "skills") await fs.mkdir(skillsDir, { recursive: true }) for (const skill of config.skills) { if (!isValidSkillName(skill.name)) { console.warn(`Skipping skill with invalid name: ${skill.name}`) continue } const target = path.join(skillsDir, skill.name) await forceSymlink(skill.sourceDir, target) } if (Object.keys(config.mcpServers).length > 0) { const mcpPath = path.join(outputRoot, "mcp.json") const existing = await readJsonSafe(mcpPath) const converted = convertMcpForCursor(config.mcpServers) const merged: CursorMcpConfig = { mcpServers: { ...(existing.mcpServers ?? {}), ...converted, }, } await fs.writeFile(mcpPath, JSON.stringify(merged, null, 2), { mode: 0o600 }) } } async function readJsonSafe(filePath: string): Promise> { try { const content = await fs.readFile(filePath, "utf-8") return JSON.parse(content) as Partial } catch (err) { if ((err as NodeJS.ErrnoException).code === "ENOENT") { return {} } throw err } } function convertMcpForCursor( servers: Record, ): Record { const result: Record = {} for (const [name, server] of Object.entries(servers)) { const entry: CursorMcpServer = {} 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 }