feat: add OpenCode/Codex outputs and update changelog (#104)
* Add OpenCode converter coverage and specs * Add Codex target support and spec docs * Generate Codex command skills and refresh spec docs * Add global Codex install path * fix: harden plugin path loading and codex descriptions * feat: ensure codex agents block on convert/install * docs: clarify target branch usage for review * chore: prep npm package metadata and release notes * docs: mention opencode and codex in changelog * docs: update CLI usage and remove stale todos * feat: install from GitHub with global outputs
This commit is contained in:
289
tests/cli.test.ts
Normal file
289
tests/cli.test.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
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 ~/.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")
|
||||
expect(await exists(path.join(tempRoot, ".opencode", "opencode.json"))).toBe(true)
|
||||
expect(await exists(path.join(tempRoot, ".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")
|
||||
expect(await exists(path.join(tempRoot, ".opencode", "opencode.json"))).toBe(true)
|
||||
expect(await exists(path.join(tempRoot, ".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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user