Files
compound-engineering-plugin…/tests/cli.test.ts
T
Trevin Chow 87ca621f34 refactor(cli): remove unsupported installer surfaces
Drop the personal Claude home sync command and remove custom writer support for native or deprecated targets. Keep install auto-detection for supported custom targets, document native marketplace paths, and add legacy cleanup coverage so stale CE artifacts from old installs can be backed up safely.
2026-04-20 01:15:25 -07:00

842 lines
30 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { promises as fs } from "fs"
import path from "path"
import os from "os"
async function exists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}
async function runGit(args: string[], cwd: string, env?: NodeJS.ProcessEnv): Promise<void> {
const proc = Bun.spawn(["git", ...args], {
cwd,
stdout: "pipe",
stderr: "pipe",
env: env ?? process.env,
})
const exitCode = await proc.exited
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`git ${args.join(" ")} failed (exit ${exitCode}).\nstderr: ${stderr}`)
}
}
describe("CLI", () => {
test("install converts fixture plugin to OpenCode output", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-opencode-"))
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const proc = Bun.spawn([
"bun",
"run",
"src/index.ts",
"install",
fixtureRoot,
"--to",
"opencode",
"--output",
tempRoot,
], {
cwd: path.join(import.meta.dir, ".."),
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Installed compound-engineering")
expect(await exists(path.join(tempRoot, "opencode.json"))).toBe(true)
expect(await exists(path.join(tempRoot, ".opencode", "agents", "repo-research-analyst.md"))).toBe(true)
expect(await exists(path.join(tempRoot, ".opencode", "agents", "security-sentinel.md"))).toBe(true)
expect(await exists(path.join(tempRoot, ".opencode", "skills", "skill-one", "SKILL.md"))).toBe(true)
expect(await exists(path.join(tempRoot, ".opencode", "plugins", "converted-hooks.ts"))).toBe(true)
})
test("install defaults output to ~/.config/opencode", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-local-default-"))
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const repoRoot = path.join(import.meta.dir, "..")
const proc = Bun.spawn([
"bun",
"run",
path.join(repoRoot, "src", "index.ts"),
"install",
fixtureRoot,
"--to",
"opencode",
], {
cwd: tempRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
HOME: tempRoot,
},
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Installed compound-engineering")
// OpenCode global config lives at ~/.config/opencode per XDG spec
expect(await exists(path.join(tempRoot, ".config", "opencode", "opencode.json"))).toBe(true)
expect(await exists(path.join(tempRoot, ".config", "opencode", "agents", "repo-research-analyst.md"))).toBe(true)
})
test("install rejects native marketplace-only plugin targets", async () => {
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const repoRoot = path.join(import.meta.dir, "..")
for (const target of ["copilot", "droid"]) {
const proc = Bun.spawn([
"bun",
"run",
path.join(repoRoot, "src", "index.ts"),
"install",
fixtureRoot,
"--to",
target,
], {
cwd: repoRoot,
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stderr = await new Response(proc.stderr).text()
expect(exitCode).not.toBe(0)
expect(stderr).toContain(`Unknown target: ${target}`)
}
})
test("cleanup backs up legacy Codex artifacts on demand", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-cleanup-codex-"))
const codexRoot = path.join(tempRoot, ".codex")
const agentsRoot = path.join(tempRoot, ".agents")
const repoRoot = path.join(import.meta.dir, "..")
await fs.mkdir(path.join(codexRoot, "skills", "ce:plan"), { recursive: true })
await fs.writeFile(path.join(codexRoot, "skills", "ce:plan", "SKILL.md"), "legacy raw colon skill")
await fs.mkdir(path.join(codexRoot, "skills", "ce:review-beta"), { recursive: true })
await fs.writeFile(path.join(codexRoot, "skills", "ce:review-beta", "SKILL.md"), "legacy raw colon beta skill")
await fs.mkdir(path.join(codexRoot, "prompts"), { recursive: true })
await fs.writeFile(path.join(codexRoot, "prompts", "report-bug.md"), "legacy prompt")
await fs.mkdir(path.join(agentsRoot, "skills", "ce-plan"), { recursive: true })
await fs.writeFile(path.join(agentsRoot, "skills", "ce-plan", "SKILL.md"), "legacy shared skill")
const proc = Bun.spawn([
"bun",
"run",
path.join(repoRoot, "src", "index.ts"),
"cleanup",
"--target",
"codex",
"--codex-home",
codexRoot,
"--agents-home",
agentsRoot,
], {
cwd: repoRoot,
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Cleaned codex")
expect(stdout).toContain("backed up 4 artifact")
expect(await exists(path.join(codexRoot, "skills", "ce:plan"))).toBe(false)
expect(await exists(path.join(codexRoot, "skills", "ce:review-beta"))).toBe(false)
expect(await exists(path.join(codexRoot, "prompts", "report-bug.md"))).toBe(false)
expect(await exists(path.join(agentsRoot, "skills", "ce-plan"))).toBe(false)
expect(await exists(path.join(codexRoot, "compound-engineering", "legacy-backup"))).toBe(true)
expect(await exists(path.join(agentsRoot, "compound-engineering", "legacy-backup"))).toBe(true)
})
test("cleanup backs up legacy Copilot workspace artifacts for native migration", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-cleanup-copilot-"))
const repoRoot = path.join(import.meta.dir, "..")
const githubRoot = path.join(tempRoot, ".github")
await fs.mkdir(path.join(githubRoot, "skills", "git-commit-push-pr"), { recursive: true })
await fs.writeFile(path.join(githubRoot, "skills", "git-commit-push-pr", "SKILL.md"), "legacy skill")
await fs.mkdir(path.join(githubRoot, "skills", "ce-plan"), { recursive: true })
await fs.writeFile(path.join(githubRoot, "skills", "ce-plan", "SKILL.md"), "current CE skill from old manual install")
await fs.mkdir(path.join(githubRoot, "agents"), { recursive: true })
await fs.writeFile(path.join(githubRoot, "agents", "repo-research-analyst.agent.md"), "legacy agent")
const proc = Bun.spawn([
"bun",
"run",
path.join(repoRoot, "src", "index.ts"),
"cleanup",
"--target",
"copilot",
"--output",
tempRoot,
], {
cwd: repoRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
HOME: tempRoot,
},
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Cleaned copilot")
expect(await exists(path.join(githubRoot, "skills", "git-commit-push-pr"))).toBe(false)
expect(await exists(path.join(githubRoot, "skills", "ce-plan"))).toBe(false)
expect(await exists(path.join(githubRoot, "agents", "repo-research-analyst.agent.md"))).toBe(false)
expect(await exists(path.join(githubRoot, "compound-engineering", "legacy-backup"))).toBe(true)
})
test("cleanup backs up deprecated Windsurf artifacts", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-cleanup-windsurf-"))
const windsurfRoot = path.join(tempRoot, ".codeium", "windsurf")
const repoRoot = path.join(import.meta.dir, "..")
await fs.mkdir(path.join(windsurfRoot, "skills", "reproduce-bug"), { recursive: true })
await fs.writeFile(path.join(windsurfRoot, "skills", "reproduce-bug", "SKILL.md"), "legacy skill")
await fs.mkdir(path.join(windsurfRoot, "skills", "repo-research-analyst"), { recursive: true })
await fs.writeFile(path.join(windsurfRoot, "skills", "repo-research-analyst", "SKILL.md"), "legacy agent skill")
await fs.mkdir(path.join(windsurfRoot, "global_workflows"), { recursive: true })
await fs.writeFile(path.join(windsurfRoot, "global_workflows", "workflows-plan.md"), "legacy workflow")
const proc = Bun.spawn([
"bun",
"run",
path.join(repoRoot, "src", "index.ts"),
"cleanup",
"--target",
"windsurf",
"--windsurf-home",
windsurfRoot,
], {
cwd: repoRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
HOME: tempRoot,
},
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Cleaned windsurf")
expect(await exists(path.join(windsurfRoot, "skills", "reproduce-bug"))).toBe(false)
expect(await exists(path.join(windsurfRoot, "skills", "repo-research-analyst"))).toBe(false)
expect(await exists(path.join(windsurfRoot, "global_workflows", "workflows-plan.md"))).toBe(false)
expect(await exists(path.join(windsurfRoot, "compound-engineering", "legacy-backup"))).toBe(true)
})
test("list returns plugins in a temp workspace", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-list-"))
const pluginsRoot = path.join(tempRoot, "plugins", "demo-plugin", ".claude-plugin")
await fs.mkdir(pluginsRoot, { recursive: true })
await fs.writeFile(path.join(pluginsRoot, "plugin.json"), "{\n \"name\": \"demo-plugin\",\n \"version\": \"1.0.0\"\n}\n")
const repoRoot = path.join(import.meta.dir, "..")
const proc = Bun.spawn(["bun", "run", path.join(repoRoot, "src", "index.ts"), "list"], {
cwd: tempRoot,
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("demo-plugin")
})
test("install pulls from GitHub when local path is missing", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-github-install-"))
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-github-workspace-"))
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-github-repo-"))
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const pluginRoot = path.join(repoRoot, "plugins", "compound-engineering")
await fs.mkdir(path.dirname(pluginRoot), { recursive: true })
await fs.cp(fixtureRoot, pluginRoot, { recursive: true })
const gitEnv = {
...process.env,
GIT_AUTHOR_NAME: "Test",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Test",
GIT_COMMITTER_EMAIL: "test@example.com",
}
await runGit(["init"], repoRoot, gitEnv)
await runGit(["add", "."], repoRoot, gitEnv)
await runGit(["commit", "-m", "fixture"], repoRoot, gitEnv)
const projectRoot = path.join(import.meta.dir, "..")
const proc = Bun.spawn([
"bun",
"run",
path.join(projectRoot, "src", "index.ts"),
"install",
"compound-engineering",
"--to",
"opencode",
], {
cwd: workspaceRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
HOME: tempRoot,
COMPOUND_PLUGIN_GITHUB_SOURCE: repoRoot,
},
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Installed compound-engineering")
// OpenCode global config lives at ~/.config/opencode per XDG spec
expect(await exists(path.join(tempRoot, ".config", "opencode", "opencode.json"))).toBe(true)
expect(await exists(path.join(tempRoot, ".config", "opencode", "agents", "ce-repo-research-analyst.md"))).toBe(true)
})
test("install uses bundled compound-engineering plugin for codex output", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-bundled-codex-home-"))
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-bundled-codex-workspace-"))
const projectRoot = path.join(import.meta.dir, "..")
const codexRoot = path.join(tempRoot, ".codex")
const proc = Bun.spawn([
"bun",
"run",
path.join(projectRoot, "src", "index.ts"),
"install",
"compound-engineering",
"--to",
"codex",
], {
cwd: workspaceRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
HOME: tempRoot,
COMPOUND_PLUGIN_GITHUB_SOURCE: "/definitely-not-a-valid-plugin-source",
},
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Installed compound-engineering")
expect(stdout).toContain(codexRoot)
expect(await exists(path.join(codexRoot, "skills", "compound-engineering", "ce-plan", "SKILL.md"))).toBe(true)
expect(await exists(path.join(tempRoot, ".agents", "skills", "ce-plan"))).toBe(false)
expect(await exists(path.join(codexRoot, "AGENTS.md"))).toBe(true)
})
test("install by name ignores same-named local directory", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-shadow-"))
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-shadow-workspace-"))
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-shadow-repo-"))
// Create a directory with the plugin name that is NOT a valid plugin
const shadowDir = path.join(workspaceRoot, "compound-engineering")
await fs.mkdir(shadowDir, { recursive: true })
await fs.writeFile(path.join(shadowDir, "README.md"), "Not a plugin")
// Set up a fake GitHub source with a valid plugin
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const pluginRoot = path.join(repoRoot, "plugins", "compound-engineering")
await fs.mkdir(path.dirname(pluginRoot), { recursive: true })
await fs.cp(fixtureRoot, pluginRoot, { recursive: true })
const gitEnv = {
...process.env,
GIT_AUTHOR_NAME: "Test",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Test",
GIT_COMMITTER_EMAIL: "test@example.com",
}
await runGit(["init"], repoRoot, gitEnv)
await runGit(["add", "."], repoRoot, gitEnv)
await runGit(["commit", "-m", "fixture"], repoRoot, gitEnv)
const projectRoot = path.join(import.meta.dir, "..")
const proc = Bun.spawn([
"bun",
"run",
path.join(projectRoot, "src", "index.ts"),
"install",
"compound-engineering",
"--to",
"opencode",
"--output",
tempRoot,
], {
cwd: workspaceRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
HOME: tempRoot,
COMPOUND_PLUGIN_GITHUB_SOURCE: repoRoot,
},
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
// Should succeed by fetching from GitHub, NOT failing on the local shadow directory
expect(stdout).toContain("Installed compound-engineering")
expect(await exists(path.join(tempRoot, "opencode.json"))).toBe(true)
})
test("install --branch clones a specific branch for non-Claude targets", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-branch-install-"))
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-branch-repo-"))
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const pluginRoot = path.join(repoRoot, "plugins", "compound-engineering")
await fs.mkdir(path.dirname(pluginRoot), { recursive: true })
await fs.cp(fixtureRoot, pluginRoot, { recursive: true })
const gitEnv = {
...process.env,
GIT_AUTHOR_NAME: "Test",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Test",
GIT_COMMITTER_EMAIL: "test@example.com",
}
await runGit(["init", "-b", "main"], repoRoot, gitEnv)
await runGit(["add", "."], repoRoot, gitEnv)
await runGit(["commit", "-m", "initial"], repoRoot, gitEnv)
await runGit(["checkout", "-b", "feat/test-branch"], repoRoot, gitEnv)
await fs.writeFile(path.join(pluginRoot, "BRANCH_MARKER.txt"), "from-branch")
await runGit(["add", "."], repoRoot, gitEnv)
await runGit(["commit", "-m", "branch commit"], repoRoot, gitEnv)
await runGit(["checkout", "main"], repoRoot, gitEnv)
const projectRoot = path.join(import.meta.dir, "..")
const proc = Bun.spawn([
"bun",
"run",
path.join(projectRoot, "src", "index.ts"),
"install",
"compound-engineering",
"--to",
"opencode",
"--output",
tempRoot,
"--branch",
"feat/test-branch",
], {
cwd: tempRoot,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
HOME: tempRoot,
COMPOUND_PLUGIN_GITHUB_SOURCE: repoRoot,
},
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Installed compound-engineering")
expect(await exists(path.join(tempRoot, "opencode.json"))).toBe(true)
})
test("convert writes OpenCode output", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-convert-"))
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const proc = Bun.spawn([
"bun",
"run",
"src/index.ts",
"convert",
fixtureRoot,
"--to",
"opencode",
"--output",
tempRoot,
], {
cwd: path.join(import.meta.dir, ".."),
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Converted compound-engineering")
expect(await exists(path.join(tempRoot, "opencode.json"))).toBe(true)
})
test("convert supports --codex-home for codex output", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-codex-home-"))
const codexRoot = path.join(tempRoot, ".codex")
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const proc = Bun.spawn([
"bun",
"run",
"src/index.ts",
"convert",
fixtureRoot,
"--to",
"codex",
"--codex-home",
codexRoot,
], {
cwd: path.join(import.meta.dir, ".."),
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Converted compound-engineering")
expect(stdout).toContain(codexRoot)
expect(await exists(path.join(codexRoot, "prompts", "workflows-review.md"))).toBe(true)
expect(await exists(path.join(codexRoot, "skills", "compound-engineering", "workflows-review", "SKILL.md"))).toBe(true)
expect(await exists(path.join(tempRoot, ".agents", "skills", "workflows-review"))).toBe(false)
expect(await exists(path.join(codexRoot, "AGENTS.md"))).toBe(true)
})
test("install supports --also with codex output", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-also-"))
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const codexRoot = path.join(tempRoot, ".codex")
const proc = Bun.spawn([
"bun",
"run",
"src/index.ts",
"install",
fixtureRoot,
"--to",
"opencode",
"--also",
"codex",
"--codex-home",
codexRoot,
"--output",
tempRoot,
], {
cwd: path.join(import.meta.dir, ".."),
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Installed compound-engineering")
expect(stdout).toContain(codexRoot)
expect(await exists(path.join(codexRoot, "prompts", "workflows-review.md"))).toBe(true)
expect(await exists(path.join(codexRoot, "skills", "compound-engineering", "workflows-review", "SKILL.md"))).toBe(true)
expect(await exists(path.join(codexRoot, "skills", "compound-engineering", "skill-one", "SKILL.md"))).toBe(true)
expect(await exists(path.join(tempRoot, ".agents", "skills", "workflows-review"))).toBe(false)
expect(await exists(path.join(tempRoot, ".agents", "skills", "skill-one"))).toBe(false)
expect(await exists(path.join(codexRoot, "AGENTS.md"))).toBe(true)
})
test("convert supports --pi-home for pi output", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-pi-home-"))
const piRoot = path.join(tempRoot, ".pi")
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const proc = Bun.spawn([
"bun",
"run",
"src/index.ts",
"convert",
fixtureRoot,
"--to",
"pi",
"--pi-home",
piRoot,
], {
cwd: path.join(import.meta.dir, ".."),
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Converted compound-engineering")
expect(stdout).toContain(piRoot)
expect(await exists(path.join(piRoot, "prompts", "workflows-review.md"))).toBe(true)
expect(await exists(path.join(piRoot, "skills", "repo-research-analyst", "SKILL.md"))).toBe(true)
expect(await exists(path.join(piRoot, "extensions", "compound-engineering-compat.ts"))).toBe(true)
expect(await exists(path.join(piRoot, "compound-engineering", "mcporter.json"))).toBe(true)
})
test("install supports --also with pi output", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-also-pi-"))
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const piRoot = path.join(tempRoot, ".pi")
const proc = Bun.spawn([
"bun",
"run",
"src/index.ts",
"install",
fixtureRoot,
"--to",
"opencode",
"--also",
"pi",
"--pi-home",
piRoot,
"--output",
tempRoot,
], {
cwd: path.join(import.meta.dir, ".."),
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Installed compound-engineering")
expect(stdout).toContain(piRoot)
expect(await exists(path.join(piRoot, "prompts", "workflows-review.md"))).toBe(true)
expect(await exists(path.join(piRoot, "extensions", "compound-engineering-compat.ts"))).toBe(true)
})
test("install --to opencode uses permissions:none by default", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-perms-none-"))
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const proc = Bun.spawn([
"bun",
"run",
"src/index.ts",
"install",
fixtureRoot,
"--to",
"opencode",
"--output",
tempRoot,
], {
cwd: path.join(import.meta.dir, ".."),
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Installed compound-engineering")
const opencodeJsonPath = path.join(tempRoot, "opencode.json")
const content = await fs.readFile(opencodeJsonPath, "utf-8")
const json = JSON.parse(content)
expect(json).not.toHaveProperty("permission")
expect(json).not.toHaveProperty("tools")
})
test("install --to opencode --permissions broad writes permission block", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-perms-broad-"))
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const proc = Bun.spawn([
"bun",
"run",
"src/index.ts",
"install",
fixtureRoot,
"--to",
"opencode",
"--permissions",
"broad",
"--output",
tempRoot,
], {
cwd: path.join(import.meta.dir, ".."),
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Installed compound-engineering")
const opencodeJsonPath = path.join(tempRoot, "opencode.json")
const content = await fs.readFile(opencodeJsonPath, "utf-8")
const json = JSON.parse(content)
expect(json).toHaveProperty("permission")
expect(json.permission).not.toBeNull()
})
test("install --to all detects custom-install targets and ignores stale cursor directories", async () => {
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "cli-install-all-home-"))
const tempCwd = await fs.mkdtemp(path.join(os.tmpdir(), "cli-install-all-cwd-"))
const repoRoot = path.join(import.meta.dir, "..")
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
await fs.mkdir(path.join(tempHome, ".config", "opencode"), { recursive: true })
await fs.mkdir(path.join(tempHome, ".codex"), { recursive: true })
await fs.mkdir(path.join(tempHome, ".pi"), { recursive: true })
await fs.mkdir(path.join(tempHome, ".factory"), { recursive: true })
await fs.mkdir(path.join(tempHome, ".copilot"), { recursive: true })
await fs.mkdir(path.join(tempHome, ".gemini"), { recursive: true })
await fs.mkdir(path.join(tempHome, ".kiro"), { recursive: true })
await fs.mkdir(path.join(tempHome, ".qwen"), { recursive: true })
await fs.mkdir(path.join(tempCwd, ".cursor"), { recursive: true })
const proc = Bun.spawn([
"bun",
"run",
path.join(repoRoot, "src", "index.ts"),
"install",
fixtureRoot,
"--to",
"all",
], {
cwd: tempCwd,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
HOME: tempHome,
},
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
}
expect(stdout).toContain("Installed compound-engineering to codex")
expect(stdout).toContain("Installed compound-engineering to opencode")
expect(stdout).toContain("Installed compound-engineering to pi")
expect(stdout).toContain("Installed compound-engineering to kiro")
expect(stdout).toContain("Installed compound-engineering to qwen")
expect(stdout).toContain("Installed compound-engineering to gemini")
expect(stdout).toContain("droid — native plugin install; skipped")
expect(stdout).toContain("copilot — native plugin install; skipped")
expect(stdout).not.toContain("cursor")
expect(await exists(path.join(tempHome, ".config", "opencode", "opencode.json"))).toBe(true)
expect(await exists(path.join(tempHome, ".codex", "skills", "compound-engineering", "skill-one", "SKILL.md"))).toBe(true)
expect(await exists(path.join(tempHome, ".pi", "agent", "skills", "skill-one", "SKILL.md"))).toBe(true)
expect(await exists(path.join(tempCwd, ".gemini", "skills", "skill-one", "SKILL.md"))).toBe(true)
expect(await exists(path.join(tempCwd, ".kiro", "skills", "skill-one", "SKILL.md"))).toBe(true)
expect(await exists(path.join(tempHome, ".qwen", "extensions", "compound-engineering", "qwen-extension.json"))).toBe(true)
})
})