feat: wire --to all into install/convert and --target all/gemini into sync

This commit is contained in:
Kieran Klaassen
2026-02-14 21:10:52 -08:00
parent e4d730d5b4
commit bc655f714e
3 changed files with 150 additions and 41 deletions

View File

@@ -6,6 +6,7 @@ import { targets } from "../targets"
import type { PermissionMode } from "../converters/claude-to-opencode" import type { PermissionMode } from "../converters/claude-to-opencode"
import { ensureCodexAgentsFile } from "../utils/codex-agents" import { ensureCodexAgentsFile } from "../utils/codex-agents"
import { expandHome, resolveTargetHome } from "../utils/resolve-home" import { expandHome, resolveTargetHome } from "../utils/resolve-home"
import { detectInstalledTools } from "../utils/detect-tools"
const permissionModes: PermissionMode[] = ["none", "broad", "from-commands"] const permissionModes: PermissionMode[] = ["none", "broad", "from-commands"]
@@ -23,7 +24,7 @@ export default defineCommand({
to: { to: {
type: "string", type: "string",
default: "opencode", default: "opencode",
description: "Target format (opencode | codex | droid | cursor | pi | gemini)", description: "Target format (opencode | codex | droid | cursor | pi | gemini | all)",
}, },
output: { output: {
type: "string", type: "string",
@@ -62,14 +63,6 @@ export default defineCommand({
}, },
async run({ args }) { async run({ args }) {
const targetName = String(args.to) 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) const permissions = String(args.permissions)
if (!permissionModes.includes(permissions as PermissionMode)) { if (!permissionModes.includes(permissions as PermissionMode)) {
@@ -87,6 +80,51 @@ export default defineCommand({
permissions: permissions as PermissionMode, 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 primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, piHome)
const bundle = target.convert(plugin, options) const bundle = target.convert(plugin, options)
if (!bundle) { if (!bundle) {

View File

@@ -8,6 +8,7 @@ import { pathExists } from "../utils/files"
import type { PermissionMode } from "../converters/claude-to-opencode" import type { PermissionMode } from "../converters/claude-to-opencode"
import { ensureCodexAgentsFile } from "../utils/codex-agents" import { ensureCodexAgentsFile } from "../utils/codex-agents"
import { expandHome, resolveTargetHome } from "../utils/resolve-home" import { expandHome, resolveTargetHome } from "../utils/resolve-home"
import { detectInstalledTools } from "../utils/detect-tools"
const permissionModes: PermissionMode[] = ["none", "broad", "from-commands"] const permissionModes: PermissionMode[] = ["none", "broad", "from-commands"]
@@ -25,7 +26,7 @@ export default defineCommand({
to: { to: {
type: "string", type: "string",
default: "opencode", default: "opencode",
description: "Target format (opencode | codex | droid | cursor | pi | gemini)", description: "Target format (opencode | codex | droid | cursor | pi | gemini | all)",
}, },
output: { output: {
type: "string", type: "string",
@@ -64,13 +65,6 @@ export default defineCommand({
}, },
async run({ args }) { async run({ args }) {
const targetName = String(args.to) 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) const permissions = String(args.permissions)
if (!permissionModes.includes(permissions as PermissionMode)) { if (!permissionModes.includes(permissions as PermissionMode)) {
@@ -84,6 +78,7 @@ export default defineCommand({
const outputRoot = resolveOutputRoot(args.output) const outputRoot = resolveOutputRoot(args.output)
const codexHome = resolveTargetHome(args.codexHome, path.join(os.homedir(), ".codex")) const codexHome = resolveTargetHome(args.codexHome, path.join(os.homedir(), ".codex"))
const piHome = resolveTargetHome(args.piHome, path.join(os.homedir(), ".pi", "agent")) const piHome = resolveTargetHome(args.piHome, path.join(os.homedir(), ".pi", "agent"))
const hasExplicitOutput = Boolean(args.output && String(args.output).trim())
const options = { const options = {
agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent", agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent",
@@ -91,11 +86,54 @@ export default defineCommand({
permissions: permissions as PermissionMode, 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) const bundle = target.convert(plugin, options)
if (!bundle) { if (!bundle) {
throw new Error(`Target ${targetName} did not return a 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) const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, piHome, hasExplicitOutput)
await target.write(primaryOutputRoot, bundle) await target.write(primaryOutputRoot, bundle)
console.log(`Installed ${plugin.manifest.name} to ${primaryOutputRoot}`) console.log(`Installed ${plugin.manifest.name} to ${primaryOutputRoot}`)

View File

@@ -7,9 +7,11 @@ import { syncToCodex } from "../sync/codex"
import { syncToPi } from "../sync/pi" import { syncToPi } from "../sync/pi"
import { syncToDroid } from "../sync/droid" import { syncToDroid } from "../sync/droid"
import { syncToCursor } from "../sync/cursor" import { syncToCursor } from "../sync/cursor"
import { syncToGemini } from "../sync/gemini"
import { expandHome } from "../utils/resolve-home" 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] type SyncTarget = (typeof validTargets)[number]
function isValidTarget(value: string): value is SyncTarget { function isValidTarget(value: string): value is SyncTarget {
@@ -30,7 +32,7 @@ function hasPotentialSecrets(mcpServers: Record<string, unknown>): boolean {
return false return false
} }
function resolveOutputRoot(target: SyncTarget): string { function resolveOutputRoot(target: string): string {
switch (target) { switch (target) {
case "opencode": case "opencode":
return path.join(os.homedir(), ".config", "opencode") return path.join(os.homedir(), ".config", "opencode")
@@ -42,19 +44,46 @@ function resolveOutputRoot(target: SyncTarget): string {
return path.join(os.homedir(), ".factory") return path.join(os.homedir(), ".factory")
case "cursor": case "cursor":
return path.join(process.cwd(), ".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<ReturnType<typeof loadClaudeHome>>, outputRoot: string): Promise<void> {
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({ export default defineCommand({
meta: { meta: {
name: "sync", 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: { args: {
target: { target: {
type: "string", type: "string",
required: true, required: true,
description: "Target: opencode | codex | pi | droid | cursor", description: "Target: opencode | codex | pi | droid | cursor | gemini | all",
}, },
claudeHome: { claudeHome: {
type: "string", 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( console.log(
`Syncing ${config.skills.length} skills, ${Object.keys(config.mcpServers).length} MCP servers...`, `Syncing ${config.skills.length} skills, ${Object.keys(config.mcpServers).length} MCP servers...`,
) )
const outputRoot = resolveOutputRoot(args.target) const outputRoot = resolveOutputRoot(args.target)
await syncTarget(args.target, config, outputRoot)
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
}
console.log(`✓ Synced to ${args.target}: ${outputRoot}`) console.log(`✓ Synced to ${args.target}: ${outputRoot}`)
}, },
}) })