Add droid and cursor sync targets, extract shared path helpers

- Add sync --target droid (skills to ~/.factory/skills/)
- Add sync --target cursor (skills + MCP to .cursor/)
- Extract expandHome/resolveTargetHome to src/utils/resolve-home.ts
- Remove duplicated path helpers from convert.ts and install.ts
- Bump version to 0.6.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kieran Klaassen
2026-02-12 20:37:15 -08:00
parent 84af459c79
commit e41904a569
10 changed files with 322 additions and 95 deletions

View File

@@ -5,9 +5,15 @@ import { loadClaudeHome } from "../parsers/claude-home"
import { syncToOpenCode } from "../sync/opencode"
import { syncToCodex } from "../sync/codex"
import { syncToPi } from "../sync/pi"
import { syncToDroid } from "../sync/droid"
import { syncToCursor } from "../sync/cursor"
import { expandHome } from "../utils/resolve-home"
function isValidTarget(value: string): value is "opencode" | "codex" | "pi" {
return value === "opencode" || value === "codex" || value === "pi"
const validTargets = ["opencode", "codex", "pi", "droid", "cursor"] as const
type SyncTarget = (typeof validTargets)[number]
function isValidTarget(value: string): value is SyncTarget {
return (validTargets as readonly string[]).includes(value)
}
/** Check if any MCP servers have env vars that might contain secrets */
@@ -24,16 +30,31 @@ function hasPotentialSecrets(mcpServers: Record<string, unknown>): boolean {
return false
}
function resolveOutputRoot(target: SyncTarget): string {
switch (target) {
case "opencode":
return path.join(os.homedir(), ".config", "opencode")
case "codex":
return path.join(os.homedir(), ".codex")
case "pi":
return path.join(os.homedir(), ".pi", "agent")
case "droid":
return path.join(os.homedir(), ".factory")
case "cursor":
return path.join(process.cwd(), ".cursor")
}
}
export default defineCommand({
meta: {
name: "sync",
description: "Sync Claude Code config (~/.claude/) to OpenCode, Codex, or Pi",
description: "Sync Claude Code config (~/.claude/) to OpenCode, Codex, Pi, Droid, or Cursor",
},
args: {
target: {
type: "string",
required: true,
description: "Target: opencode | codex | pi",
description: "Target: opencode | codex | pi | droid | cursor",
},
claudeHome: {
type: "string",
@@ -43,7 +64,7 @@ export default defineCommand({
},
async run({ args }) {
if (!isValidTarget(args.target)) {
throw new Error(`Unknown target: ${args.target}. Use 'opencode', 'codex', or 'pi'.`)
throw new Error(`Unknown target: ${args.target}. Use one of: ${validTargets.join(", ")}`)
}
const claudeHome = expandHome(args.claudeHome ?? path.join(os.homedir(), ".claude"))
@@ -61,29 +82,26 @@ export default defineCommand({
`Syncing ${config.skills.length} skills, ${Object.keys(config.mcpServers).length} MCP servers...`,
)
const outputRoot =
args.target === "opencode"
? path.join(os.homedir(), ".config", "opencode")
: args.target === "codex"
? path.join(os.homedir(), ".codex")
: path.join(os.homedir(), ".pi", "agent")
const outputRoot = resolveOutputRoot(args.target)
if (args.target === "opencode") {
await syncToOpenCode(config, outputRoot)
} else if (args.target === "codex") {
await syncToCodex(config, outputRoot)
} else {
await syncToPi(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}`)
},
})
function expandHome(value: string): string {
if (value === "~") return os.homedir()
if (value.startsWith(`~${path.sep}`)) {
return path.join(os.homedir(), value.slice(2))
}
return value
}