@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,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
|
+
})
|