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:
@@ -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
96
src/targets/openclaw.ts
Normal 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
64
src/targets/qwen.ts
Normal 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"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user