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 { try { await fs.access(filePath) return true } catch { return false } } async function runGit(args: string[], cwd: string, env?: NodeJS.ProcessEnv): Promise { 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("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", "repo-research-analyst.md"))).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", "workflows-review", "SKILL.md"))).toBe(true) 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", "workflows-review", "SKILL.md"))).toBe(true) expect(await exists(path.join(codexRoot, "skills", "skill-one", "SKILL.md"))).toBe(true) expect(await exists(path.join(codexRoot, "AGENTS.md"))).toBe(true) }) })