diff --git a/src/commands/convert.ts b/src/commands/convert.ts index 9f62511..7ac3d88 100644 --- a/src/commands/convert.ts +++ b/src/commands/convert.ts @@ -6,6 +6,7 @@ import { targets } from "../targets" import type { PermissionMode } from "../converters/claude-to-opencode" import { ensureCodexAgentsFile } from "../utils/codex-agents" import { expandHome, resolveTargetHome } from "../utils/resolve-home" +import { detectInstalledTools } from "../utils/detect-tools" const permissionModes: PermissionMode[] = ["none", "broad", "from-commands"] @@ -23,7 +24,7 @@ export default defineCommand({ to: { type: "string", default: "opencode", - description: "Target format (opencode | codex | droid | cursor | pi | gemini)", + description: "Target format (opencode | codex | droid | cursor | pi | gemini | all)", }, output: { type: "string", @@ -62,14 +63,6 @@ export default defineCommand({ }, async run({ args }) { const targetName = String(args.to) - const target = targets[targetName] - if (!target) { - throw new Error(`Unknown target: ${targetName}`) - } - - if (!target.implemented) { - throw new Error(`Target ${targetName} is registered but not implemented yet.`) - } const permissions = String(args.permissions) if (!permissionModes.includes(permissions as PermissionMode)) { @@ -87,6 +80,51 @@ export default defineCommand({ permissions: permissions as PermissionMode, } + if (targetName === "all") { + const detected = await detectInstalledTools() + const activeTargets = detected.filter((t) => t.detected) + + if (activeTargets.length === 0) { + console.log("No AI coding tools detected. Install at least one tool first.") + return + } + + console.log(`Detected ${activeTargets.length} tool(s):`) + for (const tool of detected) { + console.log(` ${tool.detected ? "✓" : "✗"} ${tool.name} — ${tool.reason}`) + } + + for (const tool of activeTargets) { + const handler = targets[tool.name] + if (!handler || !handler.implemented) { + console.warn(`Skipping ${tool.name}: not implemented.`) + continue + } + const bundle = handler.convert(plugin, options) + if (!bundle) { + console.warn(`Skipping ${tool.name}: no output returned.`) + continue + } + const root = resolveTargetOutputRoot(tool.name, outputRoot, codexHome, piHome) + await handler.write(root, bundle) + console.log(`Converted ${plugin.manifest.name} to ${tool.name} at ${root}`) + } + + if (activeTargets.some((t) => t.name === "codex")) { + await ensureCodexAgentsFile(codexHome) + } + return + } + + const target = targets[targetName] + if (!target) { + throw new Error(`Unknown target: ${targetName}`) + } + + if (!target.implemented) { + throw new Error(`Target ${targetName} is registered but not implemented yet.`) + } + const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, piHome) const bundle = target.convert(plugin, options) if (!bundle) { diff --git a/src/commands/install.ts b/src/commands/install.ts index 35506e8..fb91e4a 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -8,6 +8,7 @@ import { pathExists } from "../utils/files" import type { PermissionMode } from "../converters/claude-to-opencode" import { ensureCodexAgentsFile } from "../utils/codex-agents" import { expandHome, resolveTargetHome } from "../utils/resolve-home" +import { detectInstalledTools } from "../utils/detect-tools" const permissionModes: PermissionMode[] = ["none", "broad", "from-commands"] @@ -25,7 +26,7 @@ export default defineCommand({ to: { type: "string", default: "opencode", - description: "Target format (opencode | codex | droid | cursor | pi | gemini)", + description: "Target format (opencode | codex | droid | cursor | pi | gemini | all)", }, output: { type: "string", @@ -64,13 +65,6 @@ export default defineCommand({ }, async run({ args }) { const targetName = String(args.to) - const target = targets[targetName] - if (!target) { - throw new Error(`Unknown target: ${targetName}`) - } - if (!target.implemented) { - throw new Error(`Target ${targetName} is registered but not implemented yet.`) - } const permissions = String(args.permissions) if (!permissionModes.includes(permissions as PermissionMode)) { @@ -84,6 +78,7 @@ export default defineCommand({ const outputRoot = resolveOutputRoot(args.output) const codexHome = resolveTargetHome(args.codexHome, path.join(os.homedir(), ".codex")) const piHome = resolveTargetHome(args.piHome, path.join(os.homedir(), ".pi", "agent")) + const hasExplicitOutput = Boolean(args.output && String(args.output).trim()) const options = { agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent", @@ -91,11 +86,54 @@ export default defineCommand({ permissions: permissions as PermissionMode, } + if (targetName === "all") { + const detected = await detectInstalledTools() + const activeTargets = detected.filter((t) => t.detected) + + if (activeTargets.length === 0) { + console.log("No AI coding tools detected. Install at least one tool first.") + return + } + + console.log(`Detected ${activeTargets.length} tool(s):`) + for (const tool of detected) { + console.log(` ${tool.detected ? "✓" : "✗"} ${tool.name} — ${tool.reason}`) + } + + for (const tool of activeTargets) { + const handler = targets[tool.name] + if (!handler || !handler.implemented) { + console.warn(`Skipping ${tool.name}: not implemented.`) + continue + } + const bundle = handler.convert(plugin, options) + if (!bundle) { + console.warn(`Skipping ${tool.name}: no output returned.`) + continue + } + const root = resolveTargetOutputRoot(tool.name, outputRoot, codexHome, piHome, hasExplicitOutput) + await handler.write(root, bundle) + console.log(`Installed ${plugin.manifest.name} to ${tool.name} at ${root}`) + } + + if (activeTargets.some((t) => t.name === "codex")) { + await ensureCodexAgentsFile(codexHome) + } + return + } + + const target = targets[targetName] + if (!target) { + throw new Error(`Unknown target: ${targetName}`) + } + if (!target.implemented) { + throw new Error(`Target ${targetName} is registered but not implemented yet.`) + } + const bundle = target.convert(plugin, options) if (!bundle) { throw new Error(`Target ${targetName} did not return a bundle.`) } - const hasExplicitOutput = Boolean(args.output && String(args.output).trim()) const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, piHome, hasExplicitOutput) await target.write(primaryOutputRoot, bundle) console.log(`Installed ${plugin.manifest.name} to ${primaryOutputRoot}`) diff --git a/src/commands/sync.ts b/src/commands/sync.ts index e5b576e..c860ca3 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -7,9 +7,11 @@ import { syncToCodex } from "../sync/codex" import { syncToPi } from "../sync/pi" import { syncToDroid } from "../sync/droid" import { syncToCursor } from "../sync/cursor" +import { syncToGemini } from "../sync/gemini" import { expandHome } from "../utils/resolve-home" +import { detectInstalledTools } from "../utils/detect-tools" -const validTargets = ["opencode", "codex", "pi", "droid", "cursor"] as const +const validTargets = ["opencode", "codex", "pi", "droid", "cursor", "gemini", "all"] as const type SyncTarget = (typeof validTargets)[number] function isValidTarget(value: string): value is SyncTarget { @@ -30,7 +32,7 @@ function hasPotentialSecrets(mcpServers: Record): boolean { return false } -function resolveOutputRoot(target: SyncTarget): string { +function resolveOutputRoot(target: string): string { switch (target) { case "opencode": return path.join(os.homedir(), ".config", "opencode") @@ -42,19 +44,46 @@ function resolveOutputRoot(target: SyncTarget): string { return path.join(os.homedir(), ".factory") case "cursor": return path.join(process.cwd(), ".cursor") + case "gemini": + return path.join(process.cwd(), ".gemini") + default: + throw new Error(`No output root for target: ${target}`) + } +} + +async function syncTarget(target: string, config: Awaited>, outputRoot: string): Promise { + switch (target) { + case "opencode": + await syncToOpenCode(config, outputRoot) + break + case "codex": + await syncToCodex(config, outputRoot) + break + case "pi": + await syncToPi(config, outputRoot) + break + case "droid": + await syncToDroid(config, outputRoot) + break + case "cursor": + await syncToCursor(config, outputRoot) + break + case "gemini": + await syncToGemini(config, outputRoot) + break } } export default defineCommand({ meta: { name: "sync", - description: "Sync Claude Code config (~/.claude/) to OpenCode, Codex, Pi, Droid, or Cursor", + description: "Sync Claude Code config (~/.claude/) to OpenCode, Codex, Pi, Droid, Cursor, or Gemini", }, args: { target: { type: "string", required: true, - description: "Target: opencode | codex | pi | droid | cursor", + description: "Target: opencode | codex | pi | droid | cursor | gemini | all", }, claudeHome: { type: "string", @@ -78,30 +107,34 @@ export default defineCommand({ ) } + if (args.target === "all") { + const detected = await detectInstalledTools() + const activeTargets = detected.filter((t) => t.detected).map((t) => t.name) + + if (activeTargets.length === 0) { + console.log("No AI coding tools detected.") + return + } + + console.log(`Syncing to ${activeTargets.length} detected tool(s)...`) + for (const tool of detected) { + console.log(` ${tool.detected ? "✓" : "✗"} ${tool.name} — ${tool.reason}`) + } + + for (const name of activeTargets) { + const outputRoot = resolveOutputRoot(name) + await syncTarget(name, config, outputRoot) + console.log(`✓ Synced to ${name}: ${outputRoot}`) + } + return + } + console.log( `Syncing ${config.skills.length} skills, ${Object.keys(config.mcpServers).length} MCP servers...`, ) const outputRoot = resolveOutputRoot(args.target) - - switch (args.target) { - case "opencode": - await syncToOpenCode(config, outputRoot) - break - case "codex": - await syncToCodex(config, outputRoot) - break - case "pi": - await syncToPi(config, outputRoot) - break - case "droid": - await syncToDroid(config, outputRoot) - break - case "cursor": - await syncToCursor(config, outputRoot) - break - } - + await syncTarget(args.target, config, outputRoot) console.log(`✓ Synced to ${args.target}: ${outputRoot}`) }, })