feat(sync): add Claude home sync parity across providers
This commit is contained in:
198
src/sync/commands.ts
Normal file
198
src/sync/commands.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import path from "path"
|
||||
import type { ClaudeHomeConfig } from "../parsers/claude-home"
|
||||
import type { ClaudePlugin } from "../types/claude"
|
||||
import { backupFile, writeText } from "../utils/files"
|
||||
import { convertClaudeToCodex } from "../converters/claude-to-codex"
|
||||
import { convertClaudeToCopilot } from "../converters/claude-to-copilot"
|
||||
import { convertClaudeToDroid } from "../converters/claude-to-droid"
|
||||
import { convertClaudeToGemini } from "../converters/claude-to-gemini"
|
||||
import { convertClaudeToKiro } from "../converters/claude-to-kiro"
|
||||
import { convertClaudeToOpenCode, type ClaudeToOpenCodeOptions } from "../converters/claude-to-opencode"
|
||||
import { convertClaudeToPi } from "../converters/claude-to-pi"
|
||||
import { convertClaudeToQwen, type ClaudeToQwenOptions } from "../converters/claude-to-qwen"
|
||||
import { convertClaudeToWindsurf } from "../converters/claude-to-windsurf"
|
||||
import { writeWindsurfBundle } from "../targets/windsurf"
|
||||
|
||||
type WindsurfSyncScope = "global" | "workspace"
|
||||
|
||||
const HOME_SYNC_PLUGIN_ROOT = path.join(process.cwd(), ".compound-sync-home")
|
||||
|
||||
const DEFAULT_SYNC_OPTIONS: ClaudeToOpenCodeOptions = {
|
||||
agentMode: "subagent",
|
||||
inferTemperature: false,
|
||||
permissions: "none",
|
||||
}
|
||||
|
||||
const DEFAULT_QWEN_SYNC_OPTIONS: ClaudeToQwenOptions = {
|
||||
agentMode: "subagent",
|
||||
inferTemperature: false,
|
||||
}
|
||||
|
||||
function hasCommands(config: ClaudeHomeConfig): boolean {
|
||||
return (config.commands?.length ?? 0) > 0
|
||||
}
|
||||
|
||||
function buildClaudeHomePlugin(config: ClaudeHomeConfig): ClaudePlugin {
|
||||
return {
|
||||
root: HOME_SYNC_PLUGIN_ROOT,
|
||||
manifest: {
|
||||
name: "claude-home",
|
||||
version: "1.0.0",
|
||||
description: "Personal Claude Code home config",
|
||||
},
|
||||
agents: [],
|
||||
commands: config.commands ?? [],
|
||||
skills: config.skills,
|
||||
mcpServers: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncOpenCodeCommands(
|
||||
config: ClaudeHomeConfig,
|
||||
outputRoot: string,
|
||||
): Promise<void> {
|
||||
if (!hasCommands(config)) return
|
||||
|
||||
const plugin = buildClaudeHomePlugin(config)
|
||||
const bundle = convertClaudeToOpenCode(plugin, DEFAULT_SYNC_OPTIONS)
|
||||
|
||||
for (const commandFile of bundle.commandFiles) {
|
||||
const commandPath = path.join(outputRoot, "commands", `${commandFile.name}.md`)
|
||||
const backupPath = await backupFile(commandPath)
|
||||
if (backupPath) {
|
||||
console.log(`Backed up existing command file to ${backupPath}`)
|
||||
}
|
||||
await writeText(commandPath, commandFile.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncCodexCommands(
|
||||
config: ClaudeHomeConfig,
|
||||
outputRoot: string,
|
||||
): Promise<void> {
|
||||
if (!hasCommands(config)) return
|
||||
|
||||
const plugin = buildClaudeHomePlugin(config)
|
||||
const bundle = convertClaudeToCodex(plugin, DEFAULT_SYNC_OPTIONS)
|
||||
for (const prompt of bundle.prompts) {
|
||||
await writeText(path.join(outputRoot, "prompts", `${prompt.name}.md`), prompt.content + "\n")
|
||||
}
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
await writeText(path.join(outputRoot, "skills", skill.name, "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncPiCommands(
|
||||
config: ClaudeHomeConfig,
|
||||
outputRoot: string,
|
||||
): Promise<void> {
|
||||
if (!hasCommands(config)) return
|
||||
|
||||
const plugin = buildClaudeHomePlugin(config)
|
||||
const bundle = convertClaudeToPi(plugin, DEFAULT_SYNC_OPTIONS)
|
||||
for (const prompt of bundle.prompts) {
|
||||
await writeText(path.join(outputRoot, "prompts", `${prompt.name}.md`), prompt.content + "\n")
|
||||
}
|
||||
for (const extension of bundle.extensions) {
|
||||
await writeText(path.join(outputRoot, "extensions", extension.name), extension.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncDroidCommands(
|
||||
config: ClaudeHomeConfig,
|
||||
outputRoot: string,
|
||||
): Promise<void> {
|
||||
if (!hasCommands(config)) return
|
||||
|
||||
const plugin = buildClaudeHomePlugin(config)
|
||||
const bundle = convertClaudeToDroid(plugin, DEFAULT_SYNC_OPTIONS)
|
||||
for (const command of bundle.commands) {
|
||||
await writeText(path.join(outputRoot, "commands", `${command.name}.md`), command.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncCopilotCommands(
|
||||
config: ClaudeHomeConfig,
|
||||
outputRoot: string,
|
||||
): Promise<void> {
|
||||
if (!hasCommands(config)) return
|
||||
|
||||
const plugin = buildClaudeHomePlugin(config)
|
||||
const bundle = convertClaudeToCopilot(plugin, DEFAULT_SYNC_OPTIONS)
|
||||
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
await writeText(path.join(outputRoot, "skills", skill.name, "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncGeminiCommands(
|
||||
config: ClaudeHomeConfig,
|
||||
outputRoot: string,
|
||||
): Promise<void> {
|
||||
if (!hasCommands(config)) return
|
||||
|
||||
const plugin = buildClaudeHomePlugin(config)
|
||||
const bundle = convertClaudeToGemini(plugin, DEFAULT_SYNC_OPTIONS)
|
||||
for (const command of bundle.commands) {
|
||||
await writeText(path.join(outputRoot, "commands", `${command.name}.toml`), command.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncKiroCommands(
|
||||
config: ClaudeHomeConfig,
|
||||
outputRoot: string,
|
||||
): Promise<void> {
|
||||
if (!hasCommands(config)) return
|
||||
|
||||
const plugin = buildClaudeHomePlugin(config)
|
||||
const bundle = convertClaudeToKiro(plugin, DEFAULT_SYNC_OPTIONS)
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
await writeText(path.join(outputRoot, "skills", skill.name, "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncWindsurfCommands(
|
||||
config: ClaudeHomeConfig,
|
||||
outputRoot: string,
|
||||
scope: WindsurfSyncScope = "global",
|
||||
): Promise<void> {
|
||||
if (!hasCommands(config)) return
|
||||
|
||||
const plugin = buildClaudeHomePlugin(config)
|
||||
const bundle = convertClaudeToWindsurf(plugin, DEFAULT_SYNC_OPTIONS)
|
||||
await writeWindsurfBundle(outputRoot, {
|
||||
agentSkills: [],
|
||||
commandWorkflows: bundle.commandWorkflows,
|
||||
skillDirs: [],
|
||||
mcpConfig: null,
|
||||
}, scope)
|
||||
}
|
||||
|
||||
export async function syncQwenCommands(
|
||||
config: ClaudeHomeConfig,
|
||||
outputRoot: string,
|
||||
): Promise<void> {
|
||||
if (!hasCommands(config)) return
|
||||
|
||||
const plugin = buildClaudeHomePlugin(config)
|
||||
const bundle = convertClaudeToQwen(plugin, DEFAULT_QWEN_SYNC_OPTIONS)
|
||||
|
||||
for (const commandFile of bundle.commandFiles) {
|
||||
const parts = commandFile.name.split(":")
|
||||
if (parts.length > 1) {
|
||||
const nestedDir = path.join(outputRoot, "commands", ...parts.slice(0, -1))
|
||||
await writeText(path.join(nestedDir, `${parts[parts.length - 1]}.md`), commandFile.content + "\n")
|
||||
continue
|
||||
}
|
||||
|
||||
await writeText(path.join(outputRoot, "commands", `${commandFile.name}.md`), commandFile.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
export function warnUnsupportedOpenClawCommands(config: ClaudeHomeConfig): void {
|
||||
if (!hasCommands(config)) return
|
||||
|
||||
console.warn(
|
||||
"Warning: OpenClaw personal command sync is skipped because this sync target currently has no documented user-level command surface.",
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user