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,6 +5,7 @@ import { loadClaudePlugin } from "../parsers/claude"
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"
const permissionModes: PermissionMode[] = ["none", "broad", "from-commands"]
@@ -77,8 +78,8 @@ export default defineCommand({
const plugin = await loadClaudePlugin(String(args.source))
const outputRoot = resolveOutputRoot(args.output)
const codexHome = resolveCodexRoot(args.codexHome)
const piHome = resolvePiRoot(args.piHome)
const codexHome = resolveTargetHome(args.codexHome, path.join(os.homedir(), ".codex"))
const piHome = resolveTargetHome(args.piHome, path.join(os.homedir(), ".pi", "agent"))
const options = {
agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent",
@@ -131,38 +132,6 @@ function parseExtraTargets(value: unknown): string[] {
.filter(Boolean)
}
function resolveCodexHome(value: unknown): string | null {
if (!value) return null
const raw = String(value).trim()
if (!raw) return null
const expanded = expandHome(raw)
return path.resolve(expanded)
}
function resolveCodexRoot(value: unknown): string {
return resolveCodexHome(value) ?? path.join(os.homedir(), ".codex")
}
function resolvePiHome(value: unknown): string | null {
if (!value) return null
const raw = String(value).trim()
if (!raw) return null
const expanded = expandHome(raw)
return path.resolve(expanded)
}
function resolvePiRoot(value: unknown): string {
return resolvePiHome(value) ?? path.join(os.homedir(), ".pi", "agent")
}
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
}
function resolveOutputRoot(value: unknown): string {
if (value && String(value).trim()) {
const expanded = expandHome(String(value).trim())

View File

@@ -7,6 +7,7 @@ import { targets } from "../targets"
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"
const permissionModes: PermissionMode[] = ["none", "broad", "from-commands"]
@@ -81,8 +82,8 @@ export default defineCommand({
try {
const plugin = await loadClaudePlugin(resolvedPlugin.path)
const outputRoot = resolveOutputRoot(args.output)
const codexHome = resolveCodexRoot(args.codexHome)
const piHome = resolvePiRoot(args.piHome)
const codexHome = resolveTargetHome(args.codexHome, path.join(os.homedir(), ".codex"))
const piHome = resolveTargetHome(args.piHome, path.join(os.homedir(), ".pi", "agent"))
const options = {
agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent",
@@ -158,38 +159,6 @@ function parseExtraTargets(value: unknown): string[] {
.filter(Boolean)
}
function resolveCodexHome(value: unknown): string | null {
if (!value) return null
const raw = String(value).trim()
if (!raw) return null
const expanded = expandHome(raw)
return path.resolve(expanded)
}
function resolveCodexRoot(value: unknown): string {
return resolveCodexHome(value) ?? path.join(os.homedir(), ".codex")
}
function resolvePiHome(value: unknown): string | null {
if (!value) return null
const raw = String(value).trim()
if (!raw) return null
const expanded = expandHome(raw)
return path.resolve(expanded)
}
function resolvePiRoot(value: unknown): string {
return resolvePiHome(value) ?? path.join(os.homedir(), ".pi", "agent")
}
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
}
function resolveOutputRoot(value: unknown): string {
if (value && String(value).trim()) {
const expanded = expandHome(String(value).trim())

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
}