@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.
Files changed (93) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +5 -1
  3. package/CHANGELOG.md +50 -0
  4. package/CLAUDE.md +3 -3
  5. package/README.md +52 -14
  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/kiro.md +171 -0
  13. package/docs/specs/windsurf.md +477 -0
  14. package/package.json +1 -1
  15. package/plans/landing-page-launchkit-refresh.md +2 -2
  16. package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
  17. package/plugins/compound-engineering/CHANGELOG.md +72 -1
  18. package/plugins/compound-engineering/CLAUDE.md +9 -7
  19. package/plugins/compound-engineering/README.md +10 -7
  20. package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
  21. package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
  22. package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
  23. package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
  24. package/plugins/compound-engineering/commands/ce/compound.md +240 -0
  25. package/plugins/compound-engineering/commands/ce/plan.md +636 -0
  26. package/plugins/compound-engineering/commands/ce/review.md +525 -0
  27. package/plugins/compound-engineering/commands/ce/work.md +470 -0
  28. package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
  29. package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
  30. package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
  31. package/plugins/compound-engineering/commands/feature-video.md +15 -6
  32. package/plugins/compound-engineering/commands/heal-skill.md +1 -1
  33. package/plugins/compound-engineering/commands/lfg.md +3 -3
  34. package/plugins/compound-engineering/commands/slfg.md +3 -3
  35. package/plugins/compound-engineering/commands/test-xcode.md +2 -2
  36. package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
  37. package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
  38. package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
  39. package/plugins/compound-engineering/commands/workflows/review.md +4 -522
  40. package/plugins/compound-engineering/commands/workflows/work.md +4 -448
  41. package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
  42. package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
  43. package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
  44. package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
  45. package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
  46. package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
  47. package/plugins/compound-engineering/skills/setup/SKILL.md +2 -2
  48. package/src/commands/convert.ts +101 -23
  49. package/src/commands/install.ts +102 -41
  50. package/src/commands/sync.ts +58 -38
  51. package/src/converters/claude-to-kiro.ts +262 -0
  52. package/src/converters/claude-to-openclaw.ts +240 -0
  53. package/src/converters/claude-to-opencode.ts +12 -10
  54. package/src/converters/claude-to-qwen.ts +238 -0
  55. package/src/converters/claude-to-windsurf.ts +205 -0
  56. package/src/sync/gemini.ts +76 -0
  57. package/src/targets/index.ts +69 -1
  58. package/src/targets/kiro.ts +122 -0
  59. package/src/targets/openclaw.ts +96 -0
  60. package/src/targets/opencode.ts +76 -10
  61. package/src/targets/qwen.ts +64 -0
  62. package/src/targets/windsurf.ts +104 -0
  63. package/src/types/kiro.ts +44 -0
  64. package/src/types/openclaw.ts +52 -0
  65. package/src/types/opencode.ts +7 -8
  66. package/src/types/qwen.ts +48 -0
  67. package/src/types/windsurf.ts +34 -0
  68. package/src/utils/detect-tools.ts +46 -0
  69. package/src/utils/files.ts +7 -0
  70. package/src/utils/resolve-output.ts +50 -0
  71. package/src/utils/secrets.ts +24 -0
  72. package/tests/cli.test.ts +78 -0
  73. package/tests/converter.test.ts +43 -10
  74. package/tests/detect-tools.test.ts +96 -0
  75. package/tests/kiro-converter.test.ts +381 -0
  76. package/tests/kiro-writer.test.ts +273 -0
  77. package/tests/openclaw-converter.test.ts +200 -0
  78. package/tests/opencode-writer.test.ts +142 -5
  79. package/tests/qwen-converter.test.ts +238 -0
  80. package/tests/resolve-output.test.ts +131 -0
  81. package/tests/sync-gemini.test.ts +106 -0
  82. package/tests/windsurf-converter.test.ts +573 -0
  83. package/tests/windsurf-writer.test.ts +359 -0
  84. package/docs/css/docs.css +0 -675
  85. package/docs/css/style.css +0 -2886
  86. package/docs/index.html +0 -1046
  87. package/docs/js/main.js +0 -225
  88. package/docs/pages/agents.html +0 -649
  89. package/docs/pages/changelog.html +0 -534
  90. package/docs/pages/commands.html +0 -523
  91. package/docs/pages/getting-started.html +0 -582
  92. package/docs/pages/mcp-servers.html +0 -409
  93. package/docs/pages/skills.html +0 -611
@@ -0,0 +1,692 @@
1
+ ---
2
+ title: Adding New Converter Target Providers
3
+ category: architecture
4
+ tags: [converter, target-provider, plugin-conversion, multi-platform, pattern]
5
+ created: 2026-02-23
6
+ severity: medium
7
+ component: converter-cli
8
+ problem_type: best_practice
9
+ root_cause: architectural_pattern
10
+ ---
11
+
12
+ # Adding New Converter Target Providers
13
+
14
+ ## Problem
15
+
16
+ When adding support for a new AI platform (e.g., Devin, Cursor, Copilot), the converter CLI architecture requires consistent implementation across types, converters, writers, CLI integration, and tests. Without documented patterns and learnings, new targets take longer to implement and risk architectural inconsistency.
17
+
18
+ ## Solution
19
+
20
+ The compound-engineering-plugin uses a proven **6-phase target provider pattern** that has been successfully applied to 8 targets:
21
+
22
+ 1. **OpenCode** (primary target, reference implementation)
23
+ 2. **Codex** (second target, established pattern)
24
+ 3. **Droid/Factory** (workflow/agent conversion)
25
+ 4. **Pi** (MCPorter ecosystem)
26
+ 5. **Gemini CLI** (content transformation patterns)
27
+ 6. **Cursor** (command flattening, rule formats)
28
+ 7. **Copilot** (GitHub native, MCP prefixing)
29
+ 8. **Kiro** (limited MCP support)
30
+ 9. **Devin** (playbook conversion, knowledge entries)
31
+
32
+ Each implementation follows this architecture precisely, ensuring consistency and maintainability.
33
+
34
+ ## Architecture: The 6-Phase Pattern
35
+
36
+ ### Phase 1: Type Definitions (`src/types/{target}.ts`)
37
+
38
+ **Purpose:** Define TypeScript types for the intermediate bundle format
39
+
40
+ **Key Pattern:**
41
+
42
+ ```typescript
43
+ // Exported bundle type used by converter and writer
44
+ export type {TargetName}Bundle = {
45
+ // Component arrays matching the target format
46
+ agents?: {TargetName}Agent[]
47
+ commands?: {TargetName}Command[]
48
+ skillDirs?: {TargetName}SkillDir[]
49
+ mcpServers?: Record<string, {TargetName}McpServer>
50
+ // Target-specific fields
51
+ setup?: string // Instructions file content
52
+ }
53
+
54
+ // Individual component types
55
+ export type {TargetName}Agent = {
56
+ name: string
57
+ content: string // Full file content (with frontmatter if applicable)
58
+ category?: string // e.g., "agent", "rule", "playbook"
59
+ meta?: Record<string, unknown> // Target-specific metadata
60
+ }
61
+ ```
62
+
63
+ **Key Learnings:**
64
+
65
+ - Always include a `content` field (full file text) rather than decomposed fields — it's simpler and matches how files are written
66
+ - Use intermediate types for complex sections (e.g., `DevinPlaybookSections` in Devin converter) to make section building independently testable
67
+ - Avoid target-specific fields in the base bundle unless essential — aim for shared structure across targets
68
+ - Include a `category` field if the target has file-type variants (agents vs. commands vs. rules)
69
+
70
+ **Reference Implementations:**
71
+ - OpenCode: `src/types/opencode.ts` (command + agent split)
72
+ - Devin: `src/types/devin.ts` (playbooks + knowledge entries)
73
+ - Copilot: `src/types/copilot.ts` (agents + skills + MCP)
74
+
75
+ ---
76
+
77
+ ### Phase 2: Converter (`src/converters/claude-to-{target}.ts`)
78
+
79
+ **Purpose:** Transform Claude Code plugin format → target-specific bundle format
80
+
81
+ **Key Pattern:**
82
+
83
+ ```typescript
84
+ export type ClaudeTo{Target}Options = ClaudeToOpenCodeOptions // Reuse common options
85
+
86
+ export function convertClaudeTo{Target}(
87
+ plugin: ClaudePlugin,
88
+ _options: ClaudeTo{Target}Options,
89
+ ): {Target}Bundle {
90
+ // Pre-scan: build maps for cross-reference resolution (agents, commands)
91
+ // Needed if target requires deduplication or reference tracking
92
+ const refMap: Record<string, string> = {}
93
+ for (const agent of plugin.agents) {
94
+ refMap[normalize(agent.name)] = macroName(agent.name)
95
+ }
96
+
97
+ // Phase 1: Convert agents
98
+ const agents = plugin.agents.map(a => convert{Target}Agent(a, usedNames, refMap))
99
+
100
+ // Phase 2: Convert commands (may depend on agent names for dedup)
101
+ const commands = plugin.commands.map(c => convert{Target}Command(c, usedNames, refMap))
102
+
103
+ // Phase 3: Handle skills (usually pass-through, sometimes conversion)
104
+ const skillDirs = plugin.skills.map(s => ({ name: s.name, sourceDir: s.sourceDir }))
105
+
106
+ // Phase 4: Convert MCP servers (target-specific prefixing/type mapping)
107
+ const mcpConfig = convertMcpServers(plugin.mcpServers)
108
+
109
+ // Phase 5: Warn on unsupported features
110
+ if (plugin.hooks && Object.keys(plugin.hooks.hooks).length > 0) {
111
+ console.warn("Warning: {Target} does not support hooks. Hooks were skipped.")
112
+ }
113
+
114
+ return { agents, commands, skillDirs, mcpConfig }
115
+ }
116
+ ```
117
+
118
+ **Content Transformation (`transformContentFor{Target}`):**
119
+
120
+ Applied to both agent bodies and command bodies to rewrite paths, command references, and agent mentions:
121
+
122
+ ```typescript
123
+ export function transformContentFor{Target}(body: string): string {
124
+ let result = body
125
+
126
+ // 1. Rewrite paths (.claude/ → .github/, ~/.claude/ → ~/.{target}/)
127
+ result = result
128
+ .replace(/~\/\.claude\//g, `~/.${targetDir}/`)
129
+ .replace(/\.claude\//g, `.${targetDir}/`)
130
+
131
+ // 2. Transform Task agent calls (to natural language)
132
+ const taskPattern = /Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm
133
+ result = result.replace(taskPattern, (_match, agentName: string, args: string) => {
134
+ const skillName = normalize(agentName)
135
+ return `Use the ${skillName} skill to: ${args.trim()}`
136
+ })
137
+
138
+ // 3. Flatten slash commands (/workflows:plan → /plan)
139
+ const slashPattern = /(?<![:\w])\/([a-z][a-z0-9_:-]*?)(?=[\s,."')\]}`]|$)/gi
140
+ result = result.replace(slashPattern, (match, commandName: string) => {
141
+ if (commandName.includes("/")) return match // Skip file paths
142
+ const normalized = normalize(commandName)
143
+ return `/${normalized}`
144
+ })
145
+
146
+ // 4. Transform @agent-name references
147
+ const agentPattern = /@([a-z][a-z0-9-]*-(?:agent|reviewer|analyst|...))/gi
148
+ result = result.replace(agentPattern, (_match, agentName: string) => {
149
+ return `the ${normalize(agentName)} agent` // or "rule", "playbook", etc.
150
+ })
151
+
152
+ // 5. Remove examples (if target doesn't support them)
153
+ result = result.replace(/<examples>[\s\S]*?<\/examples>/g, "")
154
+
155
+ return result
156
+ }
157
+ ```
158
+
159
+ **Deduplication Pattern (`uniqueName`):**
160
+
161
+ Used when target has flat namespaces (Cursor, Copilot, Devin) or when name collisions occur:
162
+
163
+ ```typescript
164
+ function uniqueName(base: string, used: Set<string>): string {
165
+ if (!used.has(base)) {
166
+ used.add(base)
167
+ return base
168
+ }
169
+ let index = 2
170
+ while (used.has(`${base}-${index}`)) {
171
+ index += 1
172
+ }
173
+ const name = `${base}-${index}`
174
+ used.add(name)
175
+ return name
176
+ }
177
+
178
+ function normalizeName(value: string): string {
179
+ const trimmed = value.trim()
180
+ if (!trimmed) return "item"
181
+ const normalized = trimmed
182
+ .toLowerCase()
183
+ .replace(/[\\/]+/g, "-")
184
+ .replace(/[:\s]+/g, "-")
185
+ .replace(/[^a-z0-9_-]+/g, "-")
186
+ .replace(/-+/g, "-")
187
+ .replace(/^-+|-+$/g, "")
188
+ return normalized || "item"
189
+ }
190
+
191
+ // Flatten: drops namespace prefix (workflows:plan → plan)
192
+ function flattenCommandName(name: string): string {
193
+ const normalized = normalizeName(name)
194
+ return normalized.replace(/^[a-z]+-/, "") // Drop prefix before first dash
195
+ }
196
+ ```
197
+
198
+ **Key Learnings:**
199
+
200
+ 1. **Pre-scan for cross-references** — If target requires reference names (macros, URIs, IDs), build a map before conversion. Example: Devin needs macro names like `agent_kieran_rails_reviewer`, so pre-scan builds the map.
201
+
202
+ 2. **Content transformation is fragile** — Test extensively. Patterns that work for slash commands might false-match on file paths. Use negative lookahead to skip `/etc`, `/usr`, `/var`, etc.
203
+
204
+ 3. **Simplify heuristics, trust structural mapping** — Don't try to parse agent body for "You are..." or "NEVER do..." patterns. Instead, map agent.description → Overview, agent.body → Procedure, agent.capabilities → Specifications. Heuristics fail on edge cases and are hard to test.
205
+
206
+ 4. **Normalize early and consistently** — Use the same `normalizeName()` function throughout. Inconsistent normalization causes deduplication bugs.
207
+
208
+ 5. **MCP servers need target-specific handling:**
209
+ - **OpenCode:** Merge into `opencode.json` (preserve user keys)
210
+ - **Copilot:** Prefix env vars with `COPILOT_MCP_`, emit JSON
211
+ - **Devin:** Write setup instructions file (config is via web UI)
212
+ - **Cursor:** Pass through as-is
213
+
214
+ 6. **Warn on unsupported features** — Hooks, Gemini extensions, Kiro-incompatible MCP types. Emit to stderr and continue conversion.
215
+
216
+ **Reference Implementations:**
217
+ - OpenCode: `src/converters/claude-to-opencode.ts` (most comprehensive)
218
+ - Devin: `src/converters/claude-to-devin.ts` (content transformation + cross-references)
219
+ - Copilot: `src/converters/claude-to-copilot.ts` (MCP prefixing pattern)
220
+
221
+ ---
222
+
223
+ ### Phase 3: Writer (`src/targets/{target}.ts`)
224
+
225
+ **Purpose:** Write converted bundle to disk in target-specific directory structure
226
+
227
+ **Key Pattern:**
228
+
229
+ ```typescript
230
+ export async function write{Target}Bundle(outputRoot: string, bundle: {Target}Bundle): Promise<void> {
231
+ const paths = resolve{Target}Paths(outputRoot)
232
+ await ensureDir(paths.root)
233
+
234
+ // Write each component type
235
+ if (bundle.agents?.length > 0) {
236
+ const agentsDir = path.join(paths.root, "agents")
237
+ for (const agent of bundle.agents) {
238
+ await writeText(path.join(agentsDir, `${agent.name}.ext`), agent.content + "\n")
239
+ }
240
+ }
241
+
242
+ if (bundle.commands?.length > 0) {
243
+ const commandsDir = path.join(paths.root, "commands")
244
+ for (const command of bundle.commands) {
245
+ await writeText(path.join(commandsDir, `${command.name}.ext`), command.content + "\n")
246
+ }
247
+ }
248
+
249
+ // Copy skills (pass-through case)
250
+ if (bundle.skillDirs?.length > 0) {
251
+ const skillsDir = path.join(paths.root, "skills")
252
+ for (const skill of bundle.skillDirs) {
253
+ await copyDir(skill.sourceDir, path.join(skillsDir, skill.name))
254
+ }
255
+ }
256
+
257
+ // Write generated skills (converted from commands)
258
+ if (bundle.generatedSkills?.length > 0) {
259
+ const skillsDir = path.join(paths.root, "skills")
260
+ for (const skill of bundle.generatedSkills) {
261
+ await writeText(path.join(skillsDir, skill.name, "SKILL.md"), skill.content + "\n")
262
+ }
263
+ }
264
+
265
+ // Write MCP config (target-specific location and format)
266
+ if (bundle.mcpServers && Object.keys(bundle.mcpServers).length > 0) {
267
+ const mcpPath = path.join(paths.root, "mcp.json") // or copilot-mcp-config.json, etc.
268
+ const backupPath = await backupFile(mcpPath)
269
+ if (backupPath) {
270
+ console.log(`Backed up existing MCP config to ${backupPath}`)
271
+ }
272
+ await writeJson(mcpPath, { mcpServers: bundle.mcpServers })
273
+ }
274
+
275
+ // Write instructions or setup guides
276
+ if (bundle.setupInstructions) {
277
+ const setupPath = path.join(paths.root, "setup-instructions.md")
278
+ await writeText(setupPath, bundle.setupInstructions + "\n")
279
+ }
280
+ }
281
+
282
+ // Avoid double-nesting (.target/.target/)
283
+ function resolve{Target}Paths(outputRoot: string) {
284
+ const base = path.basename(outputRoot)
285
+ // If already pointing at .target, write directly into it
286
+ if (base === ".target") {
287
+ return { root: outputRoot }
288
+ }
289
+ // Otherwise nest under .target
290
+ return { root: path.join(outputRoot, ".target") }
291
+ }
292
+ ```
293
+
294
+ **Backup Pattern (MCP configs only):**
295
+
296
+ MCP configs are often pre-existing and user-edited. Backup before overwrite:
297
+
298
+ ```typescript
299
+ // From src/utils/files.ts
300
+ export async function backupFile(filePath: string): Promise<string | null> {
301
+ if (!existsSync(filePath)) return null
302
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
303
+ const dirname = path.dirname(filePath)
304
+ const basename = path.basename(filePath)
305
+ const ext = path.extname(basename)
306
+ const name = basename.slice(0, -ext.length)
307
+ const backupPath = path.join(dirname, `${name}.${timestamp}${ext}`)
308
+ await copyFile(filePath, backupPath)
309
+ return backupPath
310
+ }
311
+ ```
312
+
313
+ **Key Learnings:**
314
+
315
+ 1. **Always check for double-nesting** — If output root is already `.target`, don't nest again. Pattern:
316
+ ```typescript
317
+ if (path.basename(outputRoot) === ".target") {
318
+ return { root: outputRoot } // Write directly
319
+ }
320
+ return { root: path.join(outputRoot, ".target") } // Nest
321
+ ```
322
+
323
+ 2. **Use `writeText` and `writeJson` helpers** — These handle directory creation and line endings consistently
324
+
325
+ 3. **Backup MCP configs before overwriting** — MCP JSON files are often hand-edited. Always backup with timestamp.
326
+
327
+ 4. **Empty bundles should succeed gracefully** — Don't fail if a component array is empty. Many plugins may have no commands or no skills.
328
+
329
+ 5. **File extensions matter** — Match target conventions exactly:
330
+ - Copilot: `.agent.md` (note the dot)
331
+ - Cursor: `.mdc` for rules
332
+ - Devin: `.devin.md` for playbooks
333
+ - OpenCode: `.md` for commands
334
+
335
+ 6. **Permissions for sensitive files** — MCP config with API keys should use `0o600`:
336
+ ```typescript
337
+ await writeJson(mcpPath, config, { mode: 0o600 })
338
+ ```
339
+
340
+ **Reference Implementations:**
341
+ - Droid: `src/targets/droid.ts` (simpler pattern, good for learning)
342
+ - Copilot: `src/targets/copilot.ts` (double-nesting pattern)
343
+ - Devin: `src/targets/devin.ts` (setup instructions file)
344
+
345
+ ---
346
+
347
+ ### Phase 4: CLI Wiring
348
+
349
+ **File: `src/targets/index.ts`**
350
+
351
+ Register the new target in the global target registry:
352
+
353
+ ```typescript
354
+ import { convertClaudeTo{Target} } from "../converters/claude-to-{target}"
355
+ import { write{Target}Bundle } from "./{target}"
356
+ import type { {Target}Bundle } from "../types/{target}"
357
+
358
+ export const targets: Record<string, TargetHandler<any>> = {
359
+ // ... existing targets ...
360
+ {target}: {
361
+ name: "{target}",
362
+ implemented: true,
363
+ convert: convertClaudeTo{Target} as TargetHandler<{Target}Bundle>["convert"],
364
+ write: write{Target}Bundle as TargetHandler<{Target}Bundle>["write"],
365
+ },
366
+ }
367
+ ```
368
+
369
+ **File: `src/commands/convert.ts` and `src/commands/install.ts`**
370
+
371
+ Add output root resolution:
372
+
373
+ ```typescript
374
+ // In resolveTargetOutputRoot()
375
+ if (targetName === "{target}") {
376
+ return path.join(outputRoot, ".{target}")
377
+ }
378
+
379
+ // Update --to flag description
380
+ const toDescription = "Target format (opencode | codex | droid | cursor | copilot | kiro | {target})"
381
+ ```
382
+
383
+ ---
384
+
385
+ ### Phase 5: Sync Support (Optional)
386
+
387
+ **File: `src/sync/{target}.ts`**
388
+
389
+ If the target supports syncing personal skills and MCP servers:
390
+
391
+ ```typescript
392
+ export async function syncTo{Target}(outputRoot: string): Promise<void> {
393
+ const personalSkillsDir = path.join(expandHome("~/.claude/skills"))
394
+ const personalSettings = loadSettings(expandHome("~/.claude/settings.json"))
395
+
396
+ const skillsDest = path.join(outputRoot, ".{target}", "skills")
397
+ await ensureDir(skillsDest)
398
+
399
+ // Symlink personal skills
400
+ if (existsSync(personalSkillsDir)) {
401
+ const skills = readdirSync(personalSkillsDir)
402
+ for (const skill of skills) {
403
+ if (!isValidSkillName(skill)) continue
404
+ const source = path.join(personalSkillsDir, skill)
405
+ const dest = path.join(skillsDest, skill)
406
+ await forceSymlink(source, dest)
407
+ }
408
+ }
409
+
410
+ // Merge MCP servers if applicable
411
+ if (personalSettings.mcpServers) {
412
+ const mcpPath = path.join(outputRoot, ".{target}", "mcp.json")
413
+ const existing = readJson(mcpPath) || {}
414
+ const merged = {
415
+ ...existing,
416
+ mcpServers: {
417
+ ...existing.mcpServers,
418
+ ...personalSettings.mcpServers,
419
+ },
420
+ }
421
+ await writeJson(mcpPath, merged, { mode: 0o600 })
422
+ }
423
+ }
424
+ ```
425
+
426
+ **File: `src/commands/sync.ts`**
427
+
428
+ ```typescript
429
+ // Add to validTargets array
430
+ const validTargets = ["opencode", "codex", "droid", "cursor", "pi", "{target}"] as const
431
+
432
+ // In resolveOutputRoot()
433
+ case "{target}":
434
+ return path.join(process.cwd(), ".{target}")
435
+
436
+ // In main switch
437
+ case "{target}":
438
+ await syncTo{Target}(outputRoot)
439
+ break
440
+ ```
441
+
442
+ ---
443
+
444
+ ### Phase 6: Tests
445
+
446
+ **File: `tests/{target}-converter.test.ts`**
447
+
448
+ Test converter using inline `ClaudePlugin` fixtures:
449
+
450
+ ```typescript
451
+ describe("convertClaudeTo{Target}", () => {
452
+ it("converts agents to {target} format", () => {
453
+ const plugin: ClaudePlugin = {
454
+ name: "test",
455
+ agents: [
456
+ {
457
+ name: "test-agent",
458
+ description: "Test description",
459
+ body: "Test body",
460
+ capabilities: ["Cap 1", "Cap 2"],
461
+ },
462
+ ],
463
+ commands: [],
464
+ skills: [],
465
+ }
466
+
467
+ const bundle = convertClaudeTo{Target}(plugin, {})
468
+
469
+ expect(bundle.agents).toHaveLength(1)
470
+ expect(bundle.agents[0].name).toBe("test-agent")
471
+ expect(bundle.agents[0].content).toContain("Test description")
472
+ })
473
+
474
+ it("normalizes agent names", () => {
475
+ const plugin: ClaudePlugin = {
476
+ name: "test",
477
+ agents: [
478
+ { name: "Test Agent", description: "", body: "", capabilities: [] },
479
+ ],
480
+ commands: [],
481
+ skills: [],
482
+ }
483
+
484
+ const bundle = convertClaudeTo{Target}(plugin, {})
485
+ expect(bundle.agents[0].name).toBe("test-agent")
486
+ })
487
+
488
+ it("deduplicates colliding names", () => {
489
+ const plugin: ClaudePlugin = {
490
+ name: "test",
491
+ agents: [
492
+ { name: "Agent Name", description: "", body: "", capabilities: [] },
493
+ { name: "Agent Name", description: "", body: "", capabilities: [] },
494
+ ],
495
+ commands: [],
496
+ skills: [],
497
+ }
498
+
499
+ const bundle = convertClaudeTo{Target}(plugin, {})
500
+ expect(bundle.agents.map(a => a.name)).toEqual(["agent-name", "agent-name-2"])
501
+ })
502
+
503
+ it("transforms content paths (.claude → .{target})", () => {
504
+ const result = transformContentFor{Target}("See ~/.claude/config")
505
+ expect(result).toContain("~/.{target}/config")
506
+ })
507
+
508
+ it("warns when hooks are present", () => {
509
+ const spy = jest.spyOn(console, "warn")
510
+ const plugin: ClaudePlugin = {
511
+ name: "test",
512
+ agents: [],
513
+ commands: [],
514
+ skills: [],
515
+ hooks: { hooks: { "file:save": "test" } },
516
+ }
517
+
518
+ convertClaudeTo{Target}(plugin, {})
519
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining("hooks"))
520
+ })
521
+ })
522
+ ```
523
+
524
+ **File: `tests/{target}-writer.test.ts`**
525
+
526
+ Test writer using temp directories (from `tmp` package):
527
+
528
+ ```typescript
529
+ describe("write{Target}Bundle", () => {
530
+ it("writes agents to {target} format", async () => {
531
+ const tmpDir = await tmp.dir()
532
+ const bundle: {Target}Bundle = {
533
+ agents: [{ name: "test", content: "# Test\nBody" }],
534
+ commands: [],
535
+ skillDirs: [],
536
+ }
537
+
538
+ await write{Target}Bundle(tmpDir.path, bundle)
539
+
540
+ const written = readFileSync(path.join(tmpDir.path, ".{target}", "agents", "test.ext"), "utf-8")
541
+ expect(written).toContain("# Test")
542
+ })
543
+
544
+ it("does not double-nest when output root is .{target}", async () => {
545
+ const tmpDir = await tmp.dir()
546
+ const targetDir = path.join(tmpDir.path, ".{target}")
547
+ await ensureDir(targetDir)
548
+
549
+ const bundle: {Target}Bundle = {
550
+ agents: [{ name: "test", content: "# Test" }],
551
+ commands: [],
552
+ skillDirs: [],
553
+ }
554
+
555
+ await write{Target}Bundle(targetDir, bundle)
556
+
557
+ // Should write to targetDir directly, not targetDir/.{target}
558
+ const written = path.join(targetDir, "agents", "test.ext")
559
+ expect(existsSync(written)).toBe(true)
560
+ })
561
+
562
+ it("backs up existing MCP config", async () => {
563
+ const tmpDir = await tmp.dir()
564
+ const mcpPath = path.join(tmpDir.path, ".{target}", "mcp.json")
565
+ await ensureDir(path.dirname(mcpPath))
566
+ await writeJson(mcpPath, { existing: true })
567
+
568
+ const bundle: {Target}Bundle = {
569
+ agents: [],
570
+ commands: [],
571
+ skillDirs: [],
572
+ mcpServers: { "test": { command: "test" } },
573
+ }
574
+
575
+ await write{Target}Bundle(tmpDir.path, bundle)
576
+
577
+ // Backup should exist
578
+ const backups = readdirSync(path.dirname(mcpPath)).filter(f => f.includes("mcp") && f.includes("-"))
579
+ expect(backups.length).toBeGreaterThan(0)
580
+ })
581
+ })
582
+ ```
583
+
584
+ **Key Testing Patterns:**
585
+
586
+ - Test normalization, deduplication, content transformation separately
587
+ - Use inline plugin fixtures (not file-based)
588
+ - For writer tests, use temp directories and verify file existence
589
+ - Test edge cases: empty names, empty bodies, special characters
590
+ - Test error handling: missing files, permission issues
591
+
592
+ ---
593
+
594
+ ## Documentation Requirements
595
+
596
+ **File: `docs/specs/{target}.md`**
597
+
598
+ Document the target format specification:
599
+
600
+ - Last verified date (link to official docs)
601
+ - Config file locations (project-level vs. user-level)
602
+ - Agent/command/skill format with field descriptions
603
+ - MCP configuration structure
604
+ - Character limits (if any)
605
+ - Example file
606
+
607
+ **File: `README.md`**
608
+
609
+ Add to supported targets list and include usage examples.
610
+
611
+ ---
612
+
613
+ ## Common Pitfalls and Solutions
614
+
615
+ | Pitfall | Solution |
616
+ |---------|----------|
617
+ | **Double-nesting** (`.cursor/.cursor/`) | Check `path.basename(outputRoot)` before nesting |
618
+ | **Inconsistent name normalization** | Use single `normalizeName()` function everywhere |
619
+ | **Fragile content transformation** | Test regex patterns against edge cases (file paths, URLs) |
620
+ | **Heuristic section extraction fails** | Use structural mapping (description → Overview, body → Procedure) instead |
621
+ | **MCP config overwrites user edits** | Always backup with timestamp before overwriting |
622
+ | **Skill body not loaded** | Verify `ClaudeSkill` has `skillPath` field for file reading |
623
+ | **Missing deduplication** | Build `usedNames` set before conversion, pass to each converter |
624
+ | **Unsupported features cause silent loss** | Always warn to stderr (hooks, incompatible MCP types, etc.) |
625
+ | **Test isolation failures** | Use unique temp directories per test, clean up afterward |
626
+ | **Command namespace collisions after flattening** | Use `uniqueName()` with deduplication, test multiple collisions |
627
+
628
+ ---
629
+
630
+ ## Checklist for Adding a New Target
631
+
632
+ Use this checklist when adding a new target provider:
633
+
634
+ ### Implementation
635
+ - [ ] Create `src/types/{target}.ts` with bundle and component types
636
+ - [ ] Implement `src/converters/claude-to-{target}.ts` with converter and content transformer
637
+ - [ ] Implement `src/targets/{target}.ts` with writer
638
+ - [ ] Register target in `src/targets/index.ts`
639
+ - [ ] Update `src/commands/convert.ts` (add output root resolution, update help text)
640
+ - [ ] Update `src/commands/install.ts` (same as convert.ts)
641
+ - [ ] (Optional) Implement `src/sync/{target}.ts` and update `src/commands/sync.ts`
642
+
643
+ ### Testing
644
+ - [ ] Create `tests/{target}-converter.test.ts` with converter tests
645
+ - [ ] Create `tests/{target}-writer.test.ts` with writer tests
646
+ - [ ] (Optional) Create `tests/sync-{target}.test.ts` with sync tests
647
+ - [ ] Run full test suite: `bun test`
648
+ - [ ] Manual test: `bun run src/index.ts convert --to {target} ./plugins/compound-engineering`
649
+
650
+ ### Documentation
651
+ - [ ] Create `docs/specs/{target}.md` with format specification
652
+ - [ ] Update `README.md` with target in list and usage examples
653
+ - [ ] Update `CHANGELOG.md` with new target
654
+
655
+ ### Version Bumping
656
+ - [ ] Bump version in `package.json` (minor for new target)
657
+ - [ ] Update plugin.json description if component counts changed
658
+ - [ ] Verify CHANGELOG entry is clear
659
+
660
+ ---
661
+
662
+ ## References
663
+
664
+ ### Implementation Examples
665
+
666
+ **Reference implementations by priority (easiest to hardest):**
667
+
668
+ 1. **Droid** (`src/targets/droid.ts`, `src/converters/claude-to-droid.ts`) — Simplest pattern, good learning baseline
669
+ 2. **Copilot** (`src/targets/copilot.ts`, `src/converters/claude-to-copilot.ts`) — MCP prefixing, double-nesting guard
670
+ 3. **Devin** (`src/converters/claude-to-devin.ts`) — Content transformation, cross-references, intermediate types
671
+ 4. **OpenCode** (`src/converters/claude-to-opencode.ts`) — Most comprehensive, handles command structure and config merging
672
+
673
+ ### Key Utilities
674
+
675
+ - `src/utils/frontmatter.ts` — `formatFrontmatter()` and `parseFrontmatter()`
676
+ - `src/utils/files.ts` — `writeText()`, `writeJson()`, `copyDir()`, `backupFile()`, `ensureDir()`
677
+ - `src/utils/resolve-home.ts` — `expandHome()` for `~/.{target}` path resolution
678
+
679
+ ### Existing Tests
680
+
681
+ - `tests/cursor-converter.test.ts` — Comprehensive converter tests
682
+ - `tests/copilot-writer.test.ts` — Writer tests with temp directories
683
+ - `tests/sync-copilot.test.ts` — Sync pattern with symlinks and config merge
684
+
685
+ ---
686
+
687
+ ## Related Files
688
+
689
+ - `/C:/Source/compound-engineering-plugin/.claude-plugin/plugin.json` — Version and component counts
690
+ - `/C:/Source/compound-engineering-plugin/CHANGELOG.md` — Recent additions and patterns
691
+ - `/C:/Source/compound-engineering-plugin/README.md` — Usage examples for all targets
692
+ - `/C:/Source/compound-engineering-plugin/docs/solutions/plugin-versioning-requirements.md` — Checklist for releases
@@ -72,6 +72,6 @@ This documentation serves as a reminder. When Claude Code works on this plugin,
72
72
 
73
73
  ## Related Files
74
74
 
75
- - `/Users/kieranklaassen/every-marketplace/plugins/compound-engineering/.claude-plugin/plugin.json`
76
- - `/Users/kieranklaassen/every-marketplace/plugins/compound-engineering/CHANGELOG.md`
77
- - `/Users/kieranklaassen/every-marketplace/plugins/compound-engineering/README.md`
75
+ - `/Users/kieranklaassen/compound-engineering-plugin/plugins/compound-engineering/.claude-plugin/plugin.json`
76
+ - `/Users/kieranklaassen/compound-engineering-plugin/plugins/compound-engineering/CHANGELOG.md`
77
+ - `/Users/kieranklaassen/compound-engineering-plugin/plugins/compound-engineering/README.md`