181 lines
5.5 KiB
TypeScript
181 lines
5.5 KiB
TypeScript
import { defineCommand } from "citty"
|
|
import os from "os"
|
|
import path from "path"
|
|
import { loadClaudePlugin } from "../parsers/claude"
|
|
import { targets } from "../targets"
|
|
import type { PermissionMode } from "../converters/claude-to-opencode"
|
|
import { ensureCodexAgentsFile } from "../utils/codex-agents"
|
|
|
|
const permissionModes: PermissionMode[] = ["none", "broad", "from-commands"]
|
|
|
|
export default defineCommand({
|
|
meta: {
|
|
name: "convert",
|
|
description: "Convert a Claude Code plugin into another format",
|
|
},
|
|
args: {
|
|
source: {
|
|
type: "positional",
|
|
required: true,
|
|
description: "Path to the Claude plugin directory",
|
|
},
|
|
to: {
|
|
type: "string",
|
|
default: "opencode",
|
|
description: "Target format (opencode | codex | droid | cursor | pi)",
|
|
},
|
|
output: {
|
|
type: "string",
|
|
alias: "o",
|
|
description: "Output directory (project root)",
|
|
},
|
|
codexHome: {
|
|
type: "string",
|
|
alias: "codex-home",
|
|
description: "Write Codex output to this .codex root (ex: ~/.codex)",
|
|
},
|
|
piHome: {
|
|
type: "string",
|
|
alias: "pi-home",
|
|
description: "Write Pi output to this Pi root (ex: ~/.pi/agent or ./.pi)",
|
|
},
|
|
also: {
|
|
type: "string",
|
|
description: "Comma-separated extra targets to generate (ex: codex)",
|
|
},
|
|
permissions: {
|
|
type: "string",
|
|
default: "broad",
|
|
description: "Permission mapping: none | broad | from-commands",
|
|
},
|
|
agentMode: {
|
|
type: "string",
|
|
default: "subagent",
|
|
description: "Default agent mode: primary | subagent",
|
|
},
|
|
inferTemperature: {
|
|
type: "boolean",
|
|
default: true,
|
|
description: "Infer agent temperature from name/description",
|
|
},
|
|
},
|
|
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)) {
|
|
throw new Error(`Unknown permissions mode: ${permissions}`)
|
|
}
|
|
|
|
const plugin = await loadClaudePlugin(String(args.source))
|
|
const outputRoot = resolveOutputRoot(args.output)
|
|
const codexHome = resolveCodexRoot(args.codexHome)
|
|
const piHome = resolvePiRoot(args.piHome)
|
|
|
|
const options = {
|
|
agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent",
|
|
inferTemperature: Boolean(args.inferTemperature),
|
|
permissions: permissions as PermissionMode,
|
|
}
|
|
|
|
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, piHome)
|
|
const bundle = target.convert(plugin, options)
|
|
if (!bundle) {
|
|
throw new Error(`Target ${targetName} did not return a bundle.`)
|
|
}
|
|
|
|
await target.write(primaryOutputRoot, bundle)
|
|
console.log(`Converted ${plugin.manifest.name} to ${targetName} at ${primaryOutputRoot}`)
|
|
|
|
const extraTargets = parseExtraTargets(args.also)
|
|
const allTargets = [targetName, ...extraTargets]
|
|
for (const extra of extraTargets) {
|
|
const handler = targets[extra]
|
|
if (!handler) {
|
|
console.warn(`Skipping unknown target: ${extra}`)
|
|
continue
|
|
}
|
|
if (!handler.implemented) {
|
|
console.warn(`Skipping ${extra}: not implemented yet.`)
|
|
continue
|
|
}
|
|
const extraBundle = handler.convert(plugin, options)
|
|
if (!extraBundle) {
|
|
console.warn(`Skipping ${extra}: no output returned.`)
|
|
continue
|
|
}
|
|
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome, piHome)
|
|
await handler.write(extraRoot, extraBundle)
|
|
console.log(`Converted ${plugin.manifest.name} to ${extra} at ${extraRoot}`)
|
|
}
|
|
|
|
if (allTargets.includes("codex")) {
|
|
await ensureCodexAgentsFile(codexHome)
|
|
}
|
|
},
|
|
})
|
|
|
|
function parseExtraTargets(value: unknown): string[] {
|
|
if (!value) return []
|
|
return String(value)
|
|
.split(",")
|
|
.map((entry) => entry.trim())
|
|
.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())
|
|
return path.resolve(expanded)
|
|
}
|
|
return process.cwd()
|
|
}
|
|
|
|
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string, piHome: string): string {
|
|
if (targetName === "codex") return codexHome
|
|
if (targetName === "pi") return piHome
|
|
if (targetName === "droid") return path.join(os.homedir(), ".factory")
|
|
if (targetName === "cursor") return path.join(outputRoot, ".cursor")
|
|
return outputRoot
|
|
}
|