chore: Resolve merge conflict with main (openclaw + qwen + windsurf)

- Combine windsurf scope support from this branch with openclaw/qwen targets from main
- Update resolve-output.ts utility to handle openclaw/qwen with openclawHome/qwenHome/pluginName
- Add openclawHome/qwenHome args to install.ts and convert.ts
- Register openclaw and qwen in targets/index.ts alongside windsurf
- Add openclaw/qwen coverage to resolve-output.test.ts (4 new tests → 288 total)
- Update README to document all 10 targets including windsurf and openclaw

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Kieran Klaassen
2026-03-01 14:53:42 -08:00
17 changed files with 1297 additions and 11 deletions

View File

@@ -7,6 +7,8 @@ import type { CopilotBundle } from "../types/copilot"
import type { GeminiBundle } from "../types/gemini"
import type { KiroBundle } from "../types/kiro"
import type { WindsurfBundle } from "../types/windsurf"
import type { OpenClawBundle } from "../types/openclaw"
import type { QwenBundle } from "../types/qwen"
import { convertClaudeToOpenCode, type ClaudeToOpenCodeOptions } from "../converters/claude-to-opencode"
import { convertClaudeToCodex } from "../converters/claude-to-codex"
import { convertClaudeToDroid } from "../converters/claude-to-droid"
@@ -15,6 +17,8 @@ import { convertClaudeToCopilot } from "../converters/claude-to-copilot"
import { convertClaudeToGemini } from "../converters/claude-to-gemini"
import { convertClaudeToKiro } from "../converters/claude-to-kiro"
import { convertClaudeToWindsurf } from "../converters/claude-to-windsurf"
import { convertClaudeToOpenClaw } from "../converters/claude-to-openclaw"
import { convertClaudeToQwen } from "../converters/claude-to-qwen"
import { writeOpenCodeBundle } from "./opencode"
import { writeCodexBundle } from "./codex"
import { writeDroidBundle } from "./droid"
@@ -23,6 +27,8 @@ import { writeCopilotBundle } from "./copilot"
import { writeGeminiBundle } from "./gemini"
import { writeKiroBundle } from "./kiro"
import { writeWindsurfBundle } from "./windsurf"
import { writeOpenClawBundle } from "./openclaw"
import { writeQwenBundle } from "./qwen"
export type TargetScope = "global" | "workspace"
@@ -112,4 +118,16 @@ export const targets: Record<string, TargetHandler> = {
convert: convertClaudeToWindsurf as TargetHandler<WindsurfBundle>["convert"],
write: writeWindsurfBundle as TargetHandler<WindsurfBundle>["write"],
},
openclaw: {
name: "openclaw",
implemented: true,
convert: convertClaudeToOpenClaw as TargetHandler<OpenClawBundle>["convert"],
write: writeOpenClawBundle as TargetHandler<OpenClawBundle>["write"],
},
qwen: {
name: "qwen",
implemented: true,
convert: convertClaudeToQwen as TargetHandler<QwenBundle>["convert"],
write: writeQwenBundle as TargetHandler<QwenBundle>["write"],
},
}

96
src/targets/openclaw.ts Normal file
View File

@@ -0,0 +1,96 @@
import path from "path"
import { promises as fs } from "fs"
import { backupFile, copyDir, ensureDir, pathExists, readJson, walkFiles, writeJson, writeText } from "../utils/files"
import type { OpenClawBundle } from "../types/openclaw"
export async function writeOpenClawBundle(outputRoot: string, bundle: OpenClawBundle): Promise<void> {
const paths = resolveOpenClawPaths(outputRoot)
await ensureDir(paths.root)
// Write openclaw.plugin.json
await writeJson(paths.manifestPath, bundle.manifest)
// Write package.json
await writeJson(paths.packageJsonPath, bundle.packageJson)
// Write index.ts entry point
await writeText(paths.entryPointPath, bundle.entryPoint)
// Write generated skills (agents + commands converted to SKILL.md)
for (const skill of bundle.skills) {
const skillDir = path.join(paths.skillsDir, skill.dir)
await ensureDir(skillDir)
await writeText(path.join(skillDir, "SKILL.md"), skill.content + "\n")
}
// Copy original skill directories (preserving references/, assets/, scripts/)
// and rewrite .claude/ paths to .openclaw/ in markdown files
for (const skill of bundle.skillDirCopies) {
const destDir = path.join(paths.skillsDir, skill.name)
await copyDir(skill.sourceDir, destDir)
await rewritePathsInDir(destDir)
}
// Write openclaw.json config fragment if MCP servers exist
if (bundle.openclawConfig) {
const configPath = path.join(paths.root, "openclaw.json")
const backupPath = await backupFile(configPath)
if (backupPath) {
console.log(`Backed up existing config to ${backupPath}`)
}
const merged = await mergeOpenClawConfig(configPath, bundle.openclawConfig)
await writeJson(configPath, merged)
}
}
function resolveOpenClawPaths(outputRoot: string) {
return {
root: outputRoot,
manifestPath: path.join(outputRoot, "openclaw.plugin.json"),
packageJsonPath: path.join(outputRoot, "package.json"),
entryPointPath: path.join(outputRoot, "index.ts"),
skillsDir: path.join(outputRoot, "skills"),
}
}
async function rewritePathsInDir(dir: string): Promise<void> {
const files = await walkFiles(dir)
for (const file of files) {
if (!file.endsWith(".md")) continue
const content = await fs.readFile(file, "utf8")
const rewritten = content
.replace(/~\/\.claude\//g, "~/.openclaw/")
.replace(/\.claude\//g, ".openclaw/")
.replace(/\.claude-plugin\//g, "openclaw-plugin/")
if (rewritten !== content) {
await fs.writeFile(file, rewritten, "utf8")
}
}
}
async function mergeOpenClawConfig(
configPath: string,
incoming: Record<string, unknown>,
): Promise<Record<string, unknown>> {
if (!(await pathExists(configPath))) return incoming
let existing: Record<string, unknown>
try {
existing = await readJson<Record<string, unknown>>(configPath)
} catch {
console.warn(
`Warning: existing ${configPath} is not valid JSON. Writing plugin config without merging.`,
)
return incoming
}
// Merge MCP servers: existing takes precedence on conflict
const incomingMcp = (incoming.mcpServers ?? {}) as Record<string, unknown>
const existingMcp = (existing.mcpServers ?? {}) as Record<string, unknown>
const mergedMcp = { ...incomingMcp, ...existingMcp }
return {
...existing,
mcpServers: Object.keys(mergedMcp).length > 0 ? mergedMcp : undefined,
}
}

64
src/targets/qwen.ts Normal file
View File

@@ -0,0 +1,64 @@
import path from "path"
import { backupFile, copyDir, ensureDir, writeJson, writeText } from "../utils/files"
import type { QwenBundle, QwenExtensionConfig } from "../types/qwen"
export async function writeQwenBundle(outputRoot: string, bundle: QwenBundle): Promise<void> {
const qwenPaths = resolveQwenPaths(outputRoot)
await ensureDir(qwenPaths.root)
// Write qwen-extension.json config
const configPath = qwenPaths.configPath
const backupPath = await backupFile(configPath)
if (backupPath) {
console.log(`Backed up existing config to ${backupPath}`)
}
await writeJson(configPath, bundle.config)
// Write context file (QWEN.md)
if (bundle.contextFile) {
await writeText(qwenPaths.contextPath, bundle.contextFile + "\n")
}
// Write agents
const agentsDir = qwenPaths.agentsDir
await ensureDir(agentsDir)
for (const agent of bundle.agents) {
const ext = agent.format === "yaml" ? "yaml" : "md"
await writeText(path.join(agentsDir, `${agent.name}.${ext}`), agent.content + "\n")
}
// Write commands
const commandsDir = qwenPaths.commandsDir
await ensureDir(commandsDir)
for (const commandFile of bundle.commandFiles) {
// Support nested commands with colon separator
const parts = commandFile.name.split(":")
if (parts.length > 1) {
const nestedDir = path.join(commandsDir, ...parts.slice(0, -1))
await ensureDir(nestedDir)
await writeText(path.join(nestedDir, `${parts[parts.length - 1]}.md`), commandFile.content + "\n")
} else {
await writeText(path.join(commandsDir, `${commandFile.name}.md`), commandFile.content + "\n")
}
}
// Copy skills
if (bundle.skillDirs.length > 0) {
const skillsRoot = qwenPaths.skillsDir
await ensureDir(skillsRoot)
for (const skill of bundle.skillDirs) {
await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
}
}
}
function resolveQwenPaths(outputRoot: string) {
return {
root: outputRoot,
configPath: path.join(outputRoot, "qwen-extension.json"),
contextPath: path.join(outputRoot, "QWEN.md"),
agentsDir: path.join(outputRoot, "agents"),
commandsDir: path.join(outputRoot, "commands"),
skillsDir: path.join(outputRoot, "skills"),
}
}