From f819e435a54f5d7df558df5a6bee1e616a5da837 Mon Sep 17 00:00:00 2001 From: thefutureisw0rk Date: Wed, 25 Mar 2026 21:39:45 -0700 Subject: [PATCH] fix: one-step codex installs by preferring bundled plugins (#383) Co-authored-by: The Future is Work --- src/commands/install.ts | 18 +++++++++++++++++- tests/cli.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/commands/install.ts b/src/commands/install.ts index a1f2f1c..4978037 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -2,6 +2,7 @@ import { defineCommand } from "citty" import { promises as fs } from "fs" import os from "os" import path from "path" +import { fileURLToPath } from "url" import { loadClaudePlugin } from "../parsers/claude" import { targets, validateScope } from "../targets" import { pathExists } from "../utils/files" @@ -233,7 +234,12 @@ async function resolvePluginPath(input: string): Promise { throw new Error(`Local plugin path not found: ${directPath}`) } - // Otherwise, always fetch the latest from GitHub + const bundledPluginPath = await resolveBundledPluginPath(input) + if (bundledPluginPath) { + return { path: bundledPluginPath } + } + + // Otherwise, fetch the latest from GitHub return await resolveGitHubPluginPath(input) } @@ -255,6 +261,16 @@ function resolveOutputRoot(value: unknown): string { return path.join(os.homedir(), ".config", "opencode") } +async function resolveBundledPluginPath(pluginName: string): Promise { + const bundledRoot = fileURLToPath(new URL("../../plugins/", import.meta.url)) + const pluginPath = path.join(bundledRoot, pluginName) + const manifestPath = path.join(pluginPath, ".claude-plugin", "plugin.json") + if (await pathExists(manifestPath)) { + return pluginPath + } + return null +} + async function resolveGitHubPluginPath(pluginName: string): Promise { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compound-plugin-")) const source = resolveGitHubSource() diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 390d06c..af9f6c3 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -180,6 +180,46 @@ describe("CLI", () => { expect(await exists(path.join(tempRoot, ".config", "opencode", "agents", "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, "prompts", "ce-plan.md"))).toBe(true) + expect(await exists(path.join(codexRoot, "skills", "ce:plan", "SKILL.md"))).toBe(true) + 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-"))