@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,122 @@
1
+ import path from "path"
2
+ import { backupFile, copyDir, ensureDir, pathExists, readJson, writeJson, writeText } from "../utils/files"
3
+ import type { KiroBundle } from "../types/kiro"
4
+
5
+ export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): Promise<void> {
6
+ const paths = resolveKiroPaths(outputRoot)
7
+ await ensureDir(paths.kiroDir)
8
+
9
+ // Write agents
10
+ if (bundle.agents.length > 0) {
11
+ for (const agent of bundle.agents) {
12
+ // Validate name doesn't escape agents directory
13
+ validatePathSafe(agent.name, "agent")
14
+
15
+ // Write agent JSON config
16
+ await writeJson(
17
+ path.join(paths.agentsDir, `${agent.name}.json`),
18
+ agent.config,
19
+ )
20
+
21
+ // Write agent prompt file
22
+ await writeText(
23
+ path.join(paths.agentsDir, "prompts", `${agent.name}.md`),
24
+ agent.promptContent + "\n",
25
+ )
26
+ }
27
+ }
28
+
29
+ // Write generated skills (from commands)
30
+ if (bundle.generatedSkills.length > 0) {
31
+ for (const skill of bundle.generatedSkills) {
32
+ validatePathSafe(skill.name, "skill")
33
+ await writeText(
34
+ path.join(paths.skillsDir, skill.name, "SKILL.md"),
35
+ skill.content + "\n",
36
+ )
37
+ }
38
+ }
39
+
40
+ // Copy skill directories (pass-through)
41
+ if (bundle.skillDirs.length > 0) {
42
+ for (const skill of bundle.skillDirs) {
43
+ validatePathSafe(skill.name, "skill directory")
44
+ const destDir = path.join(paths.skillsDir, skill.name)
45
+
46
+ // Validate destination doesn't escape skills directory
47
+ const resolvedDest = path.resolve(destDir)
48
+ if (!resolvedDest.startsWith(path.resolve(paths.skillsDir))) {
49
+ console.warn(`Warning: Skill name "${skill.name}" escapes .kiro/skills/. Skipping.`)
50
+ continue
51
+ }
52
+
53
+ await copyDir(skill.sourceDir, destDir)
54
+ }
55
+ }
56
+
57
+ // Write steering files
58
+ if (bundle.steeringFiles.length > 0) {
59
+ for (const file of bundle.steeringFiles) {
60
+ validatePathSafe(file.name, "steering file")
61
+ await writeText(
62
+ path.join(paths.steeringDir, `${file.name}.md`),
63
+ file.content + "\n",
64
+ )
65
+ }
66
+ }
67
+
68
+ // Write MCP servers to mcp.json
69
+ if (Object.keys(bundle.mcpServers).length > 0) {
70
+ const mcpPath = path.join(paths.settingsDir, "mcp.json")
71
+ const backupPath = await backupFile(mcpPath)
72
+ if (backupPath) {
73
+ console.log(`Backed up existing mcp.json to ${backupPath}`)
74
+ }
75
+
76
+ // Merge with existing mcp.json if present
77
+ let existingConfig: Record<string, unknown> = {}
78
+ if (await pathExists(mcpPath)) {
79
+ try {
80
+ existingConfig = await readJson<Record<string, unknown>>(mcpPath)
81
+ } catch {
82
+ console.warn("Warning: existing mcp.json could not be parsed and will be replaced.")
83
+ }
84
+ }
85
+
86
+ const existingServers =
87
+ existingConfig.mcpServers && typeof existingConfig.mcpServers === "object"
88
+ ? (existingConfig.mcpServers as Record<string, unknown>)
89
+ : {}
90
+ const merged = { ...existingConfig, mcpServers: { ...existingServers, ...bundle.mcpServers } }
91
+ await writeJson(mcpPath, merged)
92
+ }
93
+ }
94
+
95
+ function resolveKiroPaths(outputRoot: string) {
96
+ const base = path.basename(outputRoot)
97
+ // If already pointing at .kiro, write directly into it
98
+ if (base === ".kiro") {
99
+ return {
100
+ kiroDir: outputRoot,
101
+ agentsDir: path.join(outputRoot, "agents"),
102
+ skillsDir: path.join(outputRoot, "skills"),
103
+ steeringDir: path.join(outputRoot, "steering"),
104
+ settingsDir: path.join(outputRoot, "settings"),
105
+ }
106
+ }
107
+ // Otherwise nest under .kiro
108
+ const kiroDir = path.join(outputRoot, ".kiro")
109
+ return {
110
+ kiroDir,
111
+ agentsDir: path.join(kiroDir, "agents"),
112
+ skillsDir: path.join(kiroDir, "skills"),
113
+ steeringDir: path.join(kiroDir, "steering"),
114
+ settingsDir: path.join(kiroDir, "settings"),
115
+ }
116
+ }
117
+
118
+ function validatePathSafe(name: string, label: string): void {
119
+ if (name.includes("..") || name.includes("/") || name.includes("\\")) {
120
+ throw new Error(`${label} name contains unsafe path characters: ${name}`)
121
+ }
122
+ }
@@ -0,0 +1,96 @@
1
+ import path from "path"
2
+ import { promises as fs } from "fs"
3
+ import { backupFile, copyDir, ensureDir, pathExists, readJson, walkFiles, writeJson, writeText } from "../utils/files"
4
+ import type { OpenClawBundle } from "../types/openclaw"
5
+
6
+ export async function writeOpenClawBundle(outputRoot: string, bundle: OpenClawBundle): Promise<void> {
7
+ const paths = resolveOpenClawPaths(outputRoot)
8
+ await ensureDir(paths.root)
9
+
10
+ // Write openclaw.plugin.json
11
+ await writeJson(paths.manifestPath, bundle.manifest)
12
+
13
+ // Write package.json
14
+ await writeJson(paths.packageJsonPath, bundle.packageJson)
15
+
16
+ // Write index.ts entry point
17
+ await writeText(paths.entryPointPath, bundle.entryPoint)
18
+
19
+ // Write generated skills (agents + commands converted to SKILL.md)
20
+ for (const skill of bundle.skills) {
21
+ const skillDir = path.join(paths.skillsDir, skill.dir)
22
+ await ensureDir(skillDir)
23
+ await writeText(path.join(skillDir, "SKILL.md"), skill.content + "\n")
24
+ }
25
+
26
+ // Copy original skill directories (preserving references/, assets/, scripts/)
27
+ // and rewrite .claude/ paths to .openclaw/ in markdown files
28
+ for (const skill of bundle.skillDirCopies) {
29
+ const destDir = path.join(paths.skillsDir, skill.name)
30
+ await copyDir(skill.sourceDir, destDir)
31
+ await rewritePathsInDir(destDir)
32
+ }
33
+
34
+ // Write openclaw.json config fragment if MCP servers exist
35
+ if (bundle.openclawConfig) {
36
+ const configPath = path.join(paths.root, "openclaw.json")
37
+ const backupPath = await backupFile(configPath)
38
+ if (backupPath) {
39
+ console.log(`Backed up existing config to ${backupPath}`)
40
+ }
41
+ const merged = await mergeOpenClawConfig(configPath, bundle.openclawConfig)
42
+ await writeJson(configPath, merged)
43
+ }
44
+ }
45
+
46
+ function resolveOpenClawPaths(outputRoot: string) {
47
+ return {
48
+ root: outputRoot,
49
+ manifestPath: path.join(outputRoot, "openclaw.plugin.json"),
50
+ packageJsonPath: path.join(outputRoot, "package.json"),
51
+ entryPointPath: path.join(outputRoot, "index.ts"),
52
+ skillsDir: path.join(outputRoot, "skills"),
53
+ }
54
+ }
55
+
56
+ async function rewritePathsInDir(dir: string): Promise<void> {
57
+ const files = await walkFiles(dir)
58
+ for (const file of files) {
59
+ if (!file.endsWith(".md")) continue
60
+ const content = await fs.readFile(file, "utf8")
61
+ const rewritten = content
62
+ .replace(/~\/\.claude\//g, "~/.openclaw/")
63
+ .replace(/\.claude\//g, ".openclaw/")
64
+ .replace(/\.claude-plugin\//g, "openclaw-plugin/")
65
+ if (rewritten !== content) {
66
+ await fs.writeFile(file, rewritten, "utf8")
67
+ }
68
+ }
69
+ }
70
+
71
+ async function mergeOpenClawConfig(
72
+ configPath: string,
73
+ incoming: Record<string, unknown>,
74
+ ): Promise<Record<string, unknown>> {
75
+ if (!(await pathExists(configPath))) return incoming
76
+
77
+ let existing: Record<string, unknown>
78
+ try {
79
+ existing = await readJson<Record<string, unknown>>(configPath)
80
+ } catch {
81
+ console.warn(
82
+ `Warning: existing ${configPath} is not valid JSON. Writing plugin config without merging.`,
83
+ )
84
+ return incoming
85
+ }
86
+
87
+ // Merge MCP servers: existing takes precedence on conflict
88
+ const incomingMcp = (incoming.mcpServers ?? {}) as Record<string, unknown>
89
+ const existingMcp = (existing.mcpServers ?? {}) as Record<string, unknown>
90
+ const mergedMcp = { ...incomingMcp, ...existingMcp }
91
+
92
+ return {
93
+ ...existing,
94
+ mcpServers: Object.keys(mergedMcp).length > 0 ? mergedMcp : undefined,
95
+ }
96
+ }
@@ -1,31 +1,93 @@
1
1
  import path from "path"
2
- import { backupFile, copyDir, ensureDir, writeJson, writeText } from "../utils/files"
3
- import type { OpenCodeBundle } from "../types/opencode"
2
+ import { backupFile, copyDir, ensureDir, pathExists, readJson, writeJson, writeText } from "../utils/files"
3
+ import type { OpenCodeBundle, OpenCodeConfig } from "../types/opencode"
4
+
5
+ // Merges plugin config into existing opencode.json. User keys win on conflict. See ADR-002.
6
+ async function mergeOpenCodeConfig(
7
+ configPath: string,
8
+ incoming: OpenCodeConfig,
9
+ ): Promise<OpenCodeConfig> {
10
+ // If no existing config, write plugin config as-is
11
+ if (!(await pathExists(configPath))) return incoming
12
+
13
+ let existing: OpenCodeConfig
14
+ try {
15
+ existing = await readJson<OpenCodeConfig>(configPath)
16
+ } catch {
17
+ // Safety first per AGENTS.md -- do not destroy user data even if their config is malformed.
18
+ // Warn and fall back to plugin-only config rather than crashing.
19
+ console.warn(
20
+ `Warning: existing ${configPath} is not valid JSON. Writing plugin config without merging.`
21
+ )
22
+ return incoming
23
+ }
24
+
25
+ // User config wins on conflict -- see ADR-002
26
+ // MCP servers: add plugin entry, skip keys already in user config.
27
+ const mergedMcp = {
28
+ ...(incoming.mcp ?? {}),
29
+ ...(existing.mcp ?? {}), // existing takes precedence (overwrites same-named plugin entry)
30
+ }
31
+
32
+ // Permission: add plugin entry, skip keys already in user config.
33
+ const mergedPermission = incoming.permission
34
+ ? {
35
+ ...(incoming.permission),
36
+ ...(existing.permission ?? {}), // existing takes precedence
37
+ }
38
+ : existing.permission
39
+
40
+ // Tools: same pattern
41
+ const mergedTools = incoming.tools
42
+ ? {
43
+ ...(incoming.tools),
44
+ ...(existing.tools ?? {}),
45
+ }
46
+ : existing.tools
47
+
48
+ return {
49
+ ...existing, // all user keys preserved
50
+ $schema: incoming.$schema ?? existing.$schema,
51
+ mcp: Object.keys(mergedMcp).length > 0 ? mergedMcp : undefined,
52
+ permission: mergedPermission,
53
+ tools: mergedTools,
54
+ }
55
+ }
4
56
 
5
57
  export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBundle): Promise<void> {
6
- const paths = resolveOpenCodePaths(outputRoot)
7
- await ensureDir(paths.root)
58
+ const openCodePaths = resolveOpenCodePaths(outputRoot)
59
+ await ensureDir(openCodePaths.root)
8
60
 
9
- const backupPath = await backupFile(paths.configPath)
61
+ const backupPath = await backupFile(openCodePaths.configPath)
10
62
  if (backupPath) {
11
63
  console.log(`Backed up existing config to ${backupPath}`)
12
64
  }
13
- await writeJson(paths.configPath, bundle.config)
65
+ const merged = await mergeOpenCodeConfig(openCodePaths.configPath, bundle.config)
66
+ await writeJson(openCodePaths.configPath, merged)
14
67
 
15
- const agentsDir = paths.agentsDir
68
+ const agentsDir = openCodePaths.agentsDir
16
69
  for (const agent of bundle.agents) {
17
70
  await writeText(path.join(agentsDir, `${agent.name}.md`), agent.content + "\n")
18
71
  }
19
72
 
73
+ for (const commandFile of bundle.commandFiles) {
74
+ const dest = path.join(openCodePaths.commandDir, `${commandFile.name}.md`)
75
+ const cmdBackupPath = await backupFile(dest)
76
+ if (cmdBackupPath) {
77
+ console.log(`Backed up existing command file to ${cmdBackupPath}`)
78
+ }
79
+ await writeText(dest, commandFile.content + "\n")
80
+ }
81
+
20
82
  if (bundle.plugins.length > 0) {
21
- const pluginsDir = paths.pluginsDir
83
+ const pluginsDir = openCodePaths.pluginsDir
22
84
  for (const plugin of bundle.plugins) {
23
85
  await writeText(path.join(pluginsDir, plugin.name), plugin.content + "\n")
24
86
  }
25
87
  }
26
88
 
27
89
  if (bundle.skillDirs.length > 0) {
28
- const skillsRoot = paths.skillsDir
90
+ const skillsRoot = openCodePaths.skillsDir
29
91
  for (const skill of bundle.skillDirs) {
30
92
  await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
31
93
  }
@@ -43,6 +105,8 @@ function resolveOpenCodePaths(outputRoot: string) {
43
105
  agentsDir: path.join(outputRoot, "agents"),
44
106
  pluginsDir: path.join(outputRoot, "plugins"),
45
107
  skillsDir: path.join(outputRoot, "skills"),
108
+ // .md command files; alternative to the command key in opencode.json
109
+ commandDir: path.join(outputRoot, "commands"),
46
110
  }
47
111
  }
48
112
 
@@ -53,5 +117,7 @@ function resolveOpenCodePaths(outputRoot: string) {
53
117
  agentsDir: path.join(outputRoot, ".opencode", "agents"),
54
118
  pluginsDir: path.join(outputRoot, ".opencode", "plugins"),
55
119
  skillsDir: path.join(outputRoot, ".opencode", "skills"),
120
+ // .md command files; alternative to the command key in opencode.json
121
+ commandDir: path.join(outputRoot, ".opencode", "commands"),
56
122
  }
57
- }
123
+ }
@@ -0,0 +1,64 @@
1
+ import path from "path"
2
+ import { backupFile, copyDir, ensureDir, writeJson, writeText } from "../utils/files"
3
+ import type { QwenBundle, QwenExtensionConfig } from "../types/qwen"
4
+
5
+ export async function writeQwenBundle(outputRoot: string, bundle: QwenBundle): Promise<void> {
6
+ const qwenPaths = resolveQwenPaths(outputRoot)
7
+ await ensureDir(qwenPaths.root)
8
+
9
+ // Write qwen-extension.json config
10
+ const configPath = qwenPaths.configPath
11
+ const backupPath = await backupFile(configPath)
12
+ if (backupPath) {
13
+ console.log(`Backed up existing config to ${backupPath}`)
14
+ }
15
+ await writeJson(configPath, bundle.config)
16
+
17
+ // Write context file (QWEN.md)
18
+ if (bundle.contextFile) {
19
+ await writeText(qwenPaths.contextPath, bundle.contextFile + "\n")
20
+ }
21
+
22
+ // Write agents
23
+ const agentsDir = qwenPaths.agentsDir
24
+ await ensureDir(agentsDir)
25
+ for (const agent of bundle.agents) {
26
+ const ext = agent.format === "yaml" ? "yaml" : "md"
27
+ await writeText(path.join(agentsDir, `${agent.name}.${ext}`), agent.content + "\n")
28
+ }
29
+
30
+ // Write commands
31
+ const commandsDir = qwenPaths.commandsDir
32
+ await ensureDir(commandsDir)
33
+ for (const commandFile of bundle.commandFiles) {
34
+ // Support nested commands with colon separator
35
+ const parts = commandFile.name.split(":")
36
+ if (parts.length > 1) {
37
+ const nestedDir = path.join(commandsDir, ...parts.slice(0, -1))
38
+ await ensureDir(nestedDir)
39
+ await writeText(path.join(nestedDir, `${parts[parts.length - 1]}.md`), commandFile.content + "\n")
40
+ } else {
41
+ await writeText(path.join(commandsDir, `${commandFile.name}.md`), commandFile.content + "\n")
42
+ }
43
+ }
44
+
45
+ // Copy skills
46
+ if (bundle.skillDirs.length > 0) {
47
+ const skillsRoot = qwenPaths.skillsDir
48
+ await ensureDir(skillsRoot)
49
+ for (const skill of bundle.skillDirs) {
50
+ await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
51
+ }
52
+ }
53
+ }
54
+
55
+ function resolveQwenPaths(outputRoot: string) {
56
+ return {
57
+ root: outputRoot,
58
+ configPath: path.join(outputRoot, "qwen-extension.json"),
59
+ contextPath: path.join(outputRoot, "QWEN.md"),
60
+ agentsDir: path.join(outputRoot, "agents"),
61
+ commandsDir: path.join(outputRoot, "commands"),
62
+ skillsDir: path.join(outputRoot, "skills"),
63
+ }
64
+ }
@@ -0,0 +1,104 @@
1
+ import path from "path"
2
+ import { backupFile, copyDir, ensureDir, pathExists, readJson, writeJsonSecure, writeText } from "../utils/files"
3
+ import { formatFrontmatter } from "../utils/frontmatter"
4
+ import type { WindsurfBundle } from "../types/windsurf"
5
+ import type { TargetScope } from "./index"
6
+
7
+ /**
8
+ * Write a WindsurfBundle directly into outputRoot.
9
+ *
10
+ * Unlike other target writers, this writer expects outputRoot to be the final
11
+ * resolved directory — the CLI handles scope-based nesting (global vs workspace).
12
+ */
13
+ export async function writeWindsurfBundle(outputRoot: string, bundle: WindsurfBundle, scope?: TargetScope): Promise<void> {
14
+ await ensureDir(outputRoot)
15
+
16
+ // Write agent skills (before pass-through copies so pass-through takes precedence on collision)
17
+ if (bundle.agentSkills.length > 0) {
18
+ const skillsDir = path.join(outputRoot, "skills")
19
+ await ensureDir(skillsDir)
20
+ for (const skill of bundle.agentSkills) {
21
+ validatePathSafe(skill.name, "agent skill")
22
+ const destDir = path.join(skillsDir, skill.name)
23
+
24
+ const resolvedDest = path.resolve(destDir)
25
+ if (!resolvedDest.startsWith(path.resolve(skillsDir))) {
26
+ console.warn(`Warning: Agent skill name "${skill.name}" escapes skills/. Skipping.`)
27
+ continue
28
+ }
29
+
30
+ await ensureDir(destDir)
31
+ await writeText(path.join(destDir, "SKILL.md"), skill.content)
32
+ }
33
+ }
34
+
35
+ // Write command workflows (flat in global_workflows/ for global scope, workflows/ for workspace)
36
+ if (bundle.commandWorkflows.length > 0) {
37
+ const workflowsDirName = scope === "global" ? "global_workflows" : "workflows"
38
+ const workflowsDir = path.join(outputRoot, workflowsDirName)
39
+ await ensureDir(workflowsDir)
40
+ for (const workflow of bundle.commandWorkflows) {
41
+ validatePathSafe(workflow.name, "command workflow")
42
+ const content = formatWorkflowContent(workflow.name, workflow.description, workflow.body)
43
+ await writeText(path.join(workflowsDir, `${workflow.name}.md`), content)
44
+ }
45
+ }
46
+
47
+ // Copy pass-through skill directories (after generated skills so copies overwrite on collision)
48
+ if (bundle.skillDirs.length > 0) {
49
+ const skillsDir = path.join(outputRoot, "skills")
50
+ await ensureDir(skillsDir)
51
+ for (const skill of bundle.skillDirs) {
52
+ validatePathSafe(skill.name, "skill directory")
53
+ const destDir = path.join(skillsDir, skill.name)
54
+
55
+ const resolvedDest = path.resolve(destDir)
56
+ if (!resolvedDest.startsWith(path.resolve(skillsDir))) {
57
+ console.warn(`Warning: Skill name "${skill.name}" escapes skills/. Skipping.`)
58
+ continue
59
+ }
60
+
61
+ await copyDir(skill.sourceDir, destDir)
62
+ }
63
+ }
64
+
65
+ // Merge MCP config
66
+ if (bundle.mcpConfig) {
67
+ const mcpPath = path.join(outputRoot, "mcp_config.json")
68
+ const backupPath = await backupFile(mcpPath)
69
+ if (backupPath) {
70
+ console.log(`Backed up existing mcp_config.json to ${backupPath}`)
71
+ }
72
+
73
+ let existingConfig: Record<string, unknown> = {}
74
+ if (await pathExists(mcpPath)) {
75
+ try {
76
+ const parsed = await readJson<unknown>(mcpPath)
77
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
78
+ existingConfig = parsed as Record<string, unknown>
79
+ }
80
+ } catch {
81
+ console.warn("Warning: existing mcp_config.json could not be parsed and will be replaced.")
82
+ }
83
+ }
84
+
85
+ const existingServers =
86
+ existingConfig.mcpServers &&
87
+ typeof existingConfig.mcpServers === "object" &&
88
+ !Array.isArray(existingConfig.mcpServers)
89
+ ? (existingConfig.mcpServers as Record<string, unknown>)
90
+ : {}
91
+ const merged = { ...existingConfig, mcpServers: { ...existingServers, ...bundle.mcpConfig.mcpServers } }
92
+ await writeJsonSecure(mcpPath, merged)
93
+ }
94
+ }
95
+
96
+ function validatePathSafe(name: string, label: string): void {
97
+ if (name.includes("..") || name.includes("/") || name.includes("\\")) {
98
+ throw new Error(`${label} name contains unsafe path characters: ${name}`)
99
+ }
100
+ }
101
+
102
+ function formatWorkflowContent(name: string, description: string, body: string): string {
103
+ return formatFrontmatter({ description }, `# ${name}\n\n${body}`) + "\n"
104
+ }
@@ -0,0 +1,44 @@
1
+ export type KiroAgent = {
2
+ name: string
3
+ config: KiroAgentConfig
4
+ promptContent: string
5
+ }
6
+
7
+ export type KiroAgentConfig = {
8
+ name: string
9
+ description: string
10
+ prompt: `file://${string}`
11
+ tools: ["*"]
12
+ resources: string[]
13
+ includeMcpJson: true
14
+ welcomeMessage?: string
15
+ }
16
+
17
+ export type KiroSkill = {
18
+ name: string
19
+ content: string // Full SKILL.md with YAML frontmatter
20
+ }
21
+
22
+ export type KiroSkillDir = {
23
+ name: string
24
+ sourceDir: string
25
+ }
26
+
27
+ export type KiroSteeringFile = {
28
+ name: string
29
+ content: string
30
+ }
31
+
32
+ export type KiroMcpServer = {
33
+ command: string
34
+ args?: string[]
35
+ env?: Record<string, string>
36
+ }
37
+
38
+ export type KiroBundle = {
39
+ agents: KiroAgent[]
40
+ generatedSkills: KiroSkill[]
41
+ skillDirs: KiroSkillDir[]
42
+ steeringFiles: KiroSteeringFile[]
43
+ mcpServers: Record<string, KiroMcpServer>
44
+ }
@@ -0,0 +1,52 @@
1
+ export type OpenClawPluginManifest = {
2
+ id: string
3
+ name: string
4
+ kind: "tool"
5
+ configSchema?: {
6
+ type: "object"
7
+ additionalProperties: boolean
8
+ properties: Record<string, OpenClawConfigProperty>
9
+ required?: string[]
10
+ }
11
+ uiHints?: Record<string, OpenClawUiHint>
12
+ skills?: string[]
13
+ }
14
+
15
+ export type OpenClawConfigProperty = {
16
+ type: string
17
+ description?: string
18
+ default?: unknown
19
+ }
20
+
21
+ export type OpenClawUiHint = {
22
+ label: string
23
+ sensitive?: boolean
24
+ placeholder?: string
25
+ }
26
+
27
+ export type OpenClawSkillFile = {
28
+ name: string
29
+ content: string
30
+ /** Subdirectory path inside skills/ (e.g. "agent-native-reviewer") */
31
+ dir: string
32
+ }
33
+
34
+ export type OpenClawCommandRegistration = {
35
+ name: string
36
+ description: string
37
+ acceptsArgs: boolean
38
+ /** The prompt body that becomes the command handler response */
39
+ body: string
40
+ }
41
+
42
+ export type OpenClawBundle = {
43
+ manifest: OpenClawPluginManifest
44
+ packageJson: Record<string, unknown>
45
+ entryPoint: string
46
+ skills: OpenClawSkillFile[]
47
+ /** Skill directories to copy verbatim (original Claude skills with references/) */
48
+ skillDirCopies: { sourceDir: string; name: string }[]
49
+ commands: OpenClawCommandRegistration[]
50
+ /** openclaw.json fragment for MCP servers */
51
+ openclawConfig?: Record<string, unknown>
52
+ }
@@ -7,7 +7,6 @@ export type OpenCodeConfig = {
7
7
  tools?: Record<string, boolean>
8
8
  permission?: Record<string, OpenCodePermission | Record<string, OpenCodePermission>>
9
9
  agent?: Record<string, OpenCodeAgentConfig>
10
- command?: Record<string, OpenCodeCommandConfig>
11
10
  mcp?: Record<string, OpenCodeMcpServer>
12
11
  }
13
12
 
@@ -20,13 +19,6 @@ export type OpenCodeAgentConfig = {
20
19
  permission?: Record<string, OpenCodePermission>
21
20
  }
22
21
 
23
- export type OpenCodeCommandConfig = {
24
- description?: string
25
- model?: string
26
- agent?: string
27
- template: string
28
- }
29
-
30
22
  export type OpenCodeMcpServer = {
31
23
  type: "local" | "remote"
32
24
  command?: string[]
@@ -46,9 +38,16 @@ export type OpenCodePluginFile = {
46
38
  content: string
47
39
  }
48
40
 
41
+ export type OpenCodeCommandFile = {
42
+ name: string
43
+ content: string
44
+ }
45
+
49
46
  export type OpenCodeBundle = {
50
47
  config: OpenCodeConfig
51
48
  agents: OpenCodeAgentFile[]
49
+ // Commands are written as individual .md files, not in opencode.json. See ADR-001.
50
+ commandFiles: OpenCodeCommandFile[]
52
51
  plugins: OpenCodePluginFile[]
53
52
  skillDirs: { sourceDir: string; name: string }[]
54
53
  }
@@ -0,0 +1,48 @@
1
+ export type QwenExtensionConfig = {
2
+ name: string
3
+ version: string
4
+ mcpServers?: Record<string, QwenMcpServer>
5
+ contextFileName?: string
6
+ commands?: string
7
+ skills?: string
8
+ agents?: string
9
+ settings?: QwenSetting[]
10
+ }
11
+
12
+ export type QwenMcpServer = {
13
+ command?: string
14
+ args?: string[]
15
+ env?: Record<string, string>
16
+ cwd?: string
17
+ }
18
+
19
+ export type QwenSetting = {
20
+ name: string
21
+ description: string
22
+ envVar: string
23
+ sensitive?: boolean
24
+ }
25
+
26
+ export type QwenAgentFile = {
27
+ name: string
28
+ content: string
29
+ format: "yaml" | "markdown"
30
+ }
31
+
32
+ export type QwenSkillDir = {
33
+ sourceDir: string
34
+ name: string
35
+ }
36
+
37
+ export type QwenCommandFile = {
38
+ name: string
39
+ content: string
40
+ }
41
+
42
+ export type QwenBundle = {
43
+ config: QwenExtensionConfig
44
+ agents: QwenAgentFile[]
45
+ commandFiles: QwenCommandFile[]
46
+ skillDirs: QwenSkillDir[]
47
+ contextFile?: string
48
+ }