@every-env/compound-plugin 0.9.0 → 2.34.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/.github/workflows/publish.yml +20 -10
- package/.releaserc.json +31 -0
- package/AGENTS.md +6 -1
- package/CHANGELOG.md +76 -0
- package/CLAUDE.md +16 -3
- package/README.md +83 -16
- package/bun.lock +977 -0
- package/docs/plans/2026-02-14-feat-auto-detect-install-and-gemini-sync-plan.md +360 -0
- package/docs/plans/2026-02-25-feat-windsurf-global-scope-support-plan.md +627 -0
- package/docs/plans/2026-03-01-feat-ce-command-aliases-backwards-compatible-deprecation-plan.md +261 -0
- package/docs/plans/2026-03-01-fix-setup-skill-non-claude-llm-fallback-plan.md +140 -0
- package/docs/plans/2026-03-03-feat-sync-claude-mcp-all-supported-providers-plan.md +639 -0
- package/docs/plans/feature_opencode-commands-as-md-and-config-merge.md +574 -0
- package/docs/solutions/adding-converter-target-providers.md +693 -0
- package/docs/solutions/plugin-versioning-requirements.md +7 -3
- package/docs/specs/windsurf.md +477 -0
- package/package.json +10 -4
- package/plans/landing-page-launchkit-refresh.md +2 -2
- package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
- package/plugins/compound-engineering/CHANGELOG.md +82 -1
- package/plugins/compound-engineering/CLAUDE.md +14 -7
- package/plugins/compound-engineering/README.md +10 -7
- package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
- package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
- package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
- package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
- package/plugins/compound-engineering/commands/ce/compound.md +240 -0
- package/plugins/compound-engineering/commands/ce/plan.md +636 -0
- package/plugins/compound-engineering/commands/ce/review.md +525 -0
- package/plugins/compound-engineering/commands/ce/work.md +470 -0
- package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
- package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
- package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
- package/plugins/compound-engineering/commands/feature-video.md +15 -6
- package/plugins/compound-engineering/commands/heal-skill.md +1 -1
- package/plugins/compound-engineering/commands/lfg.md +3 -3
- package/plugins/compound-engineering/commands/slfg.md +3 -3
- package/plugins/compound-engineering/commands/test-xcode.md +2 -2
- package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
- package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
- package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
- package/plugins/compound-engineering/commands/workflows/review.md +4 -522
- package/plugins/compound-engineering/commands/workflows/work.md +4 -448
- package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/add-workflow.md +6 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/create-new-skill.md +6 -0
- package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
- package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
- package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/setup/SKILL.md +8 -2
- package/src/commands/convert.ts +101 -24
- package/src/commands/install.ts +102 -45
- package/src/commands/sync.ts +43 -62
- package/src/converters/claude-to-openclaw.ts +240 -0
- package/src/converters/claude-to-opencode.ts +12 -10
- package/src/converters/claude-to-qwen.ts +238 -0
- package/src/converters/claude-to-windsurf.ts +205 -0
- package/src/index.ts +2 -1
- package/src/parsers/claude-home.ts +55 -3
- package/src/sync/codex.ts +38 -62
- package/src/sync/commands.ts +198 -0
- package/src/sync/copilot.ts +14 -36
- package/src/sync/droid.ts +50 -9
- package/src/sync/gemini.ts +135 -0
- package/src/sync/json-config.ts +47 -0
- package/src/sync/kiro.ts +49 -0
- package/src/sync/mcp-transports.ts +19 -0
- package/src/sync/openclaw.ts +18 -0
- package/src/sync/opencode.ts +10 -30
- package/src/sync/pi.ts +12 -36
- package/src/sync/qwen.ts +66 -0
- package/src/sync/registry.ts +141 -0
- package/src/sync/skills.ts +21 -0
- package/src/sync/windsurf.ts +59 -0
- package/src/targets/index.ts +60 -1
- package/src/targets/openclaw.ts +96 -0
- package/src/targets/opencode.ts +76 -10
- package/src/targets/qwen.ts +64 -0
- package/src/targets/windsurf.ts +104 -0
- package/src/types/kiro.ts +3 -1
- package/src/types/openclaw.ts +52 -0
- package/src/types/opencode.ts +7 -8
- package/src/types/qwen.ts +51 -0
- package/src/types/windsurf.ts +35 -0
- package/src/utils/codex-agents.ts +1 -1
- package/src/utils/detect-tools.ts +37 -0
- package/src/utils/files.ts +14 -0
- package/src/utils/resolve-output.ts +50 -0
- package/src/utils/secrets.ts +24 -0
- package/src/utils/symlink.ts +4 -6
- package/tests/claude-home.test.ts +46 -0
- package/tests/cli.test.ts +180 -0
- package/tests/converter.test.ts +43 -10
- package/tests/detect-tools.test.ts +119 -0
- package/tests/openclaw-converter.test.ts +200 -0
- package/tests/opencode-writer.test.ts +142 -5
- package/tests/qwen-converter.test.ts +238 -0
- package/tests/resolve-output.test.ts +131 -0
- package/tests/sync-codex.test.ts +64 -0
- package/tests/sync-copilot.test.ts +60 -4
- package/tests/sync-droid.test.ts +44 -4
- package/tests/sync-gemini.test.ts +160 -0
- package/tests/sync-kiro.test.ts +83 -0
- package/tests/sync-openclaw.test.ts +51 -0
- package/tests/sync-qwen.test.ts +75 -0
- package/tests/sync-windsurf.test.ts +89 -0
- package/tests/windsurf-converter.test.ts +573 -0
- package/tests/windsurf-writer.test.ts +359 -0
- package/docs/css/docs.css +0 -675
- package/docs/css/style.css +0 -2886
- package/docs/index.html +0 -1046
- package/docs/js/main.js +0 -225
- package/docs/pages/agents.html +0 -649
- package/docs/pages/changelog.html +0 -534
- package/docs/pages/commands.html +0 -523
- package/docs/pages/getting-started.html +0 -582
- package/docs/pages/mcp-servers.html +0 -409
- package/docs/pages/skills.html +0 -611
|
@@ -21,6 +21,7 @@ describe("writeOpenCodeBundle", () => {
|
|
|
21
21
|
config: { $schema: "https://opencode.ai/config.json" },
|
|
22
22
|
agents: [{ name: "agent-one", content: "Agent content" }],
|
|
23
23
|
plugins: [{ name: "hook.ts", content: "export {}" }],
|
|
24
|
+
commandFiles: [],
|
|
24
25
|
skillDirs: [
|
|
25
26
|
{
|
|
26
27
|
name: "skill-one",
|
|
@@ -44,6 +45,7 @@ describe("writeOpenCodeBundle", () => {
|
|
|
44
45
|
config: { $schema: "https://opencode.ai/config.json" },
|
|
45
46
|
agents: [{ name: "agent-one", content: "Agent content" }],
|
|
46
47
|
plugins: [],
|
|
48
|
+
commandFiles: [],
|
|
47
49
|
skillDirs: [
|
|
48
50
|
{
|
|
49
51
|
name: "skill-one",
|
|
@@ -68,6 +70,7 @@ describe("writeOpenCodeBundle", () => {
|
|
|
68
70
|
config: { $schema: "https://opencode.ai/config.json" },
|
|
69
71
|
agents: [{ name: "agent-one", content: "Agent content" }],
|
|
70
72
|
plugins: [],
|
|
73
|
+
commandFiles: [],
|
|
71
74
|
skillDirs: [
|
|
72
75
|
{
|
|
73
76
|
name: "skill-one",
|
|
@@ -85,28 +88,35 @@ describe("writeOpenCodeBundle", () => {
|
|
|
85
88
|
expect(await exists(path.join(outputRoot, ".opencode"))).toBe(false)
|
|
86
89
|
})
|
|
87
90
|
|
|
88
|
-
test("
|
|
91
|
+
test("merges plugin config into existing opencode.json without destroying user keys", async () => {
|
|
89
92
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-backup-"))
|
|
90
93
|
const outputRoot = path.join(tempRoot, ".opencode")
|
|
91
94
|
const configPath = path.join(outputRoot, "opencode.json")
|
|
92
95
|
|
|
93
|
-
// Create existing config
|
|
96
|
+
// Create existing config with user keys
|
|
94
97
|
await fs.mkdir(outputRoot, { recursive: true })
|
|
95
98
|
const originalConfig = { $schema: "https://opencode.ai/config.json", custom: "value" }
|
|
96
99
|
await fs.writeFile(configPath, JSON.stringify(originalConfig, null, 2))
|
|
97
100
|
|
|
101
|
+
// Bundle adds mcp server but keeps user's custom key
|
|
98
102
|
const bundle: OpenCodeBundle = {
|
|
99
|
-
config: {
|
|
103
|
+
config: {
|
|
104
|
+
$schema: "https://opencode.ai/config.json",
|
|
105
|
+
mcp: { "plugin-server": { type: "local", command: "uvx", args: ["plugin-srv"] } }
|
|
106
|
+
},
|
|
100
107
|
agents: [],
|
|
101
108
|
plugins: [],
|
|
109
|
+
commandFiles: [],
|
|
102
110
|
skillDirs: [],
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
await writeOpenCodeBundle(outputRoot, bundle)
|
|
106
114
|
|
|
107
|
-
//
|
|
115
|
+
// Merged config should have both user key and plugin key
|
|
108
116
|
const newConfig = JSON.parse(await fs.readFile(configPath, "utf8"))
|
|
109
|
-
expect(newConfig.
|
|
117
|
+
expect(newConfig.custom).toBe("value") // user key preserved
|
|
118
|
+
expect(newConfig.mcp).toBeDefined()
|
|
119
|
+
expect(newConfig.mcp["plugin-server"]).toBeDefined()
|
|
110
120
|
|
|
111
121
|
// Backup should exist with original content
|
|
112
122
|
const files = await fs.readdir(outputRoot)
|
|
@@ -116,4 +126,131 @@ describe("writeOpenCodeBundle", () => {
|
|
|
116
126
|
const backupContent = JSON.parse(await fs.readFile(path.join(outputRoot, backupFileName!), "utf8"))
|
|
117
127
|
expect(backupContent.custom).toBe("value")
|
|
118
128
|
})
|
|
129
|
+
|
|
130
|
+
test("merges mcp servers without overwriting user entry", async () => {
|
|
131
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-merge-mcp-"))
|
|
132
|
+
const outputRoot = path.join(tempRoot, ".opencode")
|
|
133
|
+
const configPath = path.join(outputRoot, "opencode.json")
|
|
134
|
+
|
|
135
|
+
// Create existing config with user's mcp server
|
|
136
|
+
await fs.mkdir(outputRoot, { recursive: true })
|
|
137
|
+
const existingConfig = {
|
|
138
|
+
mcp: { "user-server": { type: "local", command: "uvx", args: ["user-srv"] } }
|
|
139
|
+
}
|
|
140
|
+
await fs.writeFile(configPath, JSON.stringify(existingConfig, null, 2))
|
|
141
|
+
|
|
142
|
+
// Bundle adds plugin server AND has conflicting user-server with different args
|
|
143
|
+
const bundle: OpenCodeBundle = {
|
|
144
|
+
config: {
|
|
145
|
+
$schema: "https://opencode.ai/config.json",
|
|
146
|
+
mcp: {
|
|
147
|
+
"plugin-server": { type: "local", command: "uvx", args: ["plugin-srv"] },
|
|
148
|
+
"user-server": { type: "local", command: "uvx", args: ["plugin-override"] } // conflict
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
agents: [],
|
|
152
|
+
plugins: [],
|
|
153
|
+
commandFiles: [],
|
|
154
|
+
skillDirs: [],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await writeOpenCodeBundle(outputRoot, bundle)
|
|
158
|
+
|
|
159
|
+
// Merged config should have both servers, with user-server keeping user's original args
|
|
160
|
+
const mergedConfig = JSON.parse(await fs.readFile(configPath, "utf8"))
|
|
161
|
+
expect(mergedConfig.mcp).toBeDefined()
|
|
162
|
+
expect(mergedConfig.mcp["plugin-server"]).toBeDefined()
|
|
163
|
+
expect(mergedConfig.mcp["user-server"]).toBeDefined()
|
|
164
|
+
expect(mergedConfig.mcp["user-server"].args[0]).toBe("user-srv") // user wins on conflict
|
|
165
|
+
expect(mergedConfig.mcp["plugin-server"].args[0]).toBe("plugin-srv") // plugin entry present
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test("preserves unrelated user keys when merging opencode.json", async () => {
|
|
169
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-preserve-"))
|
|
170
|
+
const outputRoot = path.join(tempRoot, ".opencode")
|
|
171
|
+
const configPath = path.join(outputRoot, "opencode.json")
|
|
172
|
+
|
|
173
|
+
// Create existing config with multiple user keys
|
|
174
|
+
await fs.mkdir(outputRoot, { recursive: true })
|
|
175
|
+
const existingConfig = {
|
|
176
|
+
model: "my-model",
|
|
177
|
+
theme: "dark",
|
|
178
|
+
mcp: {}
|
|
179
|
+
}
|
|
180
|
+
await fs.writeFile(configPath, JSON.stringify(existingConfig, null, 2))
|
|
181
|
+
|
|
182
|
+
// Bundle adds plugin-specific keys
|
|
183
|
+
const bundle: OpenCodeBundle = {
|
|
184
|
+
config: {
|
|
185
|
+
$schema: "https://opencode.ai/config.json",
|
|
186
|
+
mcp: { "plugin-server": { type: "local", command: "uvx", args: ["plugin-srv"] } },
|
|
187
|
+
permission: { "bash": "allow" }
|
|
188
|
+
},
|
|
189
|
+
agents: [],
|
|
190
|
+
plugins: [],
|
|
191
|
+
commandFiles: [],
|
|
192
|
+
skillDirs: [],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await writeOpenCodeBundle(outputRoot, bundle)
|
|
196
|
+
|
|
197
|
+
// All user keys preserved
|
|
198
|
+
const mergedConfig = JSON.parse(await fs.readFile(configPath, "utf8"))
|
|
199
|
+
expect(mergedConfig.model).toBe("my-model")
|
|
200
|
+
expect(mergedConfig.theme).toBe("dark")
|
|
201
|
+
expect(mergedConfig.mcp["plugin-server"]).toBeDefined()
|
|
202
|
+
expect(mergedConfig.permission["bash"]).toBe("allow")
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test("writes command files as .md in commands/ directory", async () => {
|
|
206
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-cmd-"))
|
|
207
|
+
const outputRoot = path.join(tempRoot, ".config", "opencode")
|
|
208
|
+
const bundle: OpenCodeBundle = {
|
|
209
|
+
config: { $schema: "https://opencode.ai/config.json" },
|
|
210
|
+
agents: [],
|
|
211
|
+
plugins: [],
|
|
212
|
+
commandFiles: [{ name: "my-cmd", content: "---\ndescription: Test\n---\n\nDo something." }],
|
|
213
|
+
skillDirs: [],
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await writeOpenCodeBundle(outputRoot, bundle)
|
|
217
|
+
|
|
218
|
+
const cmdPath = path.join(outputRoot, "commands", "my-cmd.md")
|
|
219
|
+
expect(await exists(cmdPath)).toBe(true)
|
|
220
|
+
|
|
221
|
+
const content = await fs.readFile(cmdPath, "utf8")
|
|
222
|
+
expect(content).toBe("---\ndescription: Test\n---\n\nDo something.\n")
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test("backs up existing command .md file before overwriting", async () => {
|
|
226
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-cmd-backup-"))
|
|
227
|
+
const outputRoot = path.join(tempRoot, ".opencode")
|
|
228
|
+
const commandsDir = path.join(outputRoot, "commands")
|
|
229
|
+
await fs.mkdir(commandsDir, { recursive: true })
|
|
230
|
+
|
|
231
|
+
const cmdPath = path.join(commandsDir, "my-cmd.md")
|
|
232
|
+
await fs.writeFile(cmdPath, "old content\n")
|
|
233
|
+
|
|
234
|
+
const bundle: OpenCodeBundle = {
|
|
235
|
+
config: { $schema: "https://opencode.ai/config.json" },
|
|
236
|
+
agents: [],
|
|
237
|
+
plugins: [],
|
|
238
|
+
commandFiles: [{ name: "my-cmd", content: "---\ndescription: New\n---\n\nNew content." }],
|
|
239
|
+
skillDirs: [],
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await writeOpenCodeBundle(outputRoot, bundle)
|
|
243
|
+
|
|
244
|
+
// New content should be written
|
|
245
|
+
const content = await fs.readFile(cmdPath, "utf8")
|
|
246
|
+
expect(content).toBe("---\ndescription: New\n---\n\nNew content.\n")
|
|
247
|
+
|
|
248
|
+
// Backup should exist
|
|
249
|
+
const files = await fs.readdir(commandsDir)
|
|
250
|
+
const backupFileName = files.find((f) => f.startsWith("my-cmd.md.bak."))
|
|
251
|
+
expect(backupFileName).toBeDefined()
|
|
252
|
+
|
|
253
|
+
const backupContent = await fs.readFile(path.join(commandsDir, backupFileName!), "utf8")
|
|
254
|
+
expect(backupContent).toBe("old content\n")
|
|
255
|
+
})
|
|
119
256
|
})
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { convertClaudeToQwen } from "../src/converters/claude-to-qwen"
|
|
3
|
+
import { parseFrontmatter } from "../src/utils/frontmatter"
|
|
4
|
+
import type { ClaudePlugin } from "../src/types/claude"
|
|
5
|
+
|
|
6
|
+
const fixturePlugin: ClaudePlugin = {
|
|
7
|
+
root: "/tmp/plugin",
|
|
8
|
+
manifest: { name: "compound-engineering", version: "1.2.0", description: "A plugin for engineers" },
|
|
9
|
+
agents: [
|
|
10
|
+
{
|
|
11
|
+
name: "security-sentinel",
|
|
12
|
+
description: "Security-focused agent",
|
|
13
|
+
capabilities: ["Threat modeling", "OWASP"],
|
|
14
|
+
model: "claude-sonnet-4-20250514",
|
|
15
|
+
body: "Focus on vulnerabilities in ~/.claude/settings.",
|
|
16
|
+
sourcePath: "/tmp/plugin/agents/security-sentinel.md",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "brainstorm-agent",
|
|
20
|
+
description: "Creative brainstormer",
|
|
21
|
+
model: "inherit",
|
|
22
|
+
body: "Generate ideas.",
|
|
23
|
+
sourcePath: "/tmp/plugin/agents/brainstorm-agent.md",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
commands: [
|
|
27
|
+
{
|
|
28
|
+
name: "workflows:plan",
|
|
29
|
+
description: "Planning command",
|
|
30
|
+
argumentHint: "[FOCUS]",
|
|
31
|
+
model: "inherit",
|
|
32
|
+
allowedTools: ["Read"],
|
|
33
|
+
body: "Plan the work. Config at ~/.claude/settings.",
|
|
34
|
+
sourcePath: "/tmp/plugin/commands/workflows/plan.md",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "disabled-cmd",
|
|
38
|
+
description: "Disabled",
|
|
39
|
+
model: "inherit",
|
|
40
|
+
allowedTools: [],
|
|
41
|
+
body: "Should be excluded.",
|
|
42
|
+
disableModelInvocation: true,
|
|
43
|
+
sourcePath: "/tmp/plugin/commands/disabled-cmd.md",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
skills: [
|
|
47
|
+
{
|
|
48
|
+
name: "existing-skill",
|
|
49
|
+
description: "Existing skill",
|
|
50
|
+
sourceDir: "/tmp/plugin/skills/existing-skill",
|
|
51
|
+
skillPath: "/tmp/plugin/skills/existing-skill/SKILL.md",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
hooks: undefined,
|
|
55
|
+
mcpServers: {
|
|
56
|
+
local: { command: "npx", args: ["-y", "some-mcp"], env: { API_KEY: "${YOUR_API_KEY}" } },
|
|
57
|
+
remote: { url: "https://mcp.example.com/api", headers: { Authorization: "Bearer token" } },
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const defaultOptions = {
|
|
62
|
+
agentMode: "subagent" as const,
|
|
63
|
+
inferTemperature: false,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("convertClaudeToQwen", () => {
|
|
67
|
+
test("converts agents to yaml format with frontmatter", () => {
|
|
68
|
+
const bundle = convertClaudeToQwen(fixturePlugin, defaultOptions)
|
|
69
|
+
|
|
70
|
+
const agent = bundle.agents.find((a) => a.name === "security-sentinel")
|
|
71
|
+
expect(agent).toBeDefined()
|
|
72
|
+
expect(agent!.format).toBe("yaml")
|
|
73
|
+
const parsed = parseFrontmatter(agent!.content)
|
|
74
|
+
expect(parsed.data.name).toBe("security-sentinel")
|
|
75
|
+
expect(parsed.data.description).toBe("Security-focused agent")
|
|
76
|
+
expect(parsed.data.model).toBe("anthropic/claude-sonnet-4-20250514")
|
|
77
|
+
expect(parsed.body).toContain("Focus on vulnerabilities")
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("agent with inherit model has no model field in frontmatter", () => {
|
|
81
|
+
const bundle = convertClaudeToQwen(fixturePlugin, defaultOptions)
|
|
82
|
+
const agent = bundle.agents.find((a) => a.name === "brainstorm-agent")
|
|
83
|
+
expect(agent).toBeDefined()
|
|
84
|
+
const parsed = parseFrontmatter(agent!.content)
|
|
85
|
+
expect(parsed.data.model).toBeUndefined()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test("inferTemperature injects temperature based on agent name/description", () => {
|
|
89
|
+
const bundle = convertClaudeToQwen(fixturePlugin, { ...defaultOptions, inferTemperature: true })
|
|
90
|
+
|
|
91
|
+
const sentinel = bundle.agents.find((a) => a.name === "security-sentinel")
|
|
92
|
+
const parsed = parseFrontmatter(sentinel!.content)
|
|
93
|
+
expect(parsed.data.temperature).toBe(0.1) // review/security → 0.1
|
|
94
|
+
|
|
95
|
+
const brainstorm = bundle.agents.find((a) => a.name === "brainstorm-agent")
|
|
96
|
+
const bParsed = parseFrontmatter(brainstorm!.content)
|
|
97
|
+
expect(bParsed.data.temperature).toBe(0.6) // brainstorm → 0.6
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test("inferTemperature returns undefined for unrecognized agents (no temperature set)", () => {
|
|
101
|
+
const plugin: ClaudePlugin = {
|
|
102
|
+
...fixturePlugin,
|
|
103
|
+
agents: [{ name: "my-helper", description: "Generic helper", model: "inherit", body: "help", sourcePath: "/tmp/a.md" }],
|
|
104
|
+
}
|
|
105
|
+
const bundle = convertClaudeToQwen(plugin, { ...defaultOptions, inferTemperature: true })
|
|
106
|
+
const agent = bundle.agents[0]
|
|
107
|
+
const parsed = parseFrontmatter(agent.content)
|
|
108
|
+
expect(parsed.data.temperature).toBeUndefined()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("converts commands to command files excluding disableModelInvocation", () => {
|
|
112
|
+
const bundle = convertClaudeToQwen(fixturePlugin, defaultOptions)
|
|
113
|
+
|
|
114
|
+
const planCmd = bundle.commandFiles.find((c) => c.name === "workflows:plan")
|
|
115
|
+
expect(planCmd).toBeDefined()
|
|
116
|
+
const parsed = parseFrontmatter(planCmd!.content)
|
|
117
|
+
expect(parsed.data.description).toBe("Planning command")
|
|
118
|
+
expect(parsed.data.allowedTools).toEqual(["Read"])
|
|
119
|
+
|
|
120
|
+
const disabled = bundle.commandFiles.find((c) => c.name === "disabled-cmd")
|
|
121
|
+
expect(disabled).toBeUndefined()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test("config uses plugin manifest name and version", () => {
|
|
125
|
+
const bundle = convertClaudeToQwen(fixturePlugin, defaultOptions)
|
|
126
|
+
expect(bundle.config.name).toBe("compound-engineering")
|
|
127
|
+
expect(bundle.config.version).toBe("1.2.0")
|
|
128
|
+
expect(bundle.config.commands).toBe("commands")
|
|
129
|
+
expect(bundle.config.skills).toBe("skills")
|
|
130
|
+
expect(bundle.config.agents).toBe("agents")
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test("stdio MCP servers are included in config", () => {
|
|
134
|
+
const bundle = convertClaudeToQwen(fixturePlugin, defaultOptions)
|
|
135
|
+
expect(bundle.config.mcpServers).toBeDefined()
|
|
136
|
+
const local = bundle.config.mcpServers!.local
|
|
137
|
+
expect(local.command).toBe("npx")
|
|
138
|
+
expect(local.args).toEqual(["-y", "some-mcp"])
|
|
139
|
+
// No cwd field
|
|
140
|
+
expect((local as any).cwd).toBeUndefined()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test("remote MCP servers are skipped with a warning (not converted to curl)", () => {
|
|
144
|
+
const bundle = convertClaudeToQwen(fixturePlugin, defaultOptions)
|
|
145
|
+
// Only local (stdio) server should be present
|
|
146
|
+
expect(bundle.config.mcpServers).toBeDefined()
|
|
147
|
+
expect(bundle.config.mcpServers!.remote).toBeUndefined()
|
|
148
|
+
expect(bundle.config.mcpServers!.local).toBeDefined()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test("placeholder env vars are extracted as settings", () => {
|
|
152
|
+
const bundle = convertClaudeToQwen(fixturePlugin, defaultOptions)
|
|
153
|
+
expect(bundle.config.settings).toBeDefined()
|
|
154
|
+
const apiKeySetting = bundle.config.settings!.find((s) => s.envVar === "API_KEY")
|
|
155
|
+
expect(apiKeySetting).toBeDefined()
|
|
156
|
+
expect(apiKeySetting!.sensitive).toBe(true)
|
|
157
|
+
expect(apiKeySetting!.name).toBe("Api Key")
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test("plugin with no MCP servers has no mcpServers in config", () => {
|
|
161
|
+
const plugin: ClaudePlugin = { ...fixturePlugin, mcpServers: undefined }
|
|
162
|
+
const bundle = convertClaudeToQwen(plugin, defaultOptions)
|
|
163
|
+
expect(bundle.config.mcpServers).toBeUndefined()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test("context file uses plugin.manifest.name and manifest.description", () => {
|
|
167
|
+
const bundle = convertClaudeToQwen(fixturePlugin, defaultOptions)
|
|
168
|
+
expect(bundle.contextFile).toContain("# compound-engineering")
|
|
169
|
+
expect(bundle.contextFile).toContain("A plugin for engineers")
|
|
170
|
+
expect(bundle.contextFile).toContain("## Agents")
|
|
171
|
+
expect(bundle.contextFile).toContain("security-sentinel")
|
|
172
|
+
expect(bundle.contextFile).toContain("## Commands")
|
|
173
|
+
expect(bundle.contextFile).toContain("/workflows:plan")
|
|
174
|
+
// Disabled commands excluded
|
|
175
|
+
expect(bundle.contextFile).not.toContain("disabled-cmd")
|
|
176
|
+
expect(bundle.contextFile).toContain("## Skills")
|
|
177
|
+
expect(bundle.contextFile).toContain("existing-skill")
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test("paths are rewritten from .claude/ to .qwen/ in agent and command content", () => {
|
|
181
|
+
const bundle = convertClaudeToQwen(fixturePlugin, defaultOptions)
|
|
182
|
+
|
|
183
|
+
const agent = bundle.agents.find((a) => a.name === "security-sentinel")
|
|
184
|
+
expect(agent!.content).toContain("~/.qwen/settings")
|
|
185
|
+
expect(agent!.content).not.toContain("~/.claude/settings")
|
|
186
|
+
|
|
187
|
+
const cmd = bundle.commandFiles.find((c) => c.name === "workflows:plan")
|
|
188
|
+
expect(cmd!.content).toContain("~/.qwen/settings")
|
|
189
|
+
expect(cmd!.content).not.toContain("~/.claude/settings")
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test("opencode paths are NOT rewritten (only claude paths)", () => {
|
|
193
|
+
const plugin: ClaudePlugin = {
|
|
194
|
+
...fixturePlugin,
|
|
195
|
+
agents: [
|
|
196
|
+
{
|
|
197
|
+
name: "test-agent",
|
|
198
|
+
description: "test",
|
|
199
|
+
model: "inherit",
|
|
200
|
+
body: "See .opencode/config and ~/.config/opencode/settings",
|
|
201
|
+
sourcePath: "/tmp/a.md",
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
}
|
|
205
|
+
const bundle = convertClaudeToQwen(plugin, defaultOptions)
|
|
206
|
+
const agent = bundle.agents[0]
|
|
207
|
+
// opencode paths should NOT be rewritten
|
|
208
|
+
expect(agent.content).toContain(".opencode/config")
|
|
209
|
+
expect(agent.content).not.toContain(".qwen/config")
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test("skillDirs passes through original skills", () => {
|
|
213
|
+
const bundle = convertClaudeToQwen(fixturePlugin, defaultOptions)
|
|
214
|
+
const skill = bundle.skillDirs.find((s) => s.name === "existing-skill")
|
|
215
|
+
expect(skill).toBeDefined()
|
|
216
|
+
expect(skill!.sourceDir).toBe("/tmp/plugin/skills/existing-skill")
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test("normalizeModel prefixes claude models with anthropic/", () => {
|
|
220
|
+
const plugin: ClaudePlugin = {
|
|
221
|
+
...fixturePlugin,
|
|
222
|
+
agents: [{ name: "a", description: "d", model: "claude-opus-4-5", body: "b", sourcePath: "/tmp/a.md" }],
|
|
223
|
+
}
|
|
224
|
+
const bundle = convertClaudeToQwen(plugin, defaultOptions)
|
|
225
|
+
const parsed = parseFrontmatter(bundle.agents[0].content)
|
|
226
|
+
expect(parsed.data.model).toBe("anthropic/claude-opus-4-5")
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test("normalizeModel passes through already-namespaced models unchanged", () => {
|
|
230
|
+
const plugin: ClaudePlugin = {
|
|
231
|
+
...fixturePlugin,
|
|
232
|
+
agents: [{ name: "a", description: "d", model: "google/gemini-2.0", body: "b", sourcePath: "/tmp/a.md" }],
|
|
233
|
+
}
|
|
234
|
+
const bundle = convertClaudeToQwen(plugin, defaultOptions)
|
|
235
|
+
const parsed = parseFrontmatter(bundle.agents[0].content)
|
|
236
|
+
expect(parsed.data.model).toBe("google/gemini-2.0")
|
|
237
|
+
})
|
|
238
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import os from "os"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import { resolveTargetOutputRoot } from "../src/utils/resolve-output"
|
|
5
|
+
|
|
6
|
+
const baseOptions = {
|
|
7
|
+
outputRoot: "/tmp/output",
|
|
8
|
+
codexHome: path.join(os.homedir(), ".codex"),
|
|
9
|
+
piHome: path.join(os.homedir(), ".pi", "agent"),
|
|
10
|
+
hasExplicitOutput: false,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("resolveTargetOutputRoot", () => {
|
|
14
|
+
test("codex returns codexHome", () => {
|
|
15
|
+
const result = resolveTargetOutputRoot({ ...baseOptions, targetName: "codex" })
|
|
16
|
+
expect(result).toBe(baseOptions.codexHome)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test("pi returns piHome", () => {
|
|
20
|
+
const result = resolveTargetOutputRoot({ ...baseOptions, targetName: "pi" })
|
|
21
|
+
expect(result).toBe(baseOptions.piHome)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("droid returns ~/.factory", () => {
|
|
25
|
+
const result = resolveTargetOutputRoot({ ...baseOptions, targetName: "droid" })
|
|
26
|
+
expect(result).toBe(path.join(os.homedir(), ".factory"))
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test("cursor with no explicit output uses cwd", () => {
|
|
30
|
+
const result = resolveTargetOutputRoot({ ...baseOptions, targetName: "cursor" })
|
|
31
|
+
expect(result).toBe(path.join(process.cwd(), ".cursor"))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test("cursor with explicit output uses outputRoot", () => {
|
|
35
|
+
const result = resolveTargetOutputRoot({
|
|
36
|
+
...baseOptions,
|
|
37
|
+
targetName: "cursor",
|
|
38
|
+
hasExplicitOutput: true,
|
|
39
|
+
})
|
|
40
|
+
expect(result).toBe(path.join("/tmp/output", ".cursor"))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test("windsurf default scope (global) resolves to ~/.codeium/windsurf/", () => {
|
|
44
|
+
const result = resolveTargetOutputRoot({
|
|
45
|
+
...baseOptions,
|
|
46
|
+
targetName: "windsurf",
|
|
47
|
+
scope: "global",
|
|
48
|
+
})
|
|
49
|
+
expect(result).toBe(path.join(os.homedir(), ".codeium", "windsurf"))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test("windsurf workspace scope resolves to cwd/.windsurf/", () => {
|
|
53
|
+
const result = resolveTargetOutputRoot({
|
|
54
|
+
...baseOptions,
|
|
55
|
+
targetName: "windsurf",
|
|
56
|
+
scope: "workspace",
|
|
57
|
+
})
|
|
58
|
+
expect(result).toBe(path.join(process.cwd(), ".windsurf"))
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test("windsurf with explicit output overrides global scope", () => {
|
|
62
|
+
const result = resolveTargetOutputRoot({
|
|
63
|
+
...baseOptions,
|
|
64
|
+
targetName: "windsurf",
|
|
65
|
+
hasExplicitOutput: true,
|
|
66
|
+
scope: "global",
|
|
67
|
+
})
|
|
68
|
+
expect(result).toBe("/tmp/output")
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test("windsurf with explicit output overrides workspace scope", () => {
|
|
72
|
+
const result = resolveTargetOutputRoot({
|
|
73
|
+
...baseOptions,
|
|
74
|
+
targetName: "windsurf",
|
|
75
|
+
hasExplicitOutput: true,
|
|
76
|
+
scope: "workspace",
|
|
77
|
+
})
|
|
78
|
+
expect(result).toBe("/tmp/output")
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test("windsurf with no scope and no explicit output uses cwd/.windsurf/", () => {
|
|
82
|
+
const result = resolveTargetOutputRoot({
|
|
83
|
+
...baseOptions,
|
|
84
|
+
targetName: "windsurf",
|
|
85
|
+
})
|
|
86
|
+
expect(result).toBe(path.join(process.cwd(), ".windsurf"))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test("opencode returns outputRoot as-is", () => {
|
|
90
|
+
const result = resolveTargetOutputRoot({ ...baseOptions, targetName: "opencode" })
|
|
91
|
+
expect(result).toBe("/tmp/output")
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("openclaw uses openclawHome + pluginName", () => {
|
|
95
|
+
const result = resolveTargetOutputRoot({
|
|
96
|
+
...baseOptions,
|
|
97
|
+
targetName: "openclaw",
|
|
98
|
+
openclawHome: "/custom/openclaw/extensions",
|
|
99
|
+
pluginName: "my-plugin",
|
|
100
|
+
})
|
|
101
|
+
expect(result).toBe("/custom/openclaw/extensions/my-plugin")
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test("openclaw falls back to default home when not provided", () => {
|
|
105
|
+
const result = resolveTargetOutputRoot({
|
|
106
|
+
...baseOptions,
|
|
107
|
+
targetName: "openclaw",
|
|
108
|
+
pluginName: "my-plugin",
|
|
109
|
+
})
|
|
110
|
+
expect(result).toBe(path.join(os.homedir(), ".openclaw", "extensions", "my-plugin"))
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test("qwen uses qwenHome + pluginName", () => {
|
|
114
|
+
const result = resolveTargetOutputRoot({
|
|
115
|
+
...baseOptions,
|
|
116
|
+
targetName: "qwen",
|
|
117
|
+
qwenHome: "/custom/qwen/extensions",
|
|
118
|
+
pluginName: "my-plugin",
|
|
119
|
+
})
|
|
120
|
+
expect(result).toBe("/custom/qwen/extensions/my-plugin")
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test("qwen falls back to default home when not provided", () => {
|
|
124
|
+
const result = resolveTargetOutputRoot({
|
|
125
|
+
...baseOptions,
|
|
126
|
+
targetName: "qwen",
|
|
127
|
+
pluginName: "my-plugin",
|
|
128
|
+
})
|
|
129
|
+
expect(result).toBe(path.join(os.homedir(), ".qwen", "extensions", "my-plugin"))
|
|
130
|
+
})
|
|
131
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
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 { syncToCodex } from "../src/sync/codex"
|
|
7
|
+
|
|
8
|
+
describe("syncToCodex", () => {
|
|
9
|
+
test("writes stdio and remote MCP servers into a managed block without clobbering user config", async () => {
|
|
10
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "sync-codex-"))
|
|
11
|
+
const fixtureSkillDir = path.join(import.meta.dir, "fixtures", "sample-plugin", "skills", "skill-one")
|
|
12
|
+
const configPath = path.join(tempRoot, "config.toml")
|
|
13
|
+
|
|
14
|
+
await fs.writeFile(
|
|
15
|
+
configPath,
|
|
16
|
+
[
|
|
17
|
+
"[custom]",
|
|
18
|
+
"enabled = true",
|
|
19
|
+
"",
|
|
20
|
+
"# BEGIN compound-plugin Claude Code MCP",
|
|
21
|
+
"[mcp_servers.old]",
|
|
22
|
+
"command = \"old\"",
|
|
23
|
+
"# END compound-plugin Claude Code MCP",
|
|
24
|
+
"",
|
|
25
|
+
"[post]",
|
|
26
|
+
"value = 2",
|
|
27
|
+
"",
|
|
28
|
+
].join("\n"),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const config: ClaudeHomeConfig = {
|
|
32
|
+
skills: [
|
|
33
|
+
{
|
|
34
|
+
name: "skill-one",
|
|
35
|
+
sourceDir: fixtureSkillDir,
|
|
36
|
+
skillPath: path.join(fixtureSkillDir, "SKILL.md"),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
mcpServers: {
|
|
40
|
+
local: { command: "echo", args: ["hello"], env: { KEY: "VALUE" } },
|
|
41
|
+
remote: { url: "https://example.com/mcp", headers: { Authorization: "Bearer token" } },
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await syncToCodex(config, tempRoot)
|
|
46
|
+
|
|
47
|
+
const skillPath = path.join(tempRoot, "skills", "skill-one")
|
|
48
|
+
expect((await fs.lstat(skillPath)).isSymbolicLink()).toBe(true)
|
|
49
|
+
|
|
50
|
+
const content = await fs.readFile(configPath, "utf8")
|
|
51
|
+
expect(content).toContain("[custom]")
|
|
52
|
+
expect(content).toContain("[post]")
|
|
53
|
+
expect(content).not.toContain("[mcp_servers.old]")
|
|
54
|
+
expect(content).toContain("[mcp_servers.local]")
|
|
55
|
+
expect(content).toContain("command = \"echo\"")
|
|
56
|
+
expect(content).toContain("[mcp_servers.remote]")
|
|
57
|
+
expect(content).toContain("url = \"https://example.com/mcp\"")
|
|
58
|
+
expect(content).toContain("http_headers")
|
|
59
|
+
expect(content.match(/# BEGIN compound-plugin Claude Code MCP/g)?.length).toBe(1)
|
|
60
|
+
|
|
61
|
+
const perms = (await fs.stat(configPath)).mode & 0o777
|
|
62
|
+
expect(perms).toBe(0o600)
|
|
63
|
+
})
|
|
64
|
+
})
|