@every-env/compound-plugin 0.9.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.
Files changed (87) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +5 -1
  3. package/CHANGELOG.md +42 -0
  4. package/CLAUDE.md +3 -3
  5. package/README.md +49 -15
  6. package/docs/plans/2026-02-14-feat-auto-detect-install-and-gemini-sync-plan.md +360 -0
  7. package/docs/plans/2026-02-25-feat-windsurf-global-scope-support-plan.md +627 -0
  8. package/docs/plans/2026-03-01-feat-ce-command-aliases-backwards-compatible-deprecation-plan.md +261 -0
  9. package/docs/plans/feature_opencode-commands-as-md-and-config-merge.md +574 -0
  10. package/docs/solutions/adding-converter-target-providers.md +692 -0
  11. package/docs/solutions/plugin-versioning-requirements.md +3 -3
  12. package/docs/specs/windsurf.md +477 -0
  13. package/package.json +1 -1
  14. package/plans/landing-page-launchkit-refresh.md +2 -2
  15. package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
  16. package/plugins/compound-engineering/CHANGELOG.md +72 -1
  17. package/plugins/compound-engineering/CLAUDE.md +9 -7
  18. package/plugins/compound-engineering/README.md +10 -7
  19. package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
  20. package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
  21. package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
  22. package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
  23. package/plugins/compound-engineering/commands/ce/compound.md +240 -0
  24. package/plugins/compound-engineering/commands/ce/plan.md +636 -0
  25. package/plugins/compound-engineering/commands/ce/review.md +525 -0
  26. package/plugins/compound-engineering/commands/ce/work.md +470 -0
  27. package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
  28. package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
  29. package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
  30. package/plugins/compound-engineering/commands/feature-video.md +15 -6
  31. package/plugins/compound-engineering/commands/heal-skill.md +1 -1
  32. package/plugins/compound-engineering/commands/lfg.md +3 -3
  33. package/plugins/compound-engineering/commands/slfg.md +3 -3
  34. package/plugins/compound-engineering/commands/test-xcode.md +2 -2
  35. package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
  36. package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
  37. package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
  38. package/plugins/compound-engineering/commands/workflows/review.md +4 -522
  39. package/plugins/compound-engineering/commands/workflows/work.md +4 -448
  40. package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
  41. package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
  42. package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
  43. package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
  44. package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
  45. package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
  46. package/plugins/compound-engineering/skills/setup/SKILL.md +2 -2
  47. package/src/commands/convert.ts +101 -24
  48. package/src/commands/install.ts +102 -45
  49. package/src/commands/sync.ts +58 -38
  50. package/src/converters/claude-to-openclaw.ts +240 -0
  51. package/src/converters/claude-to-opencode.ts +12 -10
  52. package/src/converters/claude-to-qwen.ts +238 -0
  53. package/src/converters/claude-to-windsurf.ts +205 -0
  54. package/src/sync/gemini.ts +76 -0
  55. package/src/targets/index.ts +60 -1
  56. package/src/targets/openclaw.ts +96 -0
  57. package/src/targets/opencode.ts +76 -10
  58. package/src/targets/qwen.ts +64 -0
  59. package/src/targets/windsurf.ts +104 -0
  60. package/src/types/openclaw.ts +52 -0
  61. package/src/types/opencode.ts +7 -8
  62. package/src/types/qwen.ts +48 -0
  63. package/src/types/windsurf.ts +34 -0
  64. package/src/utils/detect-tools.ts +46 -0
  65. package/src/utils/files.ts +7 -0
  66. package/src/utils/resolve-output.ts +50 -0
  67. package/src/utils/secrets.ts +24 -0
  68. package/tests/cli.test.ts +78 -0
  69. package/tests/converter.test.ts +43 -10
  70. package/tests/detect-tools.test.ts +96 -0
  71. package/tests/openclaw-converter.test.ts +200 -0
  72. package/tests/opencode-writer.test.ts +142 -5
  73. package/tests/qwen-converter.test.ts +238 -0
  74. package/tests/resolve-output.test.ts +131 -0
  75. package/tests/sync-gemini.test.ts +106 -0
  76. package/tests/windsurf-converter.test.ts +573 -0
  77. package/tests/windsurf-writer.test.ts +359 -0
  78. package/docs/css/docs.css +0 -675
  79. package/docs/css/style.css +0 -2886
  80. package/docs/index.html +0 -1046
  81. package/docs/js/main.js +0 -225
  82. package/docs/pages/agents.html +0 -649
  83. package/docs/pages/changelog.html +0 -534
  84. package/docs/pages/commands.html +0 -523
  85. package/docs/pages/getting-started.html +0 -582
  86. package/docs/pages/mcp-servers.html +0 -409
  87. package/docs/pages/skills.html +0 -611
@@ -0,0 +1,359 @@
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 { writeWindsurfBundle } from "../src/targets/windsurf"
6
+ import type { WindsurfBundle } from "../src/types/windsurf"
7
+
8
+ async function exists(filePath: string): Promise<boolean> {
9
+ try {
10
+ await fs.access(filePath)
11
+ return true
12
+ } catch {
13
+ return false
14
+ }
15
+ }
16
+
17
+ const emptyBundle: WindsurfBundle = {
18
+ agentSkills: [],
19
+ commandWorkflows: [],
20
+ skillDirs: [],
21
+ mcpConfig: null,
22
+ }
23
+
24
+ describe("writeWindsurfBundle", () => {
25
+ test("creates correct directory structure with all components", async () => {
26
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-test-"))
27
+ const bundle: WindsurfBundle = {
28
+ agentSkills: [
29
+ {
30
+ name: "security-reviewer",
31
+ content: "---\nname: security-reviewer\ndescription: Security-focused agent\n---\n\n# security-reviewer\n\nReview code for vulnerabilities.\n",
32
+ },
33
+ ],
34
+ commandWorkflows: [
35
+ {
36
+ name: "workflows-plan",
37
+ description: "Planning command",
38
+ body: "> Arguments: [FOCUS]\n\nPlan the work.",
39
+ },
40
+ ],
41
+ skillDirs: [
42
+ {
43
+ name: "skill-one",
44
+ sourceDir: path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one"),
45
+ },
46
+ ],
47
+ mcpConfig: {
48
+ mcpServers: {
49
+ local: { command: "echo", args: ["hello"] },
50
+ },
51
+ },
52
+ }
53
+
54
+ await writeWindsurfBundle(tempRoot, bundle)
55
+
56
+ // No AGENTS.md — removed in v0.11.0
57
+ expect(await exists(path.join(tempRoot, "AGENTS.md"))).toBe(false)
58
+
59
+ // Agent skill written as skills/<name>/SKILL.md
60
+ const agentSkillPath = path.join(tempRoot, "skills", "security-reviewer", "SKILL.md")
61
+ expect(await exists(agentSkillPath)).toBe(true)
62
+ const agentContent = await fs.readFile(agentSkillPath, "utf8")
63
+ expect(agentContent).toContain("name: security-reviewer")
64
+ expect(agentContent).toContain("description: Security-focused agent")
65
+ expect(agentContent).toContain("Review code for vulnerabilities.")
66
+
67
+ // No workflows/agents/ or workflows/commands/ subdirectories (flat per spec)
68
+ expect(await exists(path.join(tempRoot, "workflows", "agents"))).toBe(false)
69
+ expect(await exists(path.join(tempRoot, "workflows", "commands"))).toBe(false)
70
+
71
+ // Command workflow flat in outputRoot/workflows/ (per spec)
72
+ const cmdWorkflowPath = path.join(tempRoot, "workflows", "workflows-plan.md")
73
+ expect(await exists(cmdWorkflowPath)).toBe(true)
74
+ const cmdContent = await fs.readFile(cmdWorkflowPath, "utf8")
75
+ expect(cmdContent).toContain("description: Planning command")
76
+ expect(cmdContent).toContain("Plan the work.")
77
+
78
+ // Copied skill directly in outputRoot/skills/
79
+ expect(await exists(path.join(tempRoot, "skills", "skill-one", "SKILL.md"))).toBe(true)
80
+
81
+ // MCP config directly in outputRoot/
82
+ const mcpPath = path.join(tempRoot, "mcp_config.json")
83
+ expect(await exists(mcpPath)).toBe(true)
84
+ const mcpContent = JSON.parse(await fs.readFile(mcpPath, "utf8"))
85
+ expect(mcpContent.mcpServers.local).toEqual({ command: "echo", args: ["hello"] })
86
+ })
87
+
88
+ test("writes directly into outputRoot without nesting", async () => {
89
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-direct-"))
90
+ const bundle: WindsurfBundle = {
91
+ ...emptyBundle,
92
+ agentSkills: [
93
+ {
94
+ name: "reviewer",
95
+ content: "---\nname: reviewer\ndescription: A reviewer\n---\n\n# reviewer\n\nReview content.\n",
96
+ },
97
+ ],
98
+ }
99
+
100
+ await writeWindsurfBundle(tempRoot, bundle)
101
+
102
+ // Skill should be directly in outputRoot/skills/reviewer/SKILL.md
103
+ expect(await exists(path.join(tempRoot, "skills", "reviewer", "SKILL.md"))).toBe(true)
104
+ // Should NOT create a .windsurf subdirectory
105
+ expect(await exists(path.join(tempRoot, ".windsurf"))).toBe(false)
106
+ })
107
+
108
+ test("handles empty bundle gracefully", async () => {
109
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-empty-"))
110
+
111
+ await writeWindsurfBundle(tempRoot, emptyBundle)
112
+ expect(await exists(tempRoot)).toBe(true)
113
+ // No mcp_config.json for null mcpConfig
114
+ expect(await exists(path.join(tempRoot, "mcp_config.json"))).toBe(false)
115
+ })
116
+
117
+ test("path traversal in agent skill name is rejected", async () => {
118
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-traversal-"))
119
+ const bundle: WindsurfBundle = {
120
+ ...emptyBundle,
121
+ agentSkills: [
122
+ { name: "../escape", content: "Bad content." },
123
+ ],
124
+ }
125
+
126
+ expect(writeWindsurfBundle(tempRoot, bundle)).rejects.toThrow("unsafe path")
127
+ })
128
+
129
+ test("path traversal in command workflow name is rejected", async () => {
130
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-traversal2-"))
131
+ const bundle: WindsurfBundle = {
132
+ ...emptyBundle,
133
+ commandWorkflows: [
134
+ { name: "../escape", description: "Malicious", body: "Bad content." },
135
+ ],
136
+ }
137
+
138
+ expect(writeWindsurfBundle(tempRoot, bundle)).rejects.toThrow("unsafe path")
139
+ })
140
+
141
+ test("skill directory containment check prevents escape", async () => {
142
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-skill-escape-"))
143
+ const bundle: WindsurfBundle = {
144
+ ...emptyBundle,
145
+ skillDirs: [
146
+ { name: "../escape", sourceDir: "/tmp/fake-skill" },
147
+ ],
148
+ }
149
+
150
+ expect(writeWindsurfBundle(tempRoot, bundle)).rejects.toThrow("unsafe path")
151
+ })
152
+
153
+ test("agent skill files have YAML frontmatter with name and description", async () => {
154
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-fm-"))
155
+ const bundle: WindsurfBundle = {
156
+ ...emptyBundle,
157
+ agentSkills: [
158
+ {
159
+ name: "test-agent",
160
+ content: "---\nname: test-agent\ndescription: Test agent description\n---\n\n# test-agent\n\nDo test things.\n",
161
+ },
162
+ ],
163
+ }
164
+
165
+ await writeWindsurfBundle(tempRoot, bundle)
166
+
167
+ const skillPath = path.join(tempRoot, "skills", "test-agent", "SKILL.md")
168
+ const content = await fs.readFile(skillPath, "utf8")
169
+ expect(content).toContain("---")
170
+ expect(content).toContain("name: test-agent")
171
+ expect(content).toContain("description: Test agent description")
172
+ expect(content).toContain("# test-agent")
173
+ expect(content).toContain("Do test things.")
174
+ })
175
+
176
+ // MCP config merge tests
177
+
178
+ test("writes mcp_config.json to outputRoot", async () => {
179
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-mcp-"))
180
+ const bundle: WindsurfBundle = {
181
+ ...emptyBundle,
182
+ mcpConfig: {
183
+ mcpServers: {
184
+ myserver: { command: "serve", args: ["--port", "3000"] },
185
+ },
186
+ },
187
+ }
188
+
189
+ await writeWindsurfBundle(tempRoot, bundle)
190
+
191
+ const mcpPath = path.join(tempRoot, "mcp_config.json")
192
+ expect(await exists(mcpPath)).toBe(true)
193
+ const content = JSON.parse(await fs.readFile(mcpPath, "utf8"))
194
+ expect(content.mcpServers.myserver.command).toBe("serve")
195
+ expect(content.mcpServers.myserver.args).toEqual(["--port", "3000"])
196
+ })
197
+
198
+ test("merges with existing mcp_config.json preserving user servers", async () => {
199
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-merge-"))
200
+ const mcpPath = path.join(tempRoot, "mcp_config.json")
201
+
202
+ // Write existing config with a user server
203
+ await fs.writeFile(mcpPath, JSON.stringify({
204
+ mcpServers: {
205
+ "user-server": { command: "my-tool", args: ["--flag"] },
206
+ },
207
+ }, null, 2))
208
+
209
+ const bundle: WindsurfBundle = {
210
+ ...emptyBundle,
211
+ mcpConfig: {
212
+ mcpServers: {
213
+ "plugin-server": { command: "plugin-tool" },
214
+ },
215
+ },
216
+ }
217
+
218
+ await writeWindsurfBundle(tempRoot, bundle)
219
+
220
+ const content = JSON.parse(await fs.readFile(mcpPath, "utf8"))
221
+ // Both servers should be present
222
+ expect(content.mcpServers["user-server"].command).toBe("my-tool")
223
+ expect(content.mcpServers["plugin-server"].command).toBe("plugin-tool")
224
+ })
225
+
226
+ test("backs up existing mcp_config.json before overwrite", async () => {
227
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-backup-"))
228
+ const mcpPath = path.join(tempRoot, "mcp_config.json")
229
+
230
+ await fs.writeFile(mcpPath, '{"mcpServers":{}}')
231
+
232
+ const bundle: WindsurfBundle = {
233
+ ...emptyBundle,
234
+ mcpConfig: {
235
+ mcpServers: { new: { command: "new-tool" } },
236
+ },
237
+ }
238
+
239
+ await writeWindsurfBundle(tempRoot, bundle)
240
+
241
+ // A backup file should exist
242
+ const files = await fs.readdir(tempRoot)
243
+ const backupFiles = files.filter((f) => f.startsWith("mcp_config.json.bak."))
244
+ expect(backupFiles.length).toBeGreaterThanOrEqual(1)
245
+ })
246
+
247
+ test("handles corrupted existing mcp_config.json with warning", async () => {
248
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-corrupt-"))
249
+ const mcpPath = path.join(tempRoot, "mcp_config.json")
250
+
251
+ await fs.writeFile(mcpPath, "not valid json{{{")
252
+
253
+ const warnings: string[] = []
254
+ const originalWarn = console.warn
255
+ console.warn = (...msgs: unknown[]) => warnings.push(msgs.map(String).join(" "))
256
+
257
+ const bundle: WindsurfBundle = {
258
+ ...emptyBundle,
259
+ mcpConfig: {
260
+ mcpServers: { new: { command: "new-tool" } },
261
+ },
262
+ }
263
+
264
+ await writeWindsurfBundle(tempRoot, bundle)
265
+ console.warn = originalWarn
266
+
267
+ expect(warnings.some((w) => w.includes("could not be parsed"))).toBe(true)
268
+ const content = JSON.parse(await fs.readFile(mcpPath, "utf8"))
269
+ expect(content.mcpServers.new.command).toBe("new-tool")
270
+ })
271
+
272
+ test("handles existing mcp_config.json with array at root", async () => {
273
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-array-"))
274
+ const mcpPath = path.join(tempRoot, "mcp_config.json")
275
+
276
+ await fs.writeFile(mcpPath, "[1,2,3]")
277
+
278
+ const bundle: WindsurfBundle = {
279
+ ...emptyBundle,
280
+ mcpConfig: {
281
+ mcpServers: { new: { command: "new-tool" } },
282
+ },
283
+ }
284
+
285
+ await writeWindsurfBundle(tempRoot, bundle)
286
+
287
+ const content = JSON.parse(await fs.readFile(mcpPath, "utf8"))
288
+ expect(content.mcpServers.new.command).toBe("new-tool")
289
+ // Array root should be replaced with object
290
+ expect(Array.isArray(content)).toBe(false)
291
+ })
292
+
293
+ test("preserves non-mcpServers keys in existing file", async () => {
294
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-preserve-"))
295
+ const mcpPath = path.join(tempRoot, "mcp_config.json")
296
+
297
+ await fs.writeFile(mcpPath, JSON.stringify({
298
+ customSetting: true,
299
+ version: 2,
300
+ mcpServers: { old: { command: "old-tool" } },
301
+ }, null, 2))
302
+
303
+ const bundle: WindsurfBundle = {
304
+ ...emptyBundle,
305
+ mcpConfig: {
306
+ mcpServers: { new: { command: "new-tool" } },
307
+ },
308
+ }
309
+
310
+ await writeWindsurfBundle(tempRoot, bundle)
311
+
312
+ const content = JSON.parse(await fs.readFile(mcpPath, "utf8"))
313
+ expect(content.customSetting).toBe(true)
314
+ expect(content.version).toBe(2)
315
+ expect(content.mcpServers.new.command).toBe("new-tool")
316
+ expect(content.mcpServers.old.command).toBe("old-tool")
317
+ })
318
+
319
+ test("server name collision: plugin entry wins", async () => {
320
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-collision-"))
321
+ const mcpPath = path.join(tempRoot, "mcp_config.json")
322
+
323
+ await fs.writeFile(mcpPath, JSON.stringify({
324
+ mcpServers: { shared: { command: "old-version" } },
325
+ }, null, 2))
326
+
327
+ const bundle: WindsurfBundle = {
328
+ ...emptyBundle,
329
+ mcpConfig: {
330
+ mcpServers: { shared: { command: "new-version" } },
331
+ },
332
+ }
333
+
334
+ await writeWindsurfBundle(tempRoot, bundle)
335
+
336
+ const content = JSON.parse(await fs.readFile(mcpPath, "utf8"))
337
+ expect(content.mcpServers.shared.command).toBe("new-version")
338
+ })
339
+
340
+ test("mcp_config.json written with restrictive permissions", async () => {
341
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "windsurf-perms-"))
342
+ const bundle: WindsurfBundle = {
343
+ ...emptyBundle,
344
+ mcpConfig: {
345
+ mcpServers: { server: { command: "tool" } },
346
+ },
347
+ }
348
+
349
+ await writeWindsurfBundle(tempRoot, bundle)
350
+
351
+ const mcpPath = path.join(tempRoot, "mcp_config.json")
352
+ const stat = await fs.stat(mcpPath)
353
+ // On Unix: 0o600 = owner read+write only. On Windows, permissions work differently.
354
+ if (process.platform !== "win32") {
355
+ const mode = stat.mode & 0o777
356
+ expect(mode).toBe(0o600)
357
+ }
358
+ })
359
+ })