@every-env/compound-plugin 0.8.0 → 0.12.0
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/AGENTS.md +5 -1
- package/CHANGELOG.md +50 -0
- package/CLAUDE.md +3 -3
- package/README.md +52 -14
- 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/feature_opencode-commands-as-md-and-config-merge.md +574 -0
- package/docs/solutions/adding-converter-target-providers.md +692 -0
- package/docs/solutions/plugin-versioning-requirements.md +3 -3
- package/docs/specs/kiro.md +171 -0
- package/docs/specs/windsurf.md +477 -0
- package/package.json +1 -1
- 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 +72 -1
- package/plugins/compound-engineering/CLAUDE.md +9 -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/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 +2 -2
- package/src/commands/convert.ts +101 -23
- package/src/commands/install.ts +102 -41
- package/src/commands/sync.ts +58 -38
- package/src/converters/claude-to-kiro.ts +262 -0
- 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/sync/gemini.ts +76 -0
- package/src/targets/index.ts +69 -1
- package/src/targets/kiro.ts +122 -0
- 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 +44 -0
- package/src/types/openclaw.ts +52 -0
- package/src/types/opencode.ts +7 -8
- package/src/types/qwen.ts +48 -0
- package/src/types/windsurf.ts +34 -0
- package/src/utils/detect-tools.ts +46 -0
- package/src/utils/files.ts +7 -0
- package/src/utils/resolve-output.ts +50 -0
- package/src/utils/secrets.ts +24 -0
- package/tests/cli.test.ts +78 -0
- package/tests/converter.test.ts +43 -10
- package/tests/detect-tools.test.ts +96 -0
- package/tests/kiro-converter.test.ts +381 -0
- package/tests/kiro-writer.test.ts +273 -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-gemini.test.ts +106 -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,573 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { convertClaudeToWindsurf, transformContentForWindsurf, normalizeName } from "../src/converters/claude-to-windsurf"
|
|
3
|
+
import type { ClaudePlugin } from "../src/types/claude"
|
|
4
|
+
|
|
5
|
+
const fixturePlugin: ClaudePlugin = {
|
|
6
|
+
root: "/tmp/plugin",
|
|
7
|
+
manifest: { name: "fixture", version: "1.0.0" },
|
|
8
|
+
agents: [
|
|
9
|
+
{
|
|
10
|
+
name: "Security Reviewer",
|
|
11
|
+
description: "Security-focused agent",
|
|
12
|
+
capabilities: ["Threat modeling", "OWASP"],
|
|
13
|
+
model: "claude-sonnet-4-20250514",
|
|
14
|
+
body: "Focus on vulnerabilities.",
|
|
15
|
+
sourcePath: "/tmp/plugin/agents/security-reviewer.md",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
commands: [
|
|
19
|
+
{
|
|
20
|
+
name: "workflows:plan",
|
|
21
|
+
description: "Planning command",
|
|
22
|
+
argumentHint: "[FOCUS]",
|
|
23
|
+
model: "inherit",
|
|
24
|
+
allowedTools: ["Read"],
|
|
25
|
+
body: "Plan the work.",
|
|
26
|
+
sourcePath: "/tmp/plugin/commands/workflows/plan.md",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
skills: [
|
|
30
|
+
{
|
|
31
|
+
name: "existing-skill",
|
|
32
|
+
description: "Existing skill",
|
|
33
|
+
sourceDir: "/tmp/plugin/skills/existing-skill",
|
|
34
|
+
skillPath: "/tmp/plugin/skills/existing-skill/SKILL.md",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
hooks: undefined,
|
|
38
|
+
mcpServers: {
|
|
39
|
+
local: { command: "echo", args: ["hello"] },
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const defaultOptions = {
|
|
44
|
+
agentMode: "subagent" as const,
|
|
45
|
+
inferTemperature: false,
|
|
46
|
+
permissions: "none" as const,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("convertClaudeToWindsurf", () => {
|
|
50
|
+
test("converts agents to skills with correct name and description in SKILL.md", () => {
|
|
51
|
+
const bundle = convertClaudeToWindsurf(fixturePlugin, defaultOptions)
|
|
52
|
+
|
|
53
|
+
const skill = bundle.agentSkills.find((s) => s.name === "security-reviewer")
|
|
54
|
+
expect(skill).toBeDefined()
|
|
55
|
+
expect(skill!.content).toContain("name: security-reviewer")
|
|
56
|
+
expect(skill!.content).toContain("description: Security-focused agent")
|
|
57
|
+
expect(skill!.content).toContain("Focus on vulnerabilities.")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("agent capabilities included in skill content", () => {
|
|
61
|
+
const bundle = convertClaudeToWindsurf(fixturePlugin, defaultOptions)
|
|
62
|
+
const skill = bundle.agentSkills.find((s) => s.name === "security-reviewer")
|
|
63
|
+
expect(skill!.content).toContain("## Capabilities")
|
|
64
|
+
expect(skill!.content).toContain("- Threat modeling")
|
|
65
|
+
expect(skill!.content).toContain("- OWASP")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("agent with empty description gets default description", () => {
|
|
69
|
+
const plugin: ClaudePlugin = {
|
|
70
|
+
...fixturePlugin,
|
|
71
|
+
agents: [
|
|
72
|
+
{
|
|
73
|
+
name: "my-agent",
|
|
74
|
+
body: "Do things.",
|
|
75
|
+
sourcePath: "/tmp/plugin/agents/my-agent.md",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
commands: [],
|
|
79
|
+
skills: [],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
83
|
+
expect(bundle.agentSkills[0].content).toContain("description: Converted from Claude agent my-agent")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("agent model field silently dropped", () => {
|
|
87
|
+
const bundle = convertClaudeToWindsurf(fixturePlugin, defaultOptions)
|
|
88
|
+
const skill = bundle.agentSkills.find((s) => s.name === "security-reviewer")
|
|
89
|
+
expect(skill!.content).not.toContain("model:")
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test("agent with empty body gets default body text", () => {
|
|
93
|
+
const plugin: ClaudePlugin = {
|
|
94
|
+
...fixturePlugin,
|
|
95
|
+
agents: [
|
|
96
|
+
{
|
|
97
|
+
name: "Empty Agent",
|
|
98
|
+
description: "An empty agent",
|
|
99
|
+
body: "",
|
|
100
|
+
sourcePath: "/tmp/plugin/agents/empty.md",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
commands: [],
|
|
104
|
+
skills: [],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
108
|
+
expect(bundle.agentSkills[0].content).toContain("Instructions converted from the Empty Agent agent.")
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("converts commands to workflows with description", () => {
|
|
112
|
+
const bundle = convertClaudeToWindsurf(fixturePlugin, defaultOptions)
|
|
113
|
+
|
|
114
|
+
expect(bundle.commandWorkflows).toHaveLength(1)
|
|
115
|
+
const workflow = bundle.commandWorkflows[0]
|
|
116
|
+
expect(workflow.name).toBe("workflows-plan")
|
|
117
|
+
expect(workflow.description).toBe("Planning command")
|
|
118
|
+
expect(workflow.body).toContain("Plan the work.")
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test("command argumentHint preserved as note in body", () => {
|
|
122
|
+
const bundle = convertClaudeToWindsurf(fixturePlugin, defaultOptions)
|
|
123
|
+
const workflow = bundle.commandWorkflows[0]
|
|
124
|
+
expect(workflow.body).toContain("> Arguments: [FOCUS]")
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test("command with no description gets fallback", () => {
|
|
128
|
+
const plugin: ClaudePlugin = {
|
|
129
|
+
...fixturePlugin,
|
|
130
|
+
commands: [
|
|
131
|
+
{
|
|
132
|
+
name: "my-command",
|
|
133
|
+
body: "Do things.",
|
|
134
|
+
sourcePath: "/tmp/plugin/commands/my-command.md",
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
agents: [],
|
|
138
|
+
skills: [],
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
142
|
+
expect(bundle.commandWorkflows[0].description).toBe("Converted from Claude command my-command")
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test("command with disableModelInvocation is still included", () => {
|
|
146
|
+
const plugin: ClaudePlugin = {
|
|
147
|
+
...fixturePlugin,
|
|
148
|
+
commands: [
|
|
149
|
+
{
|
|
150
|
+
name: "disabled-command",
|
|
151
|
+
description: "Disabled command",
|
|
152
|
+
disableModelInvocation: true,
|
|
153
|
+
body: "Disabled body.",
|
|
154
|
+
sourcePath: "/tmp/plugin/commands/disabled.md",
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
agents: [],
|
|
158
|
+
skills: [],
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
162
|
+
expect(bundle.commandWorkflows).toHaveLength(1)
|
|
163
|
+
expect(bundle.commandWorkflows[0].name).toBe("disabled-command")
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test("command allowedTools silently dropped", () => {
|
|
167
|
+
const bundle = convertClaudeToWindsurf(fixturePlugin, defaultOptions)
|
|
168
|
+
const workflow = bundle.commandWorkflows[0]
|
|
169
|
+
expect(workflow.body).not.toContain("allowedTools")
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test("skills pass through as directory references", () => {
|
|
173
|
+
const bundle = convertClaudeToWindsurf(fixturePlugin, defaultOptions)
|
|
174
|
+
|
|
175
|
+
expect(bundle.skillDirs).toHaveLength(1)
|
|
176
|
+
expect(bundle.skillDirs[0].name).toBe("existing-skill")
|
|
177
|
+
expect(bundle.skillDirs[0].sourceDir).toBe("/tmp/plugin/skills/existing-skill")
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test("name normalization handles various inputs", () => {
|
|
181
|
+
const plugin: ClaudePlugin = {
|
|
182
|
+
...fixturePlugin,
|
|
183
|
+
agents: [
|
|
184
|
+
{ name: "My Cool Agent!!!", description: "Cool", body: "Body.", sourcePath: "/tmp/a.md" },
|
|
185
|
+
{ name: "UPPERCASE-AGENT", description: "Upper", body: "Body.", sourcePath: "/tmp/b.md" },
|
|
186
|
+
{ name: "agent--with--double-hyphens", description: "Hyphens", body: "Body.", sourcePath: "/tmp/c.md" },
|
|
187
|
+
],
|
|
188
|
+
commands: [],
|
|
189
|
+
skills: [],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
193
|
+
expect(bundle.agentSkills[0].name).toBe("my-cool-agent")
|
|
194
|
+
expect(bundle.agentSkills[1].name).toBe("uppercase-agent")
|
|
195
|
+
expect(bundle.agentSkills[2].name).toBe("agent-with-double-hyphens")
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test("name deduplication within agent skills", () => {
|
|
199
|
+
const plugin: ClaudePlugin = {
|
|
200
|
+
...fixturePlugin,
|
|
201
|
+
agents: [
|
|
202
|
+
{ name: "reviewer", description: "First", body: "Body.", sourcePath: "/tmp/a.md" },
|
|
203
|
+
{ name: "Reviewer", description: "Second", body: "Body.", sourcePath: "/tmp/b.md" },
|
|
204
|
+
],
|
|
205
|
+
commands: [],
|
|
206
|
+
skills: [],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
210
|
+
expect(bundle.agentSkills[0].name).toBe("reviewer")
|
|
211
|
+
expect(bundle.agentSkills[1].name).toBe("reviewer-2")
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test("agent skill name deduplicates against pass-through skill names", () => {
|
|
215
|
+
const plugin: ClaudePlugin = {
|
|
216
|
+
...fixturePlugin,
|
|
217
|
+
agents: [
|
|
218
|
+
{ name: "existing-skill", description: "Agent with same name as skill", body: "Body.", sourcePath: "/tmp/a.md" },
|
|
219
|
+
],
|
|
220
|
+
commands: [],
|
|
221
|
+
skills: [
|
|
222
|
+
{
|
|
223
|
+
name: "existing-skill",
|
|
224
|
+
description: "Pass-through skill",
|
|
225
|
+
sourceDir: "/tmp/plugin/skills/existing-skill",
|
|
226
|
+
skillPath: "/tmp/plugin/skills/existing-skill/SKILL.md",
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
232
|
+
expect(bundle.agentSkills[0].name).toBe("existing-skill-2")
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test("agent skill and command with same normalized name are NOT deduplicated (separate sets)", () => {
|
|
236
|
+
const plugin: ClaudePlugin = {
|
|
237
|
+
...fixturePlugin,
|
|
238
|
+
agents: [
|
|
239
|
+
{ name: "review", description: "Agent", body: "Body.", sourcePath: "/tmp/a.md" },
|
|
240
|
+
],
|
|
241
|
+
commands: [
|
|
242
|
+
{ name: "review", description: "Command", body: "Body.", sourcePath: "/tmp/b.md" },
|
|
243
|
+
],
|
|
244
|
+
skills: [],
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
248
|
+
expect(bundle.agentSkills[0].name).toBe("review")
|
|
249
|
+
expect(bundle.commandWorkflows[0].name).toBe("review")
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test("large agent skill does not emit 12K character limit warning (skills have no limit)", () => {
|
|
253
|
+
const warnings: string[] = []
|
|
254
|
+
const originalWarn = console.warn
|
|
255
|
+
console.warn = (msg: string) => warnings.push(msg)
|
|
256
|
+
|
|
257
|
+
const plugin: ClaudePlugin = {
|
|
258
|
+
...fixturePlugin,
|
|
259
|
+
agents: [
|
|
260
|
+
{
|
|
261
|
+
name: "large-agent",
|
|
262
|
+
description: "Large agent",
|
|
263
|
+
body: "x".repeat(12_000),
|
|
264
|
+
sourcePath: "/tmp/a.md",
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
commands: [],
|
|
268
|
+
skills: [],
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
convertClaudeToWindsurf(plugin, defaultOptions)
|
|
272
|
+
console.warn = originalWarn
|
|
273
|
+
|
|
274
|
+
expect(warnings.some((w) => w.includes("12000") || w.includes("limit"))).toBe(false)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test("hooks present emits console.warn", () => {
|
|
278
|
+
const warnings: string[] = []
|
|
279
|
+
const originalWarn = console.warn
|
|
280
|
+
console.warn = (msg: string) => warnings.push(msg)
|
|
281
|
+
|
|
282
|
+
const plugin: ClaudePlugin = {
|
|
283
|
+
...fixturePlugin,
|
|
284
|
+
hooks: { hooks: { PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: "echo test" }] }] } },
|
|
285
|
+
agents: [],
|
|
286
|
+
commands: [],
|
|
287
|
+
skills: [],
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
convertClaudeToWindsurf(plugin, defaultOptions)
|
|
291
|
+
console.warn = originalWarn
|
|
292
|
+
|
|
293
|
+
expect(warnings.some((w) => w.includes("Windsurf"))).toBe(true)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test("empty plugin produces empty bundle with null mcpConfig", () => {
|
|
297
|
+
const plugin: ClaudePlugin = {
|
|
298
|
+
root: "/tmp/empty",
|
|
299
|
+
manifest: { name: "empty", version: "1.0.0" },
|
|
300
|
+
agents: [],
|
|
301
|
+
commands: [],
|
|
302
|
+
skills: [],
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
306
|
+
expect(bundle.agentSkills).toHaveLength(0)
|
|
307
|
+
expect(bundle.commandWorkflows).toHaveLength(0)
|
|
308
|
+
expect(bundle.skillDirs).toHaveLength(0)
|
|
309
|
+
expect(bundle.mcpConfig).toBeNull()
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
// MCP config tests
|
|
313
|
+
|
|
314
|
+
test("stdio server produces correct mcpConfig JSON structure", () => {
|
|
315
|
+
const bundle = convertClaudeToWindsurf(fixturePlugin, defaultOptions)
|
|
316
|
+
expect(bundle.mcpConfig).not.toBeNull()
|
|
317
|
+
expect(bundle.mcpConfig!.mcpServers.local).toEqual({
|
|
318
|
+
command: "echo",
|
|
319
|
+
args: ["hello"],
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test("stdio server with env vars includes actual values (not redacted)", () => {
|
|
324
|
+
const plugin: ClaudePlugin = {
|
|
325
|
+
...fixturePlugin,
|
|
326
|
+
mcpServers: {
|
|
327
|
+
myserver: {
|
|
328
|
+
command: "serve",
|
|
329
|
+
env: {
|
|
330
|
+
API_KEY: "secret123",
|
|
331
|
+
PORT: "3000",
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
agents: [],
|
|
336
|
+
commands: [],
|
|
337
|
+
skills: [],
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
341
|
+
expect(bundle.mcpConfig!.mcpServers.myserver.env).toEqual({
|
|
342
|
+
API_KEY: "secret123",
|
|
343
|
+
PORT: "3000",
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test("HTTP/SSE server produces correct mcpConfig with serverUrl", () => {
|
|
348
|
+
const plugin: ClaudePlugin = {
|
|
349
|
+
...fixturePlugin,
|
|
350
|
+
mcpServers: {
|
|
351
|
+
remote: { url: "https://example.com/mcp", headers: { Authorization: "Bearer abc" } },
|
|
352
|
+
},
|
|
353
|
+
agents: [],
|
|
354
|
+
commands: [],
|
|
355
|
+
skills: [],
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
359
|
+
expect(bundle.mcpConfig!.mcpServers.remote).toEqual({
|
|
360
|
+
serverUrl: "https://example.com/mcp",
|
|
361
|
+
headers: { Authorization: "Bearer abc" },
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test("mixed stdio and HTTP servers both included", () => {
|
|
366
|
+
const plugin: ClaudePlugin = {
|
|
367
|
+
...fixturePlugin,
|
|
368
|
+
mcpServers: {
|
|
369
|
+
local: { command: "echo", args: ["hello"] },
|
|
370
|
+
remote: { url: "https://example.com/mcp" },
|
|
371
|
+
},
|
|
372
|
+
agents: [],
|
|
373
|
+
commands: [],
|
|
374
|
+
skills: [],
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
378
|
+
expect(Object.keys(bundle.mcpConfig!.mcpServers)).toHaveLength(2)
|
|
379
|
+
expect(bundle.mcpConfig!.mcpServers.local.command).toBe("echo")
|
|
380
|
+
expect(bundle.mcpConfig!.mcpServers.remote.serverUrl).toBe("https://example.com/mcp")
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
test("hasPotentialSecrets emits console.warn for sensitive env keys", () => {
|
|
384
|
+
const warnings: string[] = []
|
|
385
|
+
const originalWarn = console.warn
|
|
386
|
+
console.warn = (...msgs: unknown[]) => warnings.push(msgs.map(String).join(" "))
|
|
387
|
+
|
|
388
|
+
const plugin: ClaudePlugin = {
|
|
389
|
+
...fixturePlugin,
|
|
390
|
+
mcpServers: {
|
|
391
|
+
myserver: {
|
|
392
|
+
command: "serve",
|
|
393
|
+
env: { API_KEY: "secret123", PORT: "3000" },
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
agents: [],
|
|
397
|
+
commands: [],
|
|
398
|
+
skills: [],
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
convertClaudeToWindsurf(plugin, defaultOptions)
|
|
402
|
+
console.warn = originalWarn
|
|
403
|
+
|
|
404
|
+
expect(warnings.some((w) => w.includes("secrets") && w.includes("myserver"))).toBe(true)
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test("no secrets warning when env vars are safe", () => {
|
|
408
|
+
const warnings: string[] = []
|
|
409
|
+
const originalWarn = console.warn
|
|
410
|
+
console.warn = (...msgs: unknown[]) => warnings.push(msgs.map(String).join(" "))
|
|
411
|
+
|
|
412
|
+
const plugin: ClaudePlugin = {
|
|
413
|
+
...fixturePlugin,
|
|
414
|
+
mcpServers: {
|
|
415
|
+
myserver: {
|
|
416
|
+
command: "serve",
|
|
417
|
+
env: { PORT: "3000", HOST: "localhost" },
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
agents: [],
|
|
421
|
+
commands: [],
|
|
422
|
+
skills: [],
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
convertClaudeToWindsurf(plugin, defaultOptions)
|
|
426
|
+
console.warn = originalWarn
|
|
427
|
+
|
|
428
|
+
expect(warnings.some((w) => w.includes("secrets"))).toBe(false)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
test("no MCP servers produces null mcpConfig", () => {
|
|
432
|
+
const plugin: ClaudePlugin = {
|
|
433
|
+
...fixturePlugin,
|
|
434
|
+
mcpServers: undefined,
|
|
435
|
+
agents: [],
|
|
436
|
+
commands: [],
|
|
437
|
+
skills: [],
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
441
|
+
expect(bundle.mcpConfig).toBeNull()
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
test("server with no command and no URL is skipped with warning", () => {
|
|
445
|
+
const warnings: string[] = []
|
|
446
|
+
const originalWarn = console.warn
|
|
447
|
+
console.warn = (...msgs: unknown[]) => warnings.push(msgs.map(String).join(" "))
|
|
448
|
+
|
|
449
|
+
const plugin: ClaudePlugin = {
|
|
450
|
+
...fixturePlugin,
|
|
451
|
+
mcpServers: {
|
|
452
|
+
broken: {} as { command: string },
|
|
453
|
+
},
|
|
454
|
+
agents: [],
|
|
455
|
+
commands: [],
|
|
456
|
+
skills: [],
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
460
|
+
console.warn = originalWarn
|
|
461
|
+
|
|
462
|
+
expect(bundle.mcpConfig).toBeNull()
|
|
463
|
+
expect(warnings.some((w) => w.includes("broken") && w.includes("no command or URL"))).toBe(true)
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
test("server command without args omits args field", () => {
|
|
467
|
+
const plugin: ClaudePlugin = {
|
|
468
|
+
...fixturePlugin,
|
|
469
|
+
mcpServers: {
|
|
470
|
+
simple: { command: "myserver" },
|
|
471
|
+
},
|
|
472
|
+
agents: [],
|
|
473
|
+
commands: [],
|
|
474
|
+
skills: [],
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const bundle = convertClaudeToWindsurf(plugin, defaultOptions)
|
|
478
|
+
expect(bundle.mcpConfig!.mcpServers.simple).toEqual({ command: "myserver" })
|
|
479
|
+
expect(bundle.mcpConfig!.mcpServers.simple.args).toBeUndefined()
|
|
480
|
+
})
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
describe("transformContentForWindsurf", () => {
|
|
484
|
+
test("transforms .claude/ paths to .windsurf/", () => {
|
|
485
|
+
const result = transformContentForWindsurf("Read .claude/settings.json for config.")
|
|
486
|
+
expect(result).toContain(".windsurf/settings.json")
|
|
487
|
+
expect(result).not.toContain(".claude/")
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
test("transforms ~/.claude/ paths to ~/.codeium/windsurf/", () => {
|
|
491
|
+
const result = transformContentForWindsurf("Check ~/.claude/config for settings.")
|
|
492
|
+
expect(result).toContain("~/.codeium/windsurf/config")
|
|
493
|
+
expect(result).not.toContain("~/.claude/")
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
test("transforms Task agent(args) to skill reference", () => {
|
|
497
|
+
const input = `Run these:
|
|
498
|
+
|
|
499
|
+
- Task repo-research-analyst(feature_description)
|
|
500
|
+
- Task learnings-researcher(feature_description)
|
|
501
|
+
|
|
502
|
+
Task best-practices-researcher(topic)`
|
|
503
|
+
|
|
504
|
+
const result = transformContentForWindsurf(input)
|
|
505
|
+
expect(result).toContain("Use the @repo-research-analyst skill: feature_description")
|
|
506
|
+
expect(result).toContain("Use the @learnings-researcher skill: feature_description")
|
|
507
|
+
expect(result).toContain("Use the @best-practices-researcher skill: topic")
|
|
508
|
+
expect(result).not.toContain("Task repo-research-analyst")
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
test("keeps @agent references as-is for known agents (Windsurf skill invocation syntax)", () => {
|
|
512
|
+
const result = transformContentForWindsurf("Ask @security-sentinel for a review.", ["security-sentinel"])
|
|
513
|
+
expect(result).toContain("@security-sentinel")
|
|
514
|
+
expect(result).not.toContain("/agents/")
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
test("does not transform @unknown-name when not in known agents", () => {
|
|
518
|
+
const result = transformContentForWindsurf("Contact @someone-else for help.", ["security-sentinel"])
|
|
519
|
+
expect(result).toContain("@someone-else")
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
test("transforms slash command refs to /{workflow-name} (per spec)", () => {
|
|
523
|
+
const result = transformContentForWindsurf("Run /workflows:plan to start planning.")
|
|
524
|
+
expect(result).toContain("/workflows-plan")
|
|
525
|
+
expect(result).not.toContain("/commands/")
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
test("does not transform partial .claude paths in middle of word", () => {
|
|
529
|
+
const result = transformContentForWindsurf("Check some-package/.claude-config/settings")
|
|
530
|
+
expect(result).toContain("some-package/")
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
test("handles case sensitivity in @agent-name matching", () => {
|
|
534
|
+
const result = transformContentForWindsurf("Delegate to @My-Agent for help.", ["my-agent"])
|
|
535
|
+
// @My-Agent won't match my-agent since regex is case-sensitive on the known names
|
|
536
|
+
expect(result).toContain("@My-Agent")
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
test("handles multiple occurrences of same transform", () => {
|
|
540
|
+
const result = transformContentForWindsurf(
|
|
541
|
+
"Use .claude/foo and .claude/bar for config.",
|
|
542
|
+
)
|
|
543
|
+
expect(result).toContain(".windsurf/foo")
|
|
544
|
+
expect(result).toContain(".windsurf/bar")
|
|
545
|
+
expect(result).not.toContain(".claude/")
|
|
546
|
+
})
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
describe("normalizeName", () => {
|
|
550
|
+
test("lowercases and hyphenates spaces", () => {
|
|
551
|
+
expect(normalizeName("Security Reviewer")).toBe("security-reviewer")
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
test("replaces colons with hyphens", () => {
|
|
555
|
+
expect(normalizeName("workflows:plan")).toBe("workflows-plan")
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
test("collapses consecutive hyphens", () => {
|
|
559
|
+
expect(normalizeName("agent--with--double-hyphens")).toBe("agent-with-double-hyphens")
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
test("strips leading/trailing hyphens", () => {
|
|
563
|
+
expect(normalizeName("-leading-and-trailing-")).toBe("leading-and-trailing")
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
test("empty string returns item", () => {
|
|
567
|
+
expect(normalizeName("")).toBe("item")
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
test("non-letter start returns item", () => {
|
|
571
|
+
expect(normalizeName("123-agent")).toBe("item")
|
|
572
|
+
})
|
|
573
|
+
})
|