@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.
Files changed (121) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.github/workflows/publish.yml +20 -10
  3. package/.releaserc.json +31 -0
  4. package/AGENTS.md +6 -1
  5. package/CHANGELOG.md +76 -0
  6. package/CLAUDE.md +16 -3
  7. package/README.md +83 -16
  8. package/bun.lock +977 -0
  9. package/docs/plans/2026-02-14-feat-auto-detect-install-and-gemini-sync-plan.md +360 -0
  10. package/docs/plans/2026-02-25-feat-windsurf-global-scope-support-plan.md +627 -0
  11. package/docs/plans/2026-03-01-feat-ce-command-aliases-backwards-compatible-deprecation-plan.md +261 -0
  12. package/docs/plans/2026-03-01-fix-setup-skill-non-claude-llm-fallback-plan.md +140 -0
  13. package/docs/plans/2026-03-03-feat-sync-claude-mcp-all-supported-providers-plan.md +639 -0
  14. package/docs/plans/feature_opencode-commands-as-md-and-config-merge.md +574 -0
  15. package/docs/solutions/adding-converter-target-providers.md +693 -0
  16. package/docs/solutions/plugin-versioning-requirements.md +7 -3
  17. package/docs/specs/windsurf.md +477 -0
  18. package/package.json +10 -4
  19. package/plans/landing-page-launchkit-refresh.md +2 -2
  20. package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
  21. package/plugins/compound-engineering/CHANGELOG.md +82 -1
  22. package/plugins/compound-engineering/CLAUDE.md +14 -7
  23. package/plugins/compound-engineering/README.md +10 -7
  24. package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
  25. package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
  26. package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
  27. package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
  28. package/plugins/compound-engineering/commands/ce/compound.md +240 -0
  29. package/plugins/compound-engineering/commands/ce/plan.md +636 -0
  30. package/plugins/compound-engineering/commands/ce/review.md +525 -0
  31. package/plugins/compound-engineering/commands/ce/work.md +470 -0
  32. package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
  33. package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
  34. package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
  35. package/plugins/compound-engineering/commands/feature-video.md +15 -6
  36. package/plugins/compound-engineering/commands/heal-skill.md +1 -1
  37. package/plugins/compound-engineering/commands/lfg.md +3 -3
  38. package/plugins/compound-engineering/commands/slfg.md +3 -3
  39. package/plugins/compound-engineering/commands/test-xcode.md +2 -2
  40. package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
  41. package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
  42. package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
  43. package/plugins/compound-engineering/commands/workflows/review.md +4 -522
  44. package/plugins/compound-engineering/commands/workflows/work.md +4 -448
  45. package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
  46. package/plugins/compound-engineering/skills/create-agent-skills/workflows/add-workflow.md +6 -0
  47. package/plugins/compound-engineering/skills/create-agent-skills/workflows/create-new-skill.md +6 -0
  48. package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
  49. package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
  50. package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
  51. package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
  52. package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
  53. package/plugins/compound-engineering/skills/setup/SKILL.md +8 -2
  54. package/src/commands/convert.ts +101 -24
  55. package/src/commands/install.ts +102 -45
  56. package/src/commands/sync.ts +43 -62
  57. package/src/converters/claude-to-openclaw.ts +240 -0
  58. package/src/converters/claude-to-opencode.ts +12 -10
  59. package/src/converters/claude-to-qwen.ts +238 -0
  60. package/src/converters/claude-to-windsurf.ts +205 -0
  61. package/src/index.ts +2 -1
  62. package/src/parsers/claude-home.ts +55 -3
  63. package/src/sync/codex.ts +38 -62
  64. package/src/sync/commands.ts +198 -0
  65. package/src/sync/copilot.ts +14 -36
  66. package/src/sync/droid.ts +50 -9
  67. package/src/sync/gemini.ts +135 -0
  68. package/src/sync/json-config.ts +47 -0
  69. package/src/sync/kiro.ts +49 -0
  70. package/src/sync/mcp-transports.ts +19 -0
  71. package/src/sync/openclaw.ts +18 -0
  72. package/src/sync/opencode.ts +10 -30
  73. package/src/sync/pi.ts +12 -36
  74. package/src/sync/qwen.ts +66 -0
  75. package/src/sync/registry.ts +141 -0
  76. package/src/sync/skills.ts +21 -0
  77. package/src/sync/windsurf.ts +59 -0
  78. package/src/targets/index.ts +60 -1
  79. package/src/targets/openclaw.ts +96 -0
  80. package/src/targets/opencode.ts +76 -10
  81. package/src/targets/qwen.ts +64 -0
  82. package/src/targets/windsurf.ts +104 -0
  83. package/src/types/kiro.ts +3 -1
  84. package/src/types/openclaw.ts +52 -0
  85. package/src/types/opencode.ts +7 -8
  86. package/src/types/qwen.ts +51 -0
  87. package/src/types/windsurf.ts +35 -0
  88. package/src/utils/codex-agents.ts +1 -1
  89. package/src/utils/detect-tools.ts +37 -0
  90. package/src/utils/files.ts +14 -0
  91. package/src/utils/resolve-output.ts +50 -0
  92. package/src/utils/secrets.ts +24 -0
  93. package/src/utils/symlink.ts +4 -6
  94. package/tests/claude-home.test.ts +46 -0
  95. package/tests/cli.test.ts +180 -0
  96. package/tests/converter.test.ts +43 -10
  97. package/tests/detect-tools.test.ts +119 -0
  98. package/tests/openclaw-converter.test.ts +200 -0
  99. package/tests/opencode-writer.test.ts +142 -5
  100. package/tests/qwen-converter.test.ts +238 -0
  101. package/tests/resolve-output.test.ts +131 -0
  102. package/tests/sync-codex.test.ts +64 -0
  103. package/tests/sync-copilot.test.ts +60 -4
  104. package/tests/sync-droid.test.ts +44 -4
  105. package/tests/sync-gemini.test.ts +160 -0
  106. package/tests/sync-kiro.test.ts +83 -0
  107. package/tests/sync-openclaw.test.ts +51 -0
  108. package/tests/sync-qwen.test.ts +75 -0
  109. package/tests/sync-windsurf.test.ts +89 -0
  110. package/tests/windsurf-converter.test.ts +573 -0
  111. package/tests/windsurf-writer.test.ts +359 -0
  112. package/docs/css/docs.css +0 -675
  113. package/docs/css/style.css +0 -2886
  114. package/docs/index.html +0 -1046
  115. package/docs/js/main.js +0 -225
  116. package/docs/pages/agents.html +0 -649
  117. package/docs/pages/changelog.html +0 -534
  118. package/docs/pages/commands.html +0 -523
  119. package/docs/pages/getting-started.html +0 -582
  120. package/docs/pages/mcp-servers.html +0 -409
  121. package/docs/pages/skills.html +0 -611
@@ -28,6 +28,34 @@ describe("syncToCopilot", () => {
28
28
  expect(linkedStat.isSymbolicLink()).toBe(true)
29
29
  })
30
30
 
31
+ test("converts personal commands into Copilot skills", async () => {
32
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-copilot-cmd-"))
33
+
34
+ const config: ClaudeHomeConfig = {
35
+ skills: [],
36
+ commands: [
37
+ {
38
+ name: "workflows:plan",
39
+ description: "Planning command",
40
+ argumentHint: "[goal]",
41
+ body: "Plan the work carefully.",
42
+ sourcePath: "/tmp/workflows/plan.md",
43
+ },
44
+ ],
45
+ mcpServers: {},
46
+ }
47
+
48
+ await syncToCopilot(config, tempRoot)
49
+
50
+ const skillContent = await fs.readFile(
51
+ path.join(tempRoot, "skills", "workflows-plan", "SKILL.md"),
52
+ "utf8",
53
+ )
54
+ expect(skillContent).toContain("name: workflows-plan")
55
+ expect(skillContent).toContain("Planning command")
56
+ expect(skillContent).toContain("## Arguments")
57
+ })
58
+
31
59
  test("skips skills with invalid names", async () => {
32
60
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-copilot-invalid-"))
33
61
 
@@ -51,7 +79,7 @@ describe("syncToCopilot", () => {
51
79
 
52
80
  test("merges MCP config with existing file", async () => {
53
81
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-copilot-merge-"))
54
- const mcpPath = path.join(tempRoot, "copilot-mcp-config.json")
82
+ const mcpPath = path.join(tempRoot, "mcp-config.json")
55
83
 
56
84
  await fs.writeFile(
57
85
  mcpPath,
@@ -77,6 +105,7 @@ describe("syncToCopilot", () => {
77
105
 
78
106
  expect(merged.mcpServers.existing?.command).toBe("node")
79
107
  expect(merged.mcpServers.context7?.url).toBe("https://mcp.context7.com/mcp")
108
+ expect(merged.mcpServers.context7?.type).toBe("http")
80
109
  })
81
110
 
82
111
  test("transforms MCP env var names to COPILOT_MCP_ prefix", async () => {
@@ -95,7 +124,7 @@ describe("syncToCopilot", () => {
95
124
 
96
125
  await syncToCopilot(config, tempRoot)
97
126
 
98
- const mcpPath = path.join(tempRoot, "copilot-mcp-config.json")
127
+ const mcpPath = path.join(tempRoot, "mcp-config.json")
99
128
  const mcpConfig = JSON.parse(await fs.readFile(mcpPath, "utf8")) as {
100
129
  mcpServers: Record<string, { env?: Record<string, string> }>
101
130
  }
@@ -118,7 +147,7 @@ describe("syncToCopilot", () => {
118
147
 
119
148
  await syncToCopilot(config, tempRoot)
120
149
 
121
- const mcpPath = path.join(tempRoot, "copilot-mcp-config.json")
150
+ const mcpPath = path.join(tempRoot, "mcp-config.json")
122
151
  const stat = await fs.stat(mcpPath)
123
152
  // Check owner read+write permission (0o600 = 33216 in decimal, masked to file perms)
124
153
  const perms = stat.mode & 0o777
@@ -142,7 +171,34 @@ describe("syncToCopilot", () => {
142
171
 
143
172
  await syncToCopilot(config, tempRoot)
144
173
 
145
- const mcpExists = await fs.access(path.join(tempRoot, "copilot-mcp-config.json")).then(() => true).catch(() => false)
174
+ const mcpExists = await fs.access(path.join(tempRoot, "mcp-config.json")).then(() => true).catch(() => false)
146
175
  expect(mcpExists).toBe(false)
147
176
  })
177
+
178
+ test("preserves explicit SSE transport for legacy remote servers", async () => {
179
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-copilot-sse-"))
180
+
181
+ const config: ClaudeHomeConfig = {
182
+ skills: [],
183
+ mcpServers: {
184
+ legacy: {
185
+ type: "sse",
186
+ url: "https://example.com/sse",
187
+ },
188
+ },
189
+ }
190
+
191
+ await syncToCopilot(config, tempRoot)
192
+
193
+ const mcpPath = path.join(tempRoot, "mcp-config.json")
194
+ const mcpConfig = JSON.parse(await fs.readFile(mcpPath, "utf8")) as {
195
+ mcpServers: Record<string, { type?: string; url?: string }>
196
+ }
197
+
198
+ expect(mcpConfig.mcpServers.legacy).toEqual({
199
+ type: "sse",
200
+ tools: ["*"],
201
+ url: "https://example.com/sse",
202
+ })
203
+ })
148
204
  })
@@ -6,7 +6,7 @@ import { syncToDroid } from "../src/sync/droid"
6
6
  import type { ClaudeHomeConfig } from "../src/parsers/claude-home"
7
7
 
8
8
  describe("syncToDroid", () => {
9
- test("symlinks skills to factory skills dir", async () => {
9
+ test("symlinks skills to factory skills dir and writes mcp.json", async () => {
10
10
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-droid-"))
11
11
  const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one")
12
12
 
@@ -29,9 +29,49 @@ describe("syncToDroid", () => {
29
29
  const linkedStat = await fs.lstat(linkedSkillPath)
30
30
  expect(linkedStat.isSymbolicLink()).toBe(true)
31
31
 
32
- // Droid does not write MCP config
33
- const mcpExists = await fs.access(path.join(tempRoot, "mcp.json")).then(() => true).catch(() => false)
34
- expect(mcpExists).toBe(false)
32
+ const mcpConfig = JSON.parse(
33
+ await fs.readFile(path.join(tempRoot, "mcp.json"), "utf8"),
34
+ ) as {
35
+ mcpServers: Record<string, { type: string; url?: string; disabled: boolean }>
36
+ }
37
+ expect(mcpConfig.mcpServers.context7?.type).toBe("http")
38
+ expect(mcpConfig.mcpServers.context7?.url).toBe("https://mcp.context7.com/mcp")
39
+ expect(mcpConfig.mcpServers.context7?.disabled).toBe(false)
40
+ })
41
+
42
+ test("merges existing mcp.json and overwrites same-named servers from Claude", async () => {
43
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-droid-merge-"))
44
+ await fs.writeFile(
45
+ path.join(tempRoot, "mcp.json"),
46
+ JSON.stringify({
47
+ theme: "dark",
48
+ mcpServers: {
49
+ shared: { type: "http", url: "https://old.example.com", disabled: true },
50
+ existing: { type: "stdio", command: "node", disabled: false },
51
+ },
52
+ }, null, 2),
53
+ )
54
+
55
+ const config: ClaudeHomeConfig = {
56
+ skills: [],
57
+ mcpServers: {
58
+ shared: { url: "https://new.example.com" },
59
+ },
60
+ }
61
+
62
+ await syncToDroid(config, tempRoot)
63
+
64
+ const mcpConfig = JSON.parse(
65
+ await fs.readFile(path.join(tempRoot, "mcp.json"), "utf8"),
66
+ ) as {
67
+ theme: string
68
+ mcpServers: Record<string, { type: string; url?: string; command?: string; disabled: boolean }>
69
+ }
70
+
71
+ expect(mcpConfig.theme).toBe("dark")
72
+ expect(mcpConfig.mcpServers.existing?.command).toBe("node")
73
+ expect(mcpConfig.mcpServers.shared?.url).toBe("https://new.example.com")
74
+ expect(mcpConfig.mcpServers.shared?.disabled).toBe(false)
35
75
  })
36
76
 
37
77
  test("skips skills with invalid names", async () => {
@@ -0,0 +1,160 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { promises as fs } from "fs"
3
+ import path from "path"
4
+ import os from "os"
5
+ import { syncToGemini } from "../src/sync/gemini"
6
+ import type { ClaudeHomeConfig } from "../src/parsers/claude-home"
7
+
8
+ describe("syncToGemini", () => {
9
+ test("symlinks skills and writes settings.json", async () => {
10
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-gemini-"))
11
+ const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one")
12
+
13
+ const config: ClaudeHomeConfig = {
14
+ skills: [
15
+ {
16
+ name: "skill-one",
17
+ sourceDir: fixtureSkillDir,
18
+ skillPath: path.join(fixtureSkillDir, "SKILL.md"),
19
+ },
20
+ ],
21
+ mcpServers: {
22
+ context7: { url: "https://mcp.context7.com/mcp" },
23
+ local: { command: "echo", args: ["hello"], env: { FOO: "bar" } },
24
+ },
25
+ }
26
+
27
+ await syncToGemini(config, tempRoot)
28
+
29
+ // Check skill symlink
30
+ const linkedSkillPath = path.join(tempRoot, "skills", "skill-one")
31
+ const linkedStat = await fs.lstat(linkedSkillPath)
32
+ expect(linkedStat.isSymbolicLink()).toBe(true)
33
+
34
+ // Check settings.json
35
+ const settingsPath = path.join(tempRoot, "settings.json")
36
+ const settings = JSON.parse(await fs.readFile(settingsPath, "utf8")) as {
37
+ mcpServers: Record<string, { url?: string; command?: string; args?: string[]; env?: Record<string, string> }>
38
+ }
39
+
40
+ expect(settings.mcpServers.context7?.url).toBe("https://mcp.context7.com/mcp")
41
+ expect(settings.mcpServers.local?.command).toBe("echo")
42
+ expect(settings.mcpServers.local?.args).toEqual(["hello"])
43
+ expect(settings.mcpServers.local?.env).toEqual({ FOO: "bar" })
44
+ })
45
+
46
+ test("merges existing settings.json", async () => {
47
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-gemini-merge-"))
48
+ const settingsPath = path.join(tempRoot, "settings.json")
49
+
50
+ await fs.writeFile(
51
+ settingsPath,
52
+ JSON.stringify({
53
+ theme: "dark",
54
+ mcpServers: { existing: { command: "node", args: ["server.js"] } },
55
+ }, null, 2),
56
+ )
57
+
58
+ const config: ClaudeHomeConfig = {
59
+ skills: [],
60
+ mcpServers: {
61
+ context7: { url: "https://mcp.context7.com/mcp" },
62
+ },
63
+ }
64
+
65
+ await syncToGemini(config, tempRoot)
66
+
67
+ const merged = JSON.parse(await fs.readFile(settingsPath, "utf8")) as {
68
+ theme: string
69
+ mcpServers: Record<string, { command?: string; url?: string }>
70
+ }
71
+
72
+ // Preserves existing settings
73
+ expect(merged.theme).toBe("dark")
74
+ // Preserves existing MCP servers
75
+ expect(merged.mcpServers.existing?.command).toBe("node")
76
+ // Adds new MCP servers
77
+ expect(merged.mcpServers.context7?.url).toBe("https://mcp.context7.com/mcp")
78
+ })
79
+
80
+ test("writes personal commands as Gemini TOML prompts", async () => {
81
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-gemini-cmd-"))
82
+
83
+ const config: ClaudeHomeConfig = {
84
+ skills: [],
85
+ commands: [
86
+ {
87
+ name: "workflows:plan",
88
+ description: "Planning command",
89
+ argumentHint: "[goal]",
90
+ body: "Plan the work carefully.",
91
+ sourcePath: "/tmp/workflows/plan.md",
92
+ },
93
+ ],
94
+ mcpServers: {},
95
+ }
96
+
97
+ await syncToGemini(config, tempRoot)
98
+
99
+ const content = await fs.readFile(
100
+ path.join(tempRoot, "commands", "workflows", "plan.toml"),
101
+ "utf8",
102
+ )
103
+ expect(content).toContain("Planning command")
104
+ expect(content).toContain("User request: {{args}}")
105
+ })
106
+
107
+ test("does not write settings.json when no MCP servers", async () => {
108
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-gemini-nomcp-"))
109
+ const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one")
110
+
111
+ const config: ClaudeHomeConfig = {
112
+ skills: [
113
+ {
114
+ name: "skill-one",
115
+ sourceDir: fixtureSkillDir,
116
+ skillPath: path.join(fixtureSkillDir, "SKILL.md"),
117
+ },
118
+ ],
119
+ mcpServers: {},
120
+ }
121
+
122
+ await syncToGemini(config, tempRoot)
123
+
124
+ // Skills should still be symlinked
125
+ const linkedSkillPath = path.join(tempRoot, "skills", "skill-one")
126
+ const linkedStat = await fs.lstat(linkedSkillPath)
127
+ expect(linkedStat.isSymbolicLink()).toBe(true)
128
+
129
+ // But settings.json should not exist
130
+ const settingsExists = await fs.access(path.join(tempRoot, "settings.json")).then(() => true).catch(() => false)
131
+ expect(settingsExists).toBe(false)
132
+ })
133
+
134
+ test("skips mirrored ~/.agents skills when syncing to ~/.gemini and removes stale duplicate symlinks", async () => {
135
+ const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "sync-gemini-home-"))
136
+ const geminiRoot = path.join(tempHome, ".gemini")
137
+ const agentsSkillDir = path.join(tempHome, ".agents", "skills", "skill-one")
138
+
139
+ await fs.mkdir(path.join(agentsSkillDir), { recursive: true })
140
+ await fs.writeFile(path.join(agentsSkillDir, "SKILL.md"), "# Skill One\n", "utf8")
141
+ await fs.mkdir(path.join(geminiRoot, "skills"), { recursive: true })
142
+ await fs.symlink(agentsSkillDir, path.join(geminiRoot, "skills", "skill-one"))
143
+
144
+ const config: ClaudeHomeConfig = {
145
+ skills: [
146
+ {
147
+ name: "skill-one",
148
+ sourceDir: agentsSkillDir,
149
+ skillPath: path.join(agentsSkillDir, "SKILL.md"),
150
+ },
151
+ ],
152
+ mcpServers: {},
153
+ }
154
+
155
+ await syncToGemini(config, geminiRoot)
156
+
157
+ const duplicateExists = await fs.access(path.join(geminiRoot, "skills", "skill-one")).then(() => true).catch(() => false)
158
+ expect(duplicateExists).toBe(false)
159
+ })
160
+ })
@@ -0,0 +1,83 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { promises as fs } from "fs"
3
+ import os from "os"
4
+ import path from "path"
5
+ import type { ClaudeHomeConfig } from "../src/parsers/claude-home"
6
+ import { syncToKiro } from "../src/sync/kiro"
7
+
8
+ describe("syncToKiro", () => {
9
+ test("writes user-scope settings/mcp.json with local and remote servers", async () => {
10
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-kiro-"))
11
+ const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one")
12
+
13
+ const config: ClaudeHomeConfig = {
14
+ skills: [
15
+ {
16
+ name: "skill-one",
17
+ sourceDir: fixtureSkillDir,
18
+ skillPath: path.join(fixtureSkillDir, "SKILL.md"),
19
+ },
20
+ ],
21
+ mcpServers: {
22
+ local: { command: "echo", args: ["hello"], env: { TOKEN: "secret" } },
23
+ remote: { url: "https://example.com/mcp", headers: { Authorization: "Bearer token" } },
24
+ },
25
+ }
26
+
27
+ await syncToKiro(config, tempRoot)
28
+
29
+ expect((await fs.lstat(path.join(tempRoot, "skills", "skill-one"))).isSymbolicLink()).toBe(true)
30
+
31
+ const content = JSON.parse(
32
+ await fs.readFile(path.join(tempRoot, "settings", "mcp.json"), "utf8"),
33
+ ) as {
34
+ mcpServers: Record<string, {
35
+ command?: string
36
+ args?: string[]
37
+ env?: Record<string, string>
38
+ url?: string
39
+ headers?: Record<string, string>
40
+ }>
41
+ }
42
+
43
+ expect(content.mcpServers.local?.command).toBe("echo")
44
+ expect(content.mcpServers.local?.args).toEqual(["hello"])
45
+ expect(content.mcpServers.local?.env).toEqual({ TOKEN: "secret" })
46
+ expect(content.mcpServers.remote?.url).toBe("https://example.com/mcp")
47
+ expect(content.mcpServers.remote?.headers).toEqual({ Authorization: "Bearer token" })
48
+ })
49
+
50
+ test("merges existing settings/mcp.json", async () => {
51
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-kiro-merge-"))
52
+ await fs.mkdir(path.join(tempRoot, "settings"), { recursive: true })
53
+ await fs.writeFile(
54
+ path.join(tempRoot, "settings", "mcp.json"),
55
+ JSON.stringify({
56
+ note: "preserve",
57
+ mcpServers: {
58
+ existing: { command: "node" },
59
+ },
60
+ }, null, 2),
61
+ )
62
+
63
+ const config: ClaudeHomeConfig = {
64
+ skills: [],
65
+ mcpServers: {
66
+ remote: { url: "https://example.com/mcp" },
67
+ },
68
+ }
69
+
70
+ await syncToKiro(config, tempRoot)
71
+
72
+ const content = JSON.parse(
73
+ await fs.readFile(path.join(tempRoot, "settings", "mcp.json"), "utf8"),
74
+ ) as {
75
+ note: string
76
+ mcpServers: Record<string, { command?: string; url?: string }>
77
+ }
78
+
79
+ expect(content.note).toBe("preserve")
80
+ expect(content.mcpServers.existing?.command).toBe("node")
81
+ expect(content.mcpServers.remote?.url).toBe("https://example.com/mcp")
82
+ })
83
+ })
@@ -0,0 +1,51 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { promises as fs } from "fs"
3
+ import os from "os"
4
+ import path from "path"
5
+ import type { ClaudeHomeConfig } from "../src/parsers/claude-home"
6
+ import { syncToOpenClaw } from "../src/sync/openclaw"
7
+
8
+ describe("syncToOpenClaw", () => {
9
+ test("symlinks skills and warns instead of writing unvalidated MCP config", async () => {
10
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-openclaw-"))
11
+ const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one")
12
+ const warnings: string[] = []
13
+ const originalWarn = console.warn
14
+ console.warn = (message?: unknown) => {
15
+ warnings.push(String(message))
16
+ }
17
+
18
+ try {
19
+ const config: ClaudeHomeConfig = {
20
+ skills: [
21
+ {
22
+ name: "skill-one",
23
+ sourceDir: fixtureSkillDir,
24
+ skillPath: path.join(fixtureSkillDir, "SKILL.md"),
25
+ },
26
+ ],
27
+ commands: [
28
+ {
29
+ name: "workflows:plan",
30
+ description: "Planning command",
31
+ body: "Plan the work.",
32
+ sourcePath: "/tmp/workflows/plan.md",
33
+ },
34
+ ],
35
+ mcpServers: {
36
+ remote: { url: "https://example.com/mcp" },
37
+ },
38
+ }
39
+
40
+ await syncToOpenClaw(config, tempRoot)
41
+ } finally {
42
+ console.warn = originalWarn
43
+ }
44
+
45
+ expect((await fs.lstat(path.join(tempRoot, "skills", "skill-one"))).isSymbolicLink()).toBe(true)
46
+ const openclawConfigExists = await fs.access(path.join(tempRoot, "openclaw.json")).then(() => true).catch(() => false)
47
+ expect(openclawConfigExists).toBe(false)
48
+ expect(warnings.some((warning) => warning.includes("OpenClaw personal command sync is skipped"))).toBe(true)
49
+ expect(warnings.some((warning) => warning.includes("OpenClaw MCP sync is skipped"))).toBe(true)
50
+ })
51
+ })
@@ -0,0 +1,75 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { promises as fs } from "fs"
3
+ import os from "os"
4
+ import path from "path"
5
+ import type { ClaudeHomeConfig } from "../src/parsers/claude-home"
6
+ import { syncToQwen } from "../src/sync/qwen"
7
+
8
+ describe("syncToQwen", () => {
9
+ test("defaults ambiguous remote URLs to httpUrl and warns", async () => {
10
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-qwen-"))
11
+ const warnings: string[] = []
12
+ const originalWarn = console.warn
13
+ console.warn = (message?: unknown) => {
14
+ warnings.push(String(message))
15
+ }
16
+
17
+ try {
18
+ const config: ClaudeHomeConfig = {
19
+ skills: [],
20
+ mcpServers: {
21
+ remote: { url: "https://example.com/mcp", headers: { Authorization: "Bearer token" } },
22
+ },
23
+ }
24
+
25
+ await syncToQwen(config, tempRoot)
26
+ } finally {
27
+ console.warn = originalWarn
28
+ }
29
+
30
+ const content = JSON.parse(
31
+ await fs.readFile(path.join(tempRoot, "settings.json"), "utf8"),
32
+ ) as {
33
+ mcpServers: Record<string, { httpUrl?: string; url?: string; headers?: Record<string, string> }>
34
+ }
35
+
36
+ expect(content.mcpServers.remote?.httpUrl).toBe("https://example.com/mcp")
37
+ expect(content.mcpServers.remote?.url).toBeUndefined()
38
+ expect(content.mcpServers.remote?.headers).toEqual({ Authorization: "Bearer token" })
39
+ expect(warnings.some((warning) => warning.includes("ambiguous remote transport"))).toBe(true)
40
+ })
41
+
42
+ test("uses legacy url only for explicit SSE servers and preserves existing settings", async () => {
43
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-qwen-sse-"))
44
+ await fs.writeFile(
45
+ path.join(tempRoot, "settings.json"),
46
+ JSON.stringify({
47
+ theme: "dark",
48
+ mcpServers: {
49
+ existing: { command: "node" },
50
+ },
51
+ }, null, 2),
52
+ )
53
+
54
+ const config: ClaudeHomeConfig = {
55
+ skills: [],
56
+ mcpServers: {
57
+ legacy: { type: "sse", url: "https://example.com/sse" },
58
+ },
59
+ }
60
+
61
+ await syncToQwen(config, tempRoot)
62
+
63
+ const content = JSON.parse(
64
+ await fs.readFile(path.join(tempRoot, "settings.json"), "utf8"),
65
+ ) as {
66
+ theme: string
67
+ mcpServers: Record<string, { command?: string; httpUrl?: string; url?: string }>
68
+ }
69
+
70
+ expect(content.theme).toBe("dark")
71
+ expect(content.mcpServers.existing?.command).toBe("node")
72
+ expect(content.mcpServers.legacy?.url).toBe("https://example.com/sse")
73
+ expect(content.mcpServers.legacy?.httpUrl).toBeUndefined()
74
+ })
75
+ })
@@ -0,0 +1,89 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { promises as fs } from "fs"
3
+ import os from "os"
4
+ import path from "path"
5
+ import type { ClaudeHomeConfig } from "../src/parsers/claude-home"
6
+ import { syncToWindsurf } from "../src/sync/windsurf"
7
+
8
+ describe("syncToWindsurf", () => {
9
+ test("writes stdio, http, and sse MCP servers", async () => {
10
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-windsurf-"))
11
+ const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one")
12
+
13
+ const config: ClaudeHomeConfig = {
14
+ skills: [
15
+ {
16
+ name: "skill-one",
17
+ sourceDir: fixtureSkillDir,
18
+ skillPath: path.join(fixtureSkillDir, "SKILL.md"),
19
+ },
20
+ ],
21
+ mcpServers: {
22
+ local: { command: "npx", args: ["serve"], env: { FOO: "bar" } },
23
+ remoteHttp: { url: "https://example.com/mcp", headers: { Authorization: "Bearer a" } },
24
+ remoteSse: { type: "sse", url: "https://example.com/sse" },
25
+ },
26
+ }
27
+
28
+ await syncToWindsurf(config, tempRoot)
29
+
30
+ expect((await fs.lstat(path.join(tempRoot, "skills", "skill-one"))).isSymbolicLink()).toBe(true)
31
+
32
+ const content = JSON.parse(
33
+ await fs.readFile(path.join(tempRoot, "mcp_config.json"), "utf8"),
34
+ ) as {
35
+ mcpServers: Record<string, {
36
+ command?: string
37
+ args?: string[]
38
+ env?: Record<string, string>
39
+ serverUrl?: string
40
+ url?: string
41
+ }>
42
+ }
43
+
44
+ expect(content.mcpServers.local).toEqual({
45
+ command: "npx",
46
+ args: ["serve"],
47
+ env: { FOO: "bar" },
48
+ })
49
+ expect(content.mcpServers.remoteHttp?.serverUrl).toBe("https://example.com/mcp")
50
+ expect(content.mcpServers.remoteSse?.url).toBe("https://example.com/sse")
51
+
52
+ const perms = (await fs.stat(path.join(tempRoot, "mcp_config.json"))).mode & 0o777
53
+ expect(perms).toBe(0o600)
54
+ })
55
+
56
+ test("merges existing config and overwrites same-named servers", async () => {
57
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-windsurf-merge-"))
58
+ await fs.writeFile(
59
+ path.join(tempRoot, "mcp_config.json"),
60
+ JSON.stringify({
61
+ theme: "dark",
62
+ mcpServers: {
63
+ existing: { command: "node" },
64
+ shared: { serverUrl: "https://old.example.com" },
65
+ },
66
+ }, null, 2),
67
+ )
68
+
69
+ const config: ClaudeHomeConfig = {
70
+ skills: [],
71
+ mcpServers: {
72
+ shared: { url: "https://new.example.com" },
73
+ },
74
+ }
75
+
76
+ await syncToWindsurf(config, tempRoot)
77
+
78
+ const content = JSON.parse(
79
+ await fs.readFile(path.join(tempRoot, "mcp_config.json"), "utf8"),
80
+ ) as {
81
+ theme: string
82
+ mcpServers: Record<string, { command?: string; serverUrl?: string }>
83
+ }
84
+
85
+ expect(content.theme).toBe("dark")
86
+ expect(content.mcpServers.existing?.command).toBe("node")
87
+ expect(content.mcpServers.shared?.serverUrl).toBe("https://new.example.com")
88
+ })
89
+ })