@every-env/compound-plugin 0.8.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +5 -1
- package/CHANGELOG.md +50 -0
- package/CLAUDE.md +3 -3
- package/README.md +52 -14
- package/docs/plans/2026-02-14-feat-auto-detect-install-and-gemini-sync-plan.md +360 -0
- package/docs/plans/2026-02-25-feat-windsurf-global-scope-support-plan.md +627 -0
- package/docs/plans/2026-03-01-feat-ce-command-aliases-backwards-compatible-deprecation-plan.md +261 -0
- package/docs/plans/feature_opencode-commands-as-md-and-config-merge.md +574 -0
- package/docs/solutions/adding-converter-target-providers.md +692 -0
- package/docs/solutions/plugin-versioning-requirements.md +3 -3
- package/docs/specs/kiro.md +171 -0
- package/docs/specs/windsurf.md +477 -0
- package/package.json +1 -1
- package/plans/landing-page-launchkit-refresh.md +2 -2
- package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
- package/plugins/compound-engineering/CHANGELOG.md +72 -1
- package/plugins/compound-engineering/CLAUDE.md +9 -7
- package/plugins/compound-engineering/README.md +10 -7
- package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
- package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
- package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
- package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
- package/plugins/compound-engineering/commands/ce/compound.md +240 -0
- package/plugins/compound-engineering/commands/ce/plan.md +636 -0
- package/plugins/compound-engineering/commands/ce/review.md +525 -0
- package/plugins/compound-engineering/commands/ce/work.md +470 -0
- package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
- package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
- package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
- package/plugins/compound-engineering/commands/feature-video.md +15 -6
- package/plugins/compound-engineering/commands/heal-skill.md +1 -1
- package/plugins/compound-engineering/commands/lfg.md +3 -3
- package/plugins/compound-engineering/commands/slfg.md +3 -3
- package/plugins/compound-engineering/commands/test-xcode.md +2 -2
- package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
- package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
- package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
- package/plugins/compound-engineering/commands/workflows/review.md +4 -522
- package/plugins/compound-engineering/commands/workflows/work.md +4 -448
- package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
- package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
- package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
- package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/setup/SKILL.md +2 -2
- package/src/commands/convert.ts +101 -23
- package/src/commands/install.ts +102 -41
- package/src/commands/sync.ts +58 -38
- package/src/converters/claude-to-kiro.ts +262 -0
- package/src/converters/claude-to-openclaw.ts +240 -0
- package/src/converters/claude-to-opencode.ts +12 -10
- package/src/converters/claude-to-qwen.ts +238 -0
- package/src/converters/claude-to-windsurf.ts +205 -0
- package/src/sync/gemini.ts +76 -0
- package/src/targets/index.ts +69 -1
- package/src/targets/kiro.ts +122 -0
- package/src/targets/openclaw.ts +96 -0
- package/src/targets/opencode.ts +76 -10
- package/src/targets/qwen.ts +64 -0
- package/src/targets/windsurf.ts +104 -0
- package/src/types/kiro.ts +44 -0
- package/src/types/openclaw.ts +52 -0
- package/src/types/opencode.ts +7 -8
- package/src/types/qwen.ts +48 -0
- package/src/types/windsurf.ts +34 -0
- package/src/utils/detect-tools.ts +46 -0
- package/src/utils/files.ts +7 -0
- package/src/utils/resolve-output.ts +50 -0
- package/src/utils/secrets.ts +24 -0
- package/tests/cli.test.ts +78 -0
- package/tests/converter.test.ts +43 -10
- package/tests/detect-tools.test.ts +96 -0
- package/tests/kiro-converter.test.ts +381 -0
- package/tests/kiro-writer.test.ts +273 -0
- package/tests/openclaw-converter.test.ts +200 -0
- package/tests/opencode-writer.test.ts +142 -5
- package/tests/qwen-converter.test.ts +238 -0
- package/tests/resolve-output.test.ts +131 -0
- package/tests/sync-gemini.test.ts +106 -0
- package/tests/windsurf-converter.test.ts +573 -0
- package/tests/windsurf-writer.test.ts +359 -0
- package/docs/css/docs.css +0 -675
- package/docs/css/style.css +0 -2886
- package/docs/index.html +0 -1046
- package/docs/js/main.js +0 -225
- package/docs/pages/agents.html +0 -649
- package/docs/pages/changelog.html +0 -534
- package/docs/pages/commands.html +0 -523
- package/docs/pages/getting-started.html +0 -582
- package/docs/pages/mcp-servers.html +0 -409
- package/docs/pages/skills.html +0 -611
|
@@ -0,0 +1,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
|
+
}
|
package/src/targets/opencode.ts
CHANGED
|
@@ -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
|
|
7
|
-
await ensureDir(
|
|
58
|
+
const openCodePaths = resolveOpenCodePaths(outputRoot)
|
|
59
|
+
await ensureDir(openCodePaths.root)
|
|
8
60
|
|
|
9
|
-
const backupPath = await backupFile(
|
|
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
|
|
65
|
+
const merged = await mergeOpenCodeConfig(openCodePaths.configPath, bundle.config)
|
|
66
|
+
await writeJson(openCodePaths.configPath, merged)
|
|
14
67
|
|
|
15
|
-
const 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 =
|
|
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 =
|
|
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
|
+
}
|
package/src/types/opencode.ts
CHANGED
|
@@ -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
|
+
}
|