@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,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
|
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { promises as fs } from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import os from "os"
|
|
5
|
+
import { detectInstalledTools, getDetectedTargetNames } from "../src/utils/detect-tools"
|
|
6
|
+
|
|
7
|
+
describe("detectInstalledTools", () => {
|
|
8
|
+
test("detects tools when config directories exist", async () => {
|
|
9
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "detect-tools-"))
|
|
10
|
+
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "detect-tools-cwd-"))
|
|
11
|
+
|
|
12
|
+
// Create directories for some tools
|
|
13
|
+
await fs.mkdir(path.join(tempHome, ".codex"), { recursive: true })
|
|
14
|
+
await fs.mkdir(path.join(tempCwd, ".cursor"), { recursive: true })
|
|
15
|
+
await fs.mkdir(path.join(tempCwd, ".gemini"), { recursive: true })
|
|
16
|
+
|
|
17
|
+
const results = await detectInstalledTools(tempHome, tempCwd)
|
|
18
|
+
|
|
19
|
+
const codex = results.find((t) => t.name === "codex")
|
|
20
|
+
expect(codex?.detected).toBe(true)
|
|
21
|
+
expect(codex?.reason).toContain(".codex")
|
|
22
|
+
|
|
23
|
+
const cursor = results.find((t) => t.name === "cursor")
|
|
24
|
+
expect(cursor?.detected).toBe(true)
|
|
25
|
+
expect(cursor?.reason).toContain(".cursor")
|
|
26
|
+
|
|
27
|
+
const gemini = results.find((t) => t.name === "gemini")
|
|
28
|
+
expect(gemini?.detected).toBe(true)
|
|
29
|
+
expect(gemini?.reason).toContain(".gemini")
|
|
30
|
+
|
|
31
|
+
// Tools without directories should not be detected
|
|
32
|
+
const opencode = results.find((t) => t.name === "opencode")
|
|
33
|
+
expect(opencode?.detected).toBe(false)
|
|
34
|
+
|
|
35
|
+
const droid = results.find((t) => t.name === "droid")
|
|
36
|
+
expect(droid?.detected).toBe(false)
|
|
37
|
+
|
|
38
|
+
const pi = results.find((t) => t.name === "pi")
|
|
39
|
+
expect(pi?.detected).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test("returns all tools with detected=false when no directories exist", async () => {
|
|
43
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "detect-empty-"))
|
|
44
|
+
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "detect-empty-cwd-"))
|
|
45
|
+
|
|
46
|
+
const results = await detectInstalledTools(tempHome, tempCwd)
|
|
47
|
+
|
|
48
|
+
expect(results.length).toBe(6)
|
|
49
|
+
for (const tool of results) {
|
|
50
|
+
expect(tool.detected).toBe(false)
|
|
51
|
+
expect(tool.reason).toBe("not found")
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test("detects home-based tools", async () => {
|
|
56
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "detect-home-"))
|
|
57
|
+
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "detect-home-cwd-"))
|
|
58
|
+
|
|
59
|
+
await fs.mkdir(path.join(tempHome, ".config", "opencode"), { recursive: true })
|
|
60
|
+
await fs.mkdir(path.join(tempHome, ".factory"), { recursive: true })
|
|
61
|
+
await fs.mkdir(path.join(tempHome, ".pi"), { recursive: true })
|
|
62
|
+
|
|
63
|
+
const results = await detectInstalledTools(tempHome, tempCwd)
|
|
64
|
+
|
|
65
|
+
expect(results.find((t) => t.name === "opencode")?.detected).toBe(true)
|
|
66
|
+
expect(results.find((t) => t.name === "droid")?.detected).toBe(true)
|
|
67
|
+
expect(results.find((t) => t.name === "pi")?.detected).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe("getDetectedTargetNames", () => {
|
|
72
|
+
test("returns only names of detected tools", async () => {
|
|
73
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "detect-names-"))
|
|
74
|
+
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "detect-names-cwd-"))
|
|
75
|
+
|
|
76
|
+
await fs.mkdir(path.join(tempHome, ".codex"), { recursive: true })
|
|
77
|
+
await fs.mkdir(path.join(tempCwd, ".gemini"), { recursive: true })
|
|
78
|
+
|
|
79
|
+
const names = await getDetectedTargetNames(tempHome, tempCwd)
|
|
80
|
+
|
|
81
|
+
expect(names).toContain("codex")
|
|
82
|
+
expect(names).toContain("gemini")
|
|
83
|
+
expect(names).not.toContain("opencode")
|
|
84
|
+
expect(names).not.toContain("droid")
|
|
85
|
+
expect(names).not.toContain("pi")
|
|
86
|
+
expect(names).not.toContain("cursor")
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test("returns empty array when nothing detected", async () => {
|
|
90
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "detect-none-"))
|
|
91
|
+
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "detect-none-cwd-"))
|
|
92
|
+
|
|
93
|
+
const names = await getDetectedTargetNames(tempHome, tempCwd)
|
|
94
|
+
expect(names).toEqual([])
|
|
95
|
+
})
|
|
96
|
+
})
|