@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,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,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
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type WindsurfWorkflow = {
|
|
2
|
+
name: string
|
|
3
|
+
description: string
|
|
4
|
+
body: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type WindsurfGeneratedSkill = {
|
|
8
|
+
name: string
|
|
9
|
+
content: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type WindsurfSkillDir = {
|
|
13
|
+
name: string
|
|
14
|
+
sourceDir: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type WindsurfMcpServerEntry = {
|
|
18
|
+
command?: string
|
|
19
|
+
args?: string[]
|
|
20
|
+
env?: Record<string, string>
|
|
21
|
+
serverUrl?: string
|
|
22
|
+
headers?: Record<string, string>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type WindsurfMcpConfig = {
|
|
26
|
+
mcpServers: Record<string, WindsurfMcpServerEntry>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type WindsurfBundle = {
|
|
30
|
+
agentSkills: WindsurfGeneratedSkill[]
|
|
31
|
+
commandWorkflows: WindsurfWorkflow[]
|
|
32
|
+
skillDirs: WindsurfSkillDir[]
|
|
33
|
+
mcpConfig: WindsurfMcpConfig | null
|
|
34
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os from "os"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { pathExists } from "./files"
|
|
4
|
+
|
|
5
|
+
export type DetectedTool = {
|
|
6
|
+
name: string
|
|
7
|
+
detected: boolean
|
|
8
|
+
reason: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function detectInstalledTools(
|
|
12
|
+
home: string = os.homedir(),
|
|
13
|
+
cwd: string = process.cwd(),
|
|
14
|
+
): Promise<DetectedTool[]> {
|
|
15
|
+
const checks: Array<{ name: string; paths: string[] }> = [
|
|
16
|
+
{ name: "opencode", paths: [path.join(home, ".config", "opencode"), path.join(cwd, ".opencode")] },
|
|
17
|
+
{ name: "codex", paths: [path.join(home, ".codex")] },
|
|
18
|
+
{ name: "droid", paths: [path.join(home, ".factory")] },
|
|
19
|
+
{ name: "cursor", paths: [path.join(cwd, ".cursor"), path.join(home, ".cursor")] },
|
|
20
|
+
{ name: "pi", paths: [path.join(home, ".pi")] },
|
|
21
|
+
{ name: "gemini", paths: [path.join(cwd, ".gemini"), path.join(home, ".gemini")] },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const results: DetectedTool[] = []
|
|
25
|
+
for (const check of checks) {
|
|
26
|
+
let detected = false
|
|
27
|
+
let reason = "not found"
|
|
28
|
+
for (const p of check.paths) {
|
|
29
|
+
if (await pathExists(p)) {
|
|
30
|
+
detected = true
|
|
31
|
+
reason = `found ${p}`
|
|
32
|
+
break
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
results.push({ name: check.name, detected, reason })
|
|
36
|
+
}
|
|
37
|
+
return results
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function getDetectedTargetNames(
|
|
41
|
+
home: string = os.homedir(),
|
|
42
|
+
cwd: string = process.cwd(),
|
|
43
|
+
): Promise<string[]> {
|
|
44
|
+
const tools = await detectInstalledTools(home, cwd)
|
|
45
|
+
return tools.filter((t) => t.detected).map((t) => t.name)
|
|
46
|
+
}
|
package/src/utils/files.ts
CHANGED
|
@@ -46,6 +46,13 @@ export async function writeJson(filePath: string, data: unknown): Promise<void>
|
|
|
46
46
|
await writeText(filePath, content + "\n")
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Write JSON with restrictive permissions (0o600) for files containing secrets */
|
|
50
|
+
export async function writeJsonSecure(filePath: string, data: unknown): Promise<void> {
|
|
51
|
+
const content = JSON.stringify(data, null, 2)
|
|
52
|
+
await ensureDir(path.dirname(filePath))
|
|
53
|
+
await fs.writeFile(filePath, content + "\n", { encoding: "utf8", mode: 0o600 })
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
export async function walkFiles(root: string): Promise<string[]> {
|
|
50
57
|
const entries = await fs.readdir(root, { withFileTypes: true })
|
|
51
58
|
const results: string[] = []
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os from "os"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import type { TargetScope } from "../targets"
|
|
4
|
+
|
|
5
|
+
export function resolveTargetOutputRoot(options: {
|
|
6
|
+
targetName: string
|
|
7
|
+
outputRoot: string
|
|
8
|
+
codexHome: string
|
|
9
|
+
piHome: string
|
|
10
|
+
openclawHome?: string
|
|
11
|
+
qwenHome?: string
|
|
12
|
+
pluginName?: string
|
|
13
|
+
hasExplicitOutput: boolean
|
|
14
|
+
scope?: TargetScope
|
|
15
|
+
}): string {
|
|
16
|
+
const { targetName, outputRoot, codexHome, piHome, openclawHome, qwenHome, pluginName, hasExplicitOutput, scope } = options
|
|
17
|
+
if (targetName === "codex") return codexHome
|
|
18
|
+
if (targetName === "pi") return piHome
|
|
19
|
+
if (targetName === "droid") return path.join(os.homedir(), ".factory")
|
|
20
|
+
if (targetName === "cursor") {
|
|
21
|
+
const base = hasExplicitOutput ? outputRoot : process.cwd()
|
|
22
|
+
return path.join(base, ".cursor")
|
|
23
|
+
}
|
|
24
|
+
if (targetName === "gemini") {
|
|
25
|
+
const base = hasExplicitOutput ? outputRoot : process.cwd()
|
|
26
|
+
return path.join(base, ".gemini")
|
|
27
|
+
}
|
|
28
|
+
if (targetName === "copilot") {
|
|
29
|
+
const base = hasExplicitOutput ? outputRoot : process.cwd()
|
|
30
|
+
return path.join(base, ".github")
|
|
31
|
+
}
|
|
32
|
+
if (targetName === "kiro") {
|
|
33
|
+
const base = hasExplicitOutput ? outputRoot : process.cwd()
|
|
34
|
+
return path.join(base, ".kiro")
|
|
35
|
+
}
|
|
36
|
+
if (targetName === "windsurf") {
|
|
37
|
+
if (hasExplicitOutput) return outputRoot
|
|
38
|
+
if (scope === "global") return path.join(os.homedir(), ".codeium", "windsurf")
|
|
39
|
+
return path.join(process.cwd(), ".windsurf")
|
|
40
|
+
}
|
|
41
|
+
if (targetName === "openclaw") {
|
|
42
|
+
const home = openclawHome ?? path.join(os.homedir(), ".openclaw", "extensions")
|
|
43
|
+
return path.join(home, pluginName ?? "plugin")
|
|
44
|
+
}
|
|
45
|
+
if (targetName === "qwen") {
|
|
46
|
+
const home = qwenHome ?? path.join(os.homedir(), ".qwen", "extensions")
|
|
47
|
+
return path.join(home, pluginName ?? "plugin")
|
|
48
|
+
}
|
|
49
|
+
return outputRoot
|
|
50
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const SENSITIVE_PATTERN = /key|token|secret|password|credential|api_key/i
|
|
2
|
+
|
|
3
|
+
/** Check if any MCP servers have env vars that might contain secrets */
|
|
4
|
+
export function hasPotentialSecrets(
|
|
5
|
+
servers: Record<string, { env?: Record<string, string> }>,
|
|
6
|
+
): boolean {
|
|
7
|
+
for (const server of Object.values(servers)) {
|
|
8
|
+
if (server.env) {
|
|
9
|
+
for (const key of Object.keys(server.env)) {
|
|
10
|
+
if (SENSITIVE_PATTERN.test(key)) return true
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Return names of MCP servers whose env vars may contain secrets */
|
|
18
|
+
export function findServersWithPotentialSecrets(
|
|
19
|
+
servers: Record<string, { env?: Record<string, string> }>,
|
|
20
|
+
): string[] {
|
|
21
|
+
return Object.entries(servers)
|
|
22
|
+
.filter(([, s]) => s.env && Object.keys(s.env).some((k) => SENSITIVE_PATTERN.test(k)))
|
|
23
|
+
.map(([name]) => name)
|
|
24
|
+
}
|
package/tests/cli.test.ts
CHANGED
|
@@ -426,4 +426,82 @@ describe("CLI", () => {
|
|
|
426
426
|
expect(await exists(path.join(piRoot, "prompts", "workflows-review.md"))).toBe(true)
|
|
427
427
|
expect(await exists(path.join(piRoot, "extensions", "compound-engineering-compat.ts"))).toBe(true)
|
|
428
428
|
})
|
|
429
|
+
|
|
430
|
+
test("install --to opencode uses permissions:none by default", async () => {
|
|
431
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-perms-none-"))
|
|
432
|
+
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
|
433
|
+
|
|
434
|
+
const proc = Bun.spawn([
|
|
435
|
+
"bun",
|
|
436
|
+
"run",
|
|
437
|
+
"src/index.ts",
|
|
438
|
+
"install",
|
|
439
|
+
fixtureRoot,
|
|
440
|
+
"--to",
|
|
441
|
+
"opencode",
|
|
442
|
+
"--output",
|
|
443
|
+
tempRoot,
|
|
444
|
+
], {
|
|
445
|
+
cwd: path.join(import.meta.dir, ".."),
|
|
446
|
+
stdout: "pipe",
|
|
447
|
+
stderr: "pipe",
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
const exitCode = await proc.exited
|
|
451
|
+
const stdout = await new Response(proc.stdout).text()
|
|
452
|
+
const stderr = await new Response(proc.stderr).text()
|
|
453
|
+
|
|
454
|
+
if (exitCode !== 0) {
|
|
455
|
+
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
expect(stdout).toContain("Installed compound-engineering")
|
|
459
|
+
|
|
460
|
+
const opencodeJsonPath = path.join(tempRoot, "opencode.json")
|
|
461
|
+
const content = await fs.readFile(opencodeJsonPath, "utf-8")
|
|
462
|
+
const json = JSON.parse(content)
|
|
463
|
+
|
|
464
|
+
expect(json).not.toHaveProperty("permission")
|
|
465
|
+
expect(json).not.toHaveProperty("tools")
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test("install --to opencode --permissions broad writes permission block", async () => {
|
|
469
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-perms-broad-"))
|
|
470
|
+
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
|
471
|
+
|
|
472
|
+
const proc = Bun.spawn([
|
|
473
|
+
"bun",
|
|
474
|
+
"run",
|
|
475
|
+
"src/index.ts",
|
|
476
|
+
"install",
|
|
477
|
+
fixtureRoot,
|
|
478
|
+
"--to",
|
|
479
|
+
"opencode",
|
|
480
|
+
"--permissions",
|
|
481
|
+
"broad",
|
|
482
|
+
"--output",
|
|
483
|
+
tempRoot,
|
|
484
|
+
], {
|
|
485
|
+
cwd: path.join(import.meta.dir, ".."),
|
|
486
|
+
stdout: "pipe",
|
|
487
|
+
stderr: "pipe",
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
const exitCode = await proc.exited
|
|
491
|
+
const stdout = await new Response(proc.stdout).text()
|
|
492
|
+
const stderr = await new Response(proc.stderr).text()
|
|
493
|
+
|
|
494
|
+
if (exitCode !== 0) {
|
|
495
|
+
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
expect(stdout).toContain("Installed compound-engineering")
|
|
499
|
+
|
|
500
|
+
const opencodeJsonPath = path.join(tempRoot, "opencode.json")
|
|
501
|
+
const content = await fs.readFile(opencodeJsonPath, "utf-8")
|
|
502
|
+
const json = JSON.parse(content)
|
|
503
|
+
|
|
504
|
+
expect(json).toHaveProperty("permission")
|
|
505
|
+
expect(json.permission).not.toBeNull()
|
|
506
|
+
})
|
|
429
507
|
})
|
package/tests/converter.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { ClaudePlugin } from "../src/types/claude"
|
|
|
8
8
|
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
|
9
9
|
|
|
10
10
|
describe("convertClaudeToOpenCode", () => {
|
|
11
|
-
test("
|
|
11
|
+
test("from-command mode: map allowedTools to global permission block", async () => {
|
|
12
12
|
const plugin = await loadClaudePlugin(fixtureRoot)
|
|
13
13
|
const bundle = convertClaudeToOpenCode(plugin, {
|
|
14
14
|
agentMode: "subagent",
|
|
@@ -16,8 +16,9 @@ describe("convertClaudeToOpenCode", () => {
|
|
|
16
16
|
permissions: "from-commands",
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
-
expect(bundle.config.command
|
|
20
|
-
expect(bundle.
|
|
19
|
+
expect(bundle.config.command).toBeUndefined()
|
|
20
|
+
expect(bundle.commandFiles.find((f) => f.name === "workflows:review")).toBeDefined()
|
|
21
|
+
expect(bundle.commandFiles.find((f) => f.name === "plan_review")).toBeDefined()
|
|
21
22
|
|
|
22
23
|
const permission = bundle.config.permission as Record<string, string | Record<string, string>>
|
|
23
24
|
expect(Object.keys(permission).sort()).toEqual([
|
|
@@ -71,8 +72,10 @@ describe("convertClaudeToOpenCode", () => {
|
|
|
71
72
|
expect(parsed.data.model).toBe("anthropic/claude-sonnet-4-20250514")
|
|
72
73
|
expect(parsed.data.temperature).toBe(0.1)
|
|
73
74
|
|
|
74
|
-
const modelCommand = bundle.
|
|
75
|
-
expect(modelCommand
|
|
75
|
+
const modelCommand = bundle.commandFiles.find((f) => f.name === "workflows:work")
|
|
76
|
+
expect(modelCommand).toBeDefined()
|
|
77
|
+
const commandParsed = parseFrontmatter(modelCommand!.content)
|
|
78
|
+
expect(commandParsed.data.model).toBe("openai/gpt-4o")
|
|
76
79
|
})
|
|
77
80
|
|
|
78
81
|
test("resolves bare Claude model aliases to full IDs", () => {
|
|
@@ -199,7 +202,7 @@ describe("convertClaudeToOpenCode", () => {
|
|
|
199
202
|
expect(parsed.data.mode).toBe("primary")
|
|
200
203
|
})
|
|
201
204
|
|
|
202
|
-
test("excludes commands with disable-model-invocation from
|
|
205
|
+
test("excludes commands with disable-model-invocation from commandFiles", async () => {
|
|
203
206
|
const plugin = await loadClaudePlugin(fixtureRoot)
|
|
204
207
|
const bundle = convertClaudeToOpenCode(plugin, {
|
|
205
208
|
agentMode: "subagent",
|
|
@@ -208,10 +211,10 @@ describe("convertClaudeToOpenCode", () => {
|
|
|
208
211
|
})
|
|
209
212
|
|
|
210
213
|
// deploy-docs has disable-model-invocation: true, should be excluded
|
|
211
|
-
expect(bundle.
|
|
214
|
+
expect(bundle.commandFiles.find((f) => f.name === "deploy-docs")).toBeUndefined()
|
|
212
215
|
|
|
213
216
|
// Normal commands should still be present
|
|
214
|
-
expect(bundle.
|
|
217
|
+
expect(bundle.commandFiles.find((f) => f.name === "workflows:review")).toBeDefined()
|
|
215
218
|
})
|
|
216
219
|
|
|
217
220
|
test("rewrites .claude/ paths to .opencode/ in command bodies", () => {
|
|
@@ -240,10 +243,11 @@ Run \`/compound-engineering-setup\` to create a settings file.`,
|
|
|
240
243
|
permissions: "none",
|
|
241
244
|
})
|
|
242
245
|
|
|
243
|
-
const
|
|
246
|
+
const commandFile = bundle.commandFiles.find((f) => f.name === "review")
|
|
247
|
+
expect(commandFile).toBeDefined()
|
|
244
248
|
|
|
245
249
|
// Tool-agnostic path in project root — no rewriting needed
|
|
246
|
-
expect(
|
|
250
|
+
expect(commandFile!.content).toContain("compound-engineering.local.md")
|
|
247
251
|
})
|
|
248
252
|
|
|
249
253
|
test("rewrites .claude/ paths in agent bodies", () => {
|
|
@@ -273,4 +277,33 @@ Run \`/compound-engineering-setup\` to create a settings file.`,
|
|
|
273
277
|
// Tool-agnostic path in project root — no rewriting needed
|
|
274
278
|
expect(agentFile!.content).toContain("compound-engineering.local.md")
|
|
275
279
|
})
|
|
280
|
+
|
|
281
|
+
test("command .md files include description in frontmatter", () => {
|
|
282
|
+
const plugin: ClaudePlugin = {
|
|
283
|
+
root: "/tmp/plugin",
|
|
284
|
+
manifest: { name: "fixture", version: "1.0.0" },
|
|
285
|
+
agents: [],
|
|
286
|
+
commands: [
|
|
287
|
+
{
|
|
288
|
+
name: "test-cmd",
|
|
289
|
+
description: "Test description",
|
|
290
|
+
body: "Do the thing",
|
|
291
|
+
sourcePath: "/tmp/plugin/commands/test-cmd.md",
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
skills: [],
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const bundle = convertClaudeToOpenCode(plugin, {
|
|
298
|
+
agentMode: "subagent",
|
|
299
|
+
inferTemperature: false,
|
|
300
|
+
permissions: "none",
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const commandFile = bundle.commandFiles.find((f) => f.name === "test-cmd")
|
|
304
|
+
expect(commandFile).toBeDefined()
|
|
305
|
+
const parsed = parseFrontmatter(commandFile!.content)
|
|
306
|
+
expect(parsed.data.description).toBe("Test description")
|
|
307
|
+
expect(parsed.body).toContain("Do the thing")
|
|
308
|
+
})
|
|
276
309
|
})
|