@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.
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +5 -1
- package/CHANGELOG.md +42 -0
- package/CLAUDE.md +3 -3
- package/README.md +49 -15
- 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/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 -24
- package/src/commands/install.ts +102 -45
- package/src/commands/sync.ts +58 -38
- 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 +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/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/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,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/
|
|
76
|
-
- `/Users/kieranklaassen/
|
|
77
|
-
- `/Users/kieranklaassen/
|
|
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`
|