@every-env/compound-plugin 0.9.0 → 2.34.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/.github/workflows/publish.yml +20 -10
- package/.releaserc.json +31 -0
- package/AGENTS.md +6 -1
- package/CHANGELOG.md +76 -0
- package/CLAUDE.md +16 -3
- package/README.md +83 -16
- package/bun.lock +977 -0
- package/docs/plans/2026-02-14-feat-auto-detect-install-and-gemini-sync-plan.md +360 -0
- package/docs/plans/2026-02-25-feat-windsurf-global-scope-support-plan.md +627 -0
- package/docs/plans/2026-03-01-feat-ce-command-aliases-backwards-compatible-deprecation-plan.md +261 -0
- package/docs/plans/2026-03-01-fix-setup-skill-non-claude-llm-fallback-plan.md +140 -0
- package/docs/plans/2026-03-03-feat-sync-claude-mcp-all-supported-providers-plan.md +639 -0
- package/docs/plans/feature_opencode-commands-as-md-and-config-merge.md +574 -0
- package/docs/solutions/adding-converter-target-providers.md +693 -0
- package/docs/solutions/plugin-versioning-requirements.md +7 -3
- package/docs/specs/windsurf.md +477 -0
- package/package.json +10 -4
- package/plans/landing-page-launchkit-refresh.md +2 -2
- package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
- package/plugins/compound-engineering/CHANGELOG.md +82 -1
- package/plugins/compound-engineering/CLAUDE.md +14 -7
- package/plugins/compound-engineering/README.md +10 -7
- package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
- package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
- package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
- package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
- package/plugins/compound-engineering/commands/ce/compound.md +240 -0
- package/plugins/compound-engineering/commands/ce/plan.md +636 -0
- package/plugins/compound-engineering/commands/ce/review.md +525 -0
- package/plugins/compound-engineering/commands/ce/work.md +470 -0
- package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
- package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
- package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
- package/plugins/compound-engineering/commands/feature-video.md +15 -6
- package/plugins/compound-engineering/commands/heal-skill.md +1 -1
- package/plugins/compound-engineering/commands/lfg.md +3 -3
- package/plugins/compound-engineering/commands/slfg.md +3 -3
- package/plugins/compound-engineering/commands/test-xcode.md +2 -2
- package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
- package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
- package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
- package/plugins/compound-engineering/commands/workflows/review.md +4 -522
- package/plugins/compound-engineering/commands/workflows/work.md +4 -448
- package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/add-workflow.md +6 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/create-new-skill.md +6 -0
- package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
- package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
- package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/setup/SKILL.md +8 -2
- package/src/commands/convert.ts +101 -24
- package/src/commands/install.ts +102 -45
- package/src/commands/sync.ts +43 -62
- package/src/converters/claude-to-openclaw.ts +240 -0
- package/src/converters/claude-to-opencode.ts +12 -10
- package/src/converters/claude-to-qwen.ts +238 -0
- package/src/converters/claude-to-windsurf.ts +205 -0
- package/src/index.ts +2 -1
- package/src/parsers/claude-home.ts +55 -3
- package/src/sync/codex.ts +38 -62
- package/src/sync/commands.ts +198 -0
- package/src/sync/copilot.ts +14 -36
- package/src/sync/droid.ts +50 -9
- package/src/sync/gemini.ts +135 -0
- package/src/sync/json-config.ts +47 -0
- package/src/sync/kiro.ts +49 -0
- package/src/sync/mcp-transports.ts +19 -0
- package/src/sync/openclaw.ts +18 -0
- package/src/sync/opencode.ts +10 -30
- package/src/sync/pi.ts +12 -36
- package/src/sync/qwen.ts +66 -0
- package/src/sync/registry.ts +141 -0
- package/src/sync/skills.ts +21 -0
- package/src/sync/windsurf.ts +59 -0
- package/src/targets/index.ts +60 -1
- package/src/targets/openclaw.ts +96 -0
- package/src/targets/opencode.ts +76 -10
- package/src/targets/qwen.ts +64 -0
- package/src/targets/windsurf.ts +104 -0
- package/src/types/kiro.ts +3 -1
- package/src/types/openclaw.ts +52 -0
- package/src/types/opencode.ts +7 -8
- package/src/types/qwen.ts +51 -0
- package/src/types/windsurf.ts +35 -0
- package/src/utils/codex-agents.ts +1 -1
- package/src/utils/detect-tools.ts +37 -0
- package/src/utils/files.ts +14 -0
- package/src/utils/resolve-output.ts +50 -0
- package/src/utils/secrets.ts +24 -0
- package/src/utils/symlink.ts +4 -6
- package/tests/claude-home.test.ts +46 -0
- package/tests/cli.test.ts +180 -0
- package/tests/converter.test.ts +43 -10
- package/tests/detect-tools.test.ts +119 -0
- package/tests/openclaw-converter.test.ts +200 -0
- package/tests/opencode-writer.test.ts +142 -5
- package/tests/qwen-converter.test.ts +238 -0
- package/tests/resolve-output.test.ts +131 -0
- package/tests/sync-codex.test.ts +64 -0
- package/tests/sync-copilot.test.ts +60 -4
- package/tests/sync-droid.test.ts +44 -4
- package/tests/sync-gemini.test.ts +160 -0
- package/tests/sync-kiro.test.ts +83 -0
- package/tests/sync-openclaw.test.ts +51 -0
- package/tests/sync-qwen.test.ts +75 -0
- package/tests/sync-windsurf.test.ts +89 -0
- package/tests/windsurf-converter.test.ts +573 -0
- package/tests/windsurf-writer.test.ts +359 -0
- package/docs/css/docs.css +0 -675
- package/docs/css/style.css +0 -2886
- package/docs/index.html +0 -1046
- package/docs/js/main.js +0 -225
- package/docs/pages/agents.html +0 -649
- package/docs/pages/changelog.html +0 -534
- package/docs/pages/commands.html +0 -523
- package/docs/pages/getting-started.html +0 -582
- package/docs/pages/mcp-servers.html +0 -409
- package/docs/pages/skills.html +0 -611
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import os from "os"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import type { ClaudeHomeConfig } from "../parsers/claude-home"
|
|
4
|
+
import { syncToCodex } from "./codex"
|
|
5
|
+
import { syncToCopilot } from "./copilot"
|
|
6
|
+
import { syncToDroid } from "./droid"
|
|
7
|
+
import { syncToGemini } from "./gemini"
|
|
8
|
+
import { syncToKiro } from "./kiro"
|
|
9
|
+
import { syncToOpenClaw } from "./openclaw"
|
|
10
|
+
import { syncToOpenCode } from "./opencode"
|
|
11
|
+
import { syncToPi } from "./pi"
|
|
12
|
+
import { syncToQwen } from "./qwen"
|
|
13
|
+
import { syncToWindsurf } from "./windsurf"
|
|
14
|
+
|
|
15
|
+
function getCopilotHomeRoot(home: string): string {
|
|
16
|
+
return path.join(home, ".copilot")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getGeminiHomeRoot(home: string): string {
|
|
20
|
+
return path.join(home, ".gemini")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SyncTargetName =
|
|
24
|
+
| "opencode"
|
|
25
|
+
| "codex"
|
|
26
|
+
| "pi"
|
|
27
|
+
| "droid"
|
|
28
|
+
| "copilot"
|
|
29
|
+
| "gemini"
|
|
30
|
+
| "windsurf"
|
|
31
|
+
| "kiro"
|
|
32
|
+
| "qwen"
|
|
33
|
+
| "openclaw"
|
|
34
|
+
|
|
35
|
+
export type SyncTargetDefinition = {
|
|
36
|
+
name: SyncTargetName
|
|
37
|
+
detectPaths: (home: string, cwd: string) => string[]
|
|
38
|
+
resolveOutputRoot: (home: string, cwd: string) => string
|
|
39
|
+
sync: (config: ClaudeHomeConfig, outputRoot: string) => Promise<void>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const syncTargets: SyncTargetDefinition[] = [
|
|
43
|
+
{
|
|
44
|
+
name: "opencode",
|
|
45
|
+
detectPaths: (home, cwd) => [
|
|
46
|
+
path.join(home, ".config", "opencode"),
|
|
47
|
+
path.join(cwd, ".opencode"),
|
|
48
|
+
],
|
|
49
|
+
resolveOutputRoot: (home) => path.join(home, ".config", "opencode"),
|
|
50
|
+
sync: syncToOpenCode,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "codex",
|
|
54
|
+
detectPaths: (home) => [path.join(home, ".codex")],
|
|
55
|
+
resolveOutputRoot: (home) => path.join(home, ".codex"),
|
|
56
|
+
sync: syncToCodex,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "pi",
|
|
60
|
+
detectPaths: (home) => [path.join(home, ".pi")],
|
|
61
|
+
resolveOutputRoot: (home) => path.join(home, ".pi", "agent"),
|
|
62
|
+
sync: syncToPi,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "droid",
|
|
66
|
+
detectPaths: (home) => [path.join(home, ".factory")],
|
|
67
|
+
resolveOutputRoot: (home) => path.join(home, ".factory"),
|
|
68
|
+
sync: syncToDroid,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "copilot",
|
|
72
|
+
detectPaths: (home, cwd) => [
|
|
73
|
+
getCopilotHomeRoot(home),
|
|
74
|
+
path.join(cwd, ".github", "skills"),
|
|
75
|
+
path.join(cwd, ".github", "agents"),
|
|
76
|
+
path.join(cwd, ".github", "copilot-instructions.md"),
|
|
77
|
+
],
|
|
78
|
+
resolveOutputRoot: (home) => getCopilotHomeRoot(home),
|
|
79
|
+
sync: syncToCopilot,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "gemini",
|
|
83
|
+
detectPaths: (home, cwd) => [
|
|
84
|
+
path.join(cwd, ".gemini"),
|
|
85
|
+
getGeminiHomeRoot(home),
|
|
86
|
+
],
|
|
87
|
+
resolveOutputRoot: (home) => getGeminiHomeRoot(home),
|
|
88
|
+
sync: syncToGemini,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "windsurf",
|
|
92
|
+
detectPaths: (home, cwd) => [
|
|
93
|
+
path.join(home, ".codeium", "windsurf"),
|
|
94
|
+
path.join(cwd, ".windsurf"),
|
|
95
|
+
],
|
|
96
|
+
resolveOutputRoot: (home) => path.join(home, ".codeium", "windsurf"),
|
|
97
|
+
sync: syncToWindsurf,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "kiro",
|
|
101
|
+
detectPaths: (home, cwd) => [
|
|
102
|
+
path.join(home, ".kiro"),
|
|
103
|
+
path.join(cwd, ".kiro"),
|
|
104
|
+
],
|
|
105
|
+
resolveOutputRoot: (home) => path.join(home, ".kiro"),
|
|
106
|
+
sync: syncToKiro,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "qwen",
|
|
110
|
+
detectPaths: (home, cwd) => [
|
|
111
|
+
path.join(home, ".qwen"),
|
|
112
|
+
path.join(cwd, ".qwen"),
|
|
113
|
+
],
|
|
114
|
+
resolveOutputRoot: (home) => path.join(home, ".qwen"),
|
|
115
|
+
sync: syncToQwen,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "openclaw",
|
|
119
|
+
detectPaths: (home) => [path.join(home, ".openclaw")],
|
|
120
|
+
resolveOutputRoot: (home) => path.join(home, ".openclaw"),
|
|
121
|
+
sync: syncToOpenClaw,
|
|
122
|
+
},
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
export const syncTargetNames = syncTargets.map((target) => target.name)
|
|
126
|
+
|
|
127
|
+
export function isSyncTargetName(value: string): value is SyncTargetName {
|
|
128
|
+
return syncTargetNames.includes(value as SyncTargetName)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getSyncTarget(name: SyncTargetName): SyncTargetDefinition {
|
|
132
|
+
const target = syncTargets.find((entry) => entry.name === name)
|
|
133
|
+
if (!target) {
|
|
134
|
+
throw new Error(`Unknown sync target: ${name}`)
|
|
135
|
+
}
|
|
136
|
+
return target
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getDefaultSyncRegistryContext(): { home: string; cwd: string } {
|
|
140
|
+
return { home: os.homedir(), cwd: process.cwd() }
|
|
141
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import type { ClaudeSkill } from "../types/claude"
|
|
3
|
+
import { ensureDir } from "../utils/files"
|
|
4
|
+
import { forceSymlink, isValidSkillName } from "../utils/symlink"
|
|
5
|
+
|
|
6
|
+
export async function syncSkills(
|
|
7
|
+
skills: ClaudeSkill[],
|
|
8
|
+
skillsDir: string,
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
await ensureDir(skillsDir)
|
|
11
|
+
|
|
12
|
+
for (const skill of skills) {
|
|
13
|
+
if (!isValidSkillName(skill.name)) {
|
|
14
|
+
console.warn(`Skipping skill with invalid name: ${skill.name}`)
|
|
15
|
+
continue
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const target = path.join(skillsDir, skill.name)
|
|
19
|
+
await forceSymlink(skill.sourceDir, target)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import type { ClaudeHomeConfig } from "../parsers/claude-home"
|
|
3
|
+
import type { ClaudeMcpServer } from "../types/claude"
|
|
4
|
+
import type { WindsurfMcpServerEntry } from "../types/windsurf"
|
|
5
|
+
import { syncWindsurfCommands } from "./commands"
|
|
6
|
+
import { mergeJsonConfigAtKey } from "./json-config"
|
|
7
|
+
import { hasExplicitSseTransport } from "./mcp-transports"
|
|
8
|
+
import { syncSkills } from "./skills"
|
|
9
|
+
|
|
10
|
+
export async function syncToWindsurf(
|
|
11
|
+
config: ClaudeHomeConfig,
|
|
12
|
+
outputRoot: string,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
await syncSkills(config.skills, path.join(outputRoot, "skills"))
|
|
15
|
+
await syncWindsurfCommands(config, outputRoot, "global")
|
|
16
|
+
|
|
17
|
+
if (Object.keys(config.mcpServers).length > 0) {
|
|
18
|
+
await mergeJsonConfigAtKey({
|
|
19
|
+
configPath: path.join(outputRoot, "mcp_config.json"),
|
|
20
|
+
key: "mcpServers",
|
|
21
|
+
incoming: convertMcpForWindsurf(config.mcpServers),
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function convertMcpForWindsurf(
|
|
27
|
+
servers: Record<string, ClaudeMcpServer>,
|
|
28
|
+
): Record<string, WindsurfMcpServerEntry> {
|
|
29
|
+
const result: Record<string, WindsurfMcpServerEntry> = {}
|
|
30
|
+
|
|
31
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
32
|
+
if (server.command) {
|
|
33
|
+
result[name] = {
|
|
34
|
+
command: server.command,
|
|
35
|
+
args: server.args,
|
|
36
|
+
env: server.env,
|
|
37
|
+
}
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!server.url) {
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const entry: WindsurfMcpServerEntry = {
|
|
46
|
+
headers: server.headers,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (hasExplicitSseTransport(server)) {
|
|
50
|
+
entry.url = server.url
|
|
51
|
+
} else {
|
|
52
|
+
entry.serverUrl = server.url
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
result[name] = entry
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result
|
|
59
|
+
}
|
package/src/targets/index.ts
CHANGED
|
@@ -6,6 +6,9 @@ import type { PiBundle } from "../types/pi"
|
|
|
6
6
|
import type { CopilotBundle } from "../types/copilot"
|
|
7
7
|
import type { GeminiBundle } from "../types/gemini"
|
|
8
8
|
import type { KiroBundle } from "../types/kiro"
|
|
9
|
+
import type { WindsurfBundle } from "../types/windsurf"
|
|
10
|
+
import type { OpenClawBundle } from "../types/openclaw"
|
|
11
|
+
import type { QwenBundle } from "../types/qwen"
|
|
9
12
|
import { convertClaudeToOpenCode, type ClaudeToOpenCodeOptions } from "../converters/claude-to-opencode"
|
|
10
13
|
import { convertClaudeToCodex } from "../converters/claude-to-codex"
|
|
11
14
|
import { convertClaudeToDroid } from "../converters/claude-to-droid"
|
|
@@ -13,6 +16,9 @@ import { convertClaudeToPi } from "../converters/claude-to-pi"
|
|
|
13
16
|
import { convertClaudeToCopilot } from "../converters/claude-to-copilot"
|
|
14
17
|
import { convertClaudeToGemini } from "../converters/claude-to-gemini"
|
|
15
18
|
import { convertClaudeToKiro } from "../converters/claude-to-kiro"
|
|
19
|
+
import { convertClaudeToWindsurf } from "../converters/claude-to-windsurf"
|
|
20
|
+
import { convertClaudeToOpenClaw } from "../converters/claude-to-openclaw"
|
|
21
|
+
import { convertClaudeToQwen } from "../converters/claude-to-qwen"
|
|
16
22
|
import { writeOpenCodeBundle } from "./opencode"
|
|
17
23
|
import { writeCodexBundle } from "./codex"
|
|
18
24
|
import { writeDroidBundle } from "./droid"
|
|
@@ -20,12 +26,45 @@ import { writePiBundle } from "./pi"
|
|
|
20
26
|
import { writeCopilotBundle } from "./copilot"
|
|
21
27
|
import { writeGeminiBundle } from "./gemini"
|
|
22
28
|
import { writeKiroBundle } from "./kiro"
|
|
29
|
+
import { writeWindsurfBundle } from "./windsurf"
|
|
30
|
+
import { writeOpenClawBundle } from "./openclaw"
|
|
31
|
+
import { writeQwenBundle } from "./qwen"
|
|
32
|
+
|
|
33
|
+
export type TargetScope = "global" | "workspace"
|
|
34
|
+
|
|
35
|
+
export function isTargetScope(value: string): value is TargetScope {
|
|
36
|
+
return value === "global" || value === "workspace"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate a --scope flag against a target's supported scopes.
|
|
41
|
+
* Returns the resolved scope (explicit or default) or throws on invalid input.
|
|
42
|
+
*/
|
|
43
|
+
export function validateScope(
|
|
44
|
+
targetName: string,
|
|
45
|
+
target: TargetHandler,
|
|
46
|
+
scopeArg: string | undefined,
|
|
47
|
+
): TargetScope | undefined {
|
|
48
|
+
if (scopeArg === undefined) return target.defaultScope
|
|
49
|
+
|
|
50
|
+
if (!target.supportedScopes) {
|
|
51
|
+
throw new Error(`Target "${targetName}" does not support the --scope flag.`)
|
|
52
|
+
}
|
|
53
|
+
if (!isTargetScope(scopeArg) || !target.supportedScopes.includes(scopeArg)) {
|
|
54
|
+
throw new Error(`Target "${targetName}" does not support --scope ${scopeArg}. Supported: ${target.supportedScopes.join(", ")}`)
|
|
55
|
+
}
|
|
56
|
+
return scopeArg
|
|
57
|
+
}
|
|
23
58
|
|
|
24
59
|
export type TargetHandler<TBundle = unknown> = {
|
|
25
60
|
name: string
|
|
26
61
|
implemented: boolean
|
|
62
|
+
/** Default scope when --scope is not provided. Only meaningful when supportedScopes is defined. */
|
|
63
|
+
defaultScope?: TargetScope
|
|
64
|
+
/** Valid scope values. If absent, the --scope flag is rejected for this target. */
|
|
65
|
+
supportedScopes?: TargetScope[]
|
|
27
66
|
convert: (plugin: ClaudePlugin, options: ClaudeToOpenCodeOptions) => TBundle | null
|
|
28
|
-
write: (outputRoot: string, bundle: TBundle) => Promise<void>
|
|
67
|
+
write: (outputRoot: string, bundle: TBundle, scope?: TargetScope) => Promise<void>
|
|
29
68
|
}
|
|
30
69
|
|
|
31
70
|
export const targets: Record<string, TargetHandler> = {
|
|
@@ -71,4 +110,24 @@ export const targets: Record<string, TargetHandler> = {
|
|
|
71
110
|
convert: convertClaudeToKiro as TargetHandler<KiroBundle>["convert"],
|
|
72
111
|
write: writeKiroBundle as TargetHandler<KiroBundle>["write"],
|
|
73
112
|
},
|
|
113
|
+
windsurf: {
|
|
114
|
+
name: "windsurf",
|
|
115
|
+
implemented: true,
|
|
116
|
+
defaultScope: "global",
|
|
117
|
+
supportedScopes: ["global", "workspace"],
|
|
118
|
+
convert: convertClaudeToWindsurf as TargetHandler<WindsurfBundle>["convert"],
|
|
119
|
+
write: writeWindsurfBundle as TargetHandler<WindsurfBundle>["write"],
|
|
120
|
+
},
|
|
121
|
+
openclaw: {
|
|
122
|
+
name: "openclaw",
|
|
123
|
+
implemented: true,
|
|
124
|
+
convert: convertClaudeToOpenClaw as TargetHandler<OpenClawBundle>["convert"],
|
|
125
|
+
write: writeOpenClawBundle as TargetHandler<OpenClawBundle>["write"],
|
|
126
|
+
},
|
|
127
|
+
qwen: {
|
|
128
|
+
name: "qwen",
|
|
129
|
+
implemented: true,
|
|
130
|
+
convert: convertClaudeToQwen as TargetHandler<QwenBundle>["convert"],
|
|
131
|
+
write: writeQwenBundle as TargetHandler<QwenBundle>["write"],
|
|
132
|
+
},
|
|
74
133
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { promises as fs } from "fs"
|
|
3
|
+
import { backupFile, copyDir, ensureDir, pathExists, readJson, walkFiles, writeJson, writeText } from "../utils/files"
|
|
4
|
+
import type { OpenClawBundle } from "../types/openclaw"
|
|
5
|
+
|
|
6
|
+
export async function writeOpenClawBundle(outputRoot: string, bundle: OpenClawBundle): Promise<void> {
|
|
7
|
+
const paths = resolveOpenClawPaths(outputRoot)
|
|
8
|
+
await ensureDir(paths.root)
|
|
9
|
+
|
|
10
|
+
// Write openclaw.plugin.json
|
|
11
|
+
await writeJson(paths.manifestPath, bundle.manifest)
|
|
12
|
+
|
|
13
|
+
// Write package.json
|
|
14
|
+
await writeJson(paths.packageJsonPath, bundle.packageJson)
|
|
15
|
+
|
|
16
|
+
// Write index.ts entry point
|
|
17
|
+
await writeText(paths.entryPointPath, bundle.entryPoint)
|
|
18
|
+
|
|
19
|
+
// Write generated skills (agents + commands converted to SKILL.md)
|
|
20
|
+
for (const skill of bundle.skills) {
|
|
21
|
+
const skillDir = path.join(paths.skillsDir, skill.dir)
|
|
22
|
+
await ensureDir(skillDir)
|
|
23
|
+
await writeText(path.join(skillDir, "SKILL.md"), skill.content + "\n")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Copy original skill directories (preserving references/, assets/, scripts/)
|
|
27
|
+
// and rewrite .claude/ paths to .openclaw/ in markdown files
|
|
28
|
+
for (const skill of bundle.skillDirCopies) {
|
|
29
|
+
const destDir = path.join(paths.skillsDir, skill.name)
|
|
30
|
+
await copyDir(skill.sourceDir, destDir)
|
|
31
|
+
await rewritePathsInDir(destDir)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Write openclaw.json config fragment if MCP servers exist
|
|
35
|
+
if (bundle.openclawConfig) {
|
|
36
|
+
const configPath = path.join(paths.root, "openclaw.json")
|
|
37
|
+
const backupPath = await backupFile(configPath)
|
|
38
|
+
if (backupPath) {
|
|
39
|
+
console.log(`Backed up existing config to ${backupPath}`)
|
|
40
|
+
}
|
|
41
|
+
const merged = await mergeOpenClawConfig(configPath, bundle.openclawConfig)
|
|
42
|
+
await writeJson(configPath, merged)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveOpenClawPaths(outputRoot: string) {
|
|
47
|
+
return {
|
|
48
|
+
root: outputRoot,
|
|
49
|
+
manifestPath: path.join(outputRoot, "openclaw.plugin.json"),
|
|
50
|
+
packageJsonPath: path.join(outputRoot, "package.json"),
|
|
51
|
+
entryPointPath: path.join(outputRoot, "index.ts"),
|
|
52
|
+
skillsDir: path.join(outputRoot, "skills"),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function rewritePathsInDir(dir: string): Promise<void> {
|
|
57
|
+
const files = await walkFiles(dir)
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
if (!file.endsWith(".md")) continue
|
|
60
|
+
const content = await fs.readFile(file, "utf8")
|
|
61
|
+
const rewritten = content
|
|
62
|
+
.replace(/~\/\.claude\//g, "~/.openclaw/")
|
|
63
|
+
.replace(/\.claude\//g, ".openclaw/")
|
|
64
|
+
.replace(/\.claude-plugin\//g, "openclaw-plugin/")
|
|
65
|
+
if (rewritten !== content) {
|
|
66
|
+
await fs.writeFile(file, rewritten, "utf8")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function mergeOpenClawConfig(
|
|
72
|
+
configPath: string,
|
|
73
|
+
incoming: Record<string, unknown>,
|
|
74
|
+
): Promise<Record<string, unknown>> {
|
|
75
|
+
if (!(await pathExists(configPath))) return incoming
|
|
76
|
+
|
|
77
|
+
let existing: Record<string, unknown>
|
|
78
|
+
try {
|
|
79
|
+
existing = await readJson<Record<string, unknown>>(configPath)
|
|
80
|
+
} catch {
|
|
81
|
+
console.warn(
|
|
82
|
+
`Warning: existing ${configPath} is not valid JSON. Writing plugin config without merging.`,
|
|
83
|
+
)
|
|
84
|
+
return incoming
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Merge MCP servers: existing takes precedence on conflict
|
|
88
|
+
const incomingMcp = (incoming.mcpServers ?? {}) as Record<string, unknown>
|
|
89
|
+
const existingMcp = (existing.mcpServers ?? {}) as Record<string, unknown>
|
|
90
|
+
const mergedMcp = { ...incomingMcp, ...existingMcp }
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
...existing,
|
|
94
|
+
mcpServers: Object.keys(mergedMcp).length > 0 ? mergedMcp : undefined,
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/targets/opencode.ts
CHANGED
|
@@ -1,31 +1,93 @@
|
|
|
1
1
|
import path from "path"
|
|
2
|
-
import { backupFile, copyDir, ensureDir, writeJson, writeText } from "../utils/files"
|
|
3
|
-
import type { OpenCodeBundle } from "../types/opencode"
|
|
2
|
+
import { backupFile, copyDir, ensureDir, pathExists, readJson, writeJson, writeText } from "../utils/files"
|
|
3
|
+
import type { OpenCodeBundle, OpenCodeConfig } from "../types/opencode"
|
|
4
|
+
|
|
5
|
+
// Merges plugin config into existing opencode.json. User keys win on conflict. See ADR-002.
|
|
6
|
+
async function mergeOpenCodeConfig(
|
|
7
|
+
configPath: string,
|
|
8
|
+
incoming: OpenCodeConfig,
|
|
9
|
+
): Promise<OpenCodeConfig> {
|
|
10
|
+
// If no existing config, write plugin config as-is
|
|
11
|
+
if (!(await pathExists(configPath))) return incoming
|
|
12
|
+
|
|
13
|
+
let existing: OpenCodeConfig
|
|
14
|
+
try {
|
|
15
|
+
existing = await readJson<OpenCodeConfig>(configPath)
|
|
16
|
+
} catch {
|
|
17
|
+
// Safety first per AGENTS.md -- do not destroy user data even if their config is malformed.
|
|
18
|
+
// Warn and fall back to plugin-only config rather than crashing.
|
|
19
|
+
console.warn(
|
|
20
|
+
`Warning: existing ${configPath} is not valid JSON. Writing plugin config without merging.`
|
|
21
|
+
)
|
|
22
|
+
return incoming
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// User config wins on conflict -- see ADR-002
|
|
26
|
+
// MCP servers: add plugin entry, skip keys already in user config.
|
|
27
|
+
const mergedMcp = {
|
|
28
|
+
...(incoming.mcp ?? {}),
|
|
29
|
+
...(existing.mcp ?? {}), // existing takes precedence (overwrites same-named plugin entry)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Permission: add plugin entry, skip keys already in user config.
|
|
33
|
+
const mergedPermission = incoming.permission
|
|
34
|
+
? {
|
|
35
|
+
...(incoming.permission),
|
|
36
|
+
...(existing.permission ?? {}), // existing takes precedence
|
|
37
|
+
}
|
|
38
|
+
: existing.permission
|
|
39
|
+
|
|
40
|
+
// Tools: same pattern
|
|
41
|
+
const mergedTools = incoming.tools
|
|
42
|
+
? {
|
|
43
|
+
...(incoming.tools),
|
|
44
|
+
...(existing.tools ?? {}),
|
|
45
|
+
}
|
|
46
|
+
: existing.tools
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...existing, // all user keys preserved
|
|
50
|
+
$schema: incoming.$schema ?? existing.$schema,
|
|
51
|
+
mcp: Object.keys(mergedMcp).length > 0 ? mergedMcp : undefined,
|
|
52
|
+
permission: mergedPermission,
|
|
53
|
+
tools: mergedTools,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
4
56
|
|
|
5
57
|
export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBundle): Promise<void> {
|
|
6
|
-
const
|
|
7
|
-
await ensureDir(
|
|
58
|
+
const openCodePaths = resolveOpenCodePaths(outputRoot)
|
|
59
|
+
await ensureDir(openCodePaths.root)
|
|
8
60
|
|
|
9
|
-
const backupPath = await backupFile(
|
|
61
|
+
const backupPath = await backupFile(openCodePaths.configPath)
|
|
10
62
|
if (backupPath) {
|
|
11
63
|
console.log(`Backed up existing config to ${backupPath}`)
|
|
12
64
|
}
|
|
13
|
-
await
|
|
65
|
+
const merged = await mergeOpenCodeConfig(openCodePaths.configPath, bundle.config)
|
|
66
|
+
await writeJson(openCodePaths.configPath, merged)
|
|
14
67
|
|
|
15
|
-
const agentsDir =
|
|
68
|
+
const agentsDir = openCodePaths.agentsDir
|
|
16
69
|
for (const agent of bundle.agents) {
|
|
17
70
|
await writeText(path.join(agentsDir, `${agent.name}.md`), agent.content + "\n")
|
|
18
71
|
}
|
|
19
72
|
|
|
73
|
+
for (const commandFile of bundle.commandFiles) {
|
|
74
|
+
const dest = path.join(openCodePaths.commandDir, `${commandFile.name}.md`)
|
|
75
|
+
const cmdBackupPath = await backupFile(dest)
|
|
76
|
+
if (cmdBackupPath) {
|
|
77
|
+
console.log(`Backed up existing command file to ${cmdBackupPath}`)
|
|
78
|
+
}
|
|
79
|
+
await writeText(dest, commandFile.content + "\n")
|
|
80
|
+
}
|
|
81
|
+
|
|
20
82
|
if (bundle.plugins.length > 0) {
|
|
21
|
-
const pluginsDir =
|
|
83
|
+
const pluginsDir = openCodePaths.pluginsDir
|
|
22
84
|
for (const plugin of bundle.plugins) {
|
|
23
85
|
await writeText(path.join(pluginsDir, plugin.name), plugin.content + "\n")
|
|
24
86
|
}
|
|
25
87
|
}
|
|
26
88
|
|
|
27
89
|
if (bundle.skillDirs.length > 0) {
|
|
28
|
-
const skillsRoot =
|
|
90
|
+
const skillsRoot = openCodePaths.skillsDir
|
|
29
91
|
for (const skill of bundle.skillDirs) {
|
|
30
92
|
await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
|
|
31
93
|
}
|
|
@@ -43,6 +105,8 @@ function resolveOpenCodePaths(outputRoot: string) {
|
|
|
43
105
|
agentsDir: path.join(outputRoot, "agents"),
|
|
44
106
|
pluginsDir: path.join(outputRoot, "plugins"),
|
|
45
107
|
skillsDir: path.join(outputRoot, "skills"),
|
|
108
|
+
// .md command files; alternative to the command key in opencode.json
|
|
109
|
+
commandDir: path.join(outputRoot, "commands"),
|
|
46
110
|
}
|
|
47
111
|
}
|
|
48
112
|
|
|
@@ -53,5 +117,7 @@ function resolveOpenCodePaths(outputRoot: string) {
|
|
|
53
117
|
agentsDir: path.join(outputRoot, ".opencode", "agents"),
|
|
54
118
|
pluginsDir: path.join(outputRoot, ".opencode", "plugins"),
|
|
55
119
|
skillsDir: path.join(outputRoot, ".opencode", "skills"),
|
|
120
|
+
// .md command files; alternative to the command key in opencode.json
|
|
121
|
+
commandDir: path.join(outputRoot, ".opencode", "commands"),
|
|
56
122
|
}
|
|
57
|
-
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { backupFile, copyDir, ensureDir, writeJson, writeText } from "../utils/files"
|
|
3
|
+
import type { QwenBundle, QwenExtensionConfig } from "../types/qwen"
|
|
4
|
+
|
|
5
|
+
export async function writeQwenBundle(outputRoot: string, bundle: QwenBundle): Promise<void> {
|
|
6
|
+
const qwenPaths = resolveQwenPaths(outputRoot)
|
|
7
|
+
await ensureDir(qwenPaths.root)
|
|
8
|
+
|
|
9
|
+
// Write qwen-extension.json config
|
|
10
|
+
const configPath = qwenPaths.configPath
|
|
11
|
+
const backupPath = await backupFile(configPath)
|
|
12
|
+
if (backupPath) {
|
|
13
|
+
console.log(`Backed up existing config to ${backupPath}`)
|
|
14
|
+
}
|
|
15
|
+
await writeJson(configPath, bundle.config)
|
|
16
|
+
|
|
17
|
+
// Write context file (QWEN.md)
|
|
18
|
+
if (bundle.contextFile) {
|
|
19
|
+
await writeText(qwenPaths.contextPath, bundle.contextFile + "\n")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Write agents
|
|
23
|
+
const agentsDir = qwenPaths.agentsDir
|
|
24
|
+
await ensureDir(agentsDir)
|
|
25
|
+
for (const agent of bundle.agents) {
|
|
26
|
+
const ext = agent.format === "yaml" ? "yaml" : "md"
|
|
27
|
+
await writeText(path.join(agentsDir, `${agent.name}.${ext}`), agent.content + "\n")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Write commands
|
|
31
|
+
const commandsDir = qwenPaths.commandsDir
|
|
32
|
+
await ensureDir(commandsDir)
|
|
33
|
+
for (const commandFile of bundle.commandFiles) {
|
|
34
|
+
// Support nested commands with colon separator
|
|
35
|
+
const parts = commandFile.name.split(":")
|
|
36
|
+
if (parts.length > 1) {
|
|
37
|
+
const nestedDir = path.join(commandsDir, ...parts.slice(0, -1))
|
|
38
|
+
await ensureDir(nestedDir)
|
|
39
|
+
await writeText(path.join(nestedDir, `${parts[parts.length - 1]}.md`), commandFile.content + "\n")
|
|
40
|
+
} else {
|
|
41
|
+
await writeText(path.join(commandsDir, `${commandFile.name}.md`), commandFile.content + "\n")
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Copy skills
|
|
46
|
+
if (bundle.skillDirs.length > 0) {
|
|
47
|
+
const skillsRoot = qwenPaths.skillsDir
|
|
48
|
+
await ensureDir(skillsRoot)
|
|
49
|
+
for (const skill of bundle.skillDirs) {
|
|
50
|
+
await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveQwenPaths(outputRoot: string) {
|
|
56
|
+
return {
|
|
57
|
+
root: outputRoot,
|
|
58
|
+
configPath: path.join(outputRoot, "qwen-extension.json"),
|
|
59
|
+
contextPath: path.join(outputRoot, "QWEN.md"),
|
|
60
|
+
agentsDir: path.join(outputRoot, "agents"),
|
|
61
|
+
commandsDir: path.join(outputRoot, "commands"),
|
|
62
|
+
skillsDir: path.join(outputRoot, "skills"),
|
|
63
|
+
}
|
|
64
|
+
}
|