fix(codex): support packaged lazycodex installs

This commit is contained in:
YeonGyu-Kim
2026-05-31 14:39:03 +09:00
parent 247bba39e5
commit d0d1735e6b
15 changed files with 335 additions and 19 deletions
+5 -2
View File
@@ -10,6 +10,7 @@ import type { InstalledPlugin, RunCommand } from "./types"
type LinkPlatform = NodeJS.Platform
export async function installCachedPlugin(input: {
readonly buildSource?: boolean
readonly codexHome: string
readonly marketplaceName: string
readonly name: string
@@ -17,8 +18,10 @@ export async function installCachedPlugin(input: {
readonly version: string
readonly runCommand: RunCommand
}): Promise<InstalledPlugin> {
await maybeRunNpmInstall(input.sourcePath, input.runCommand)
await maybeRunNpmBuild(input.sourcePath, input.runCommand)
if (input.buildSource !== false) {
await maybeRunNpmInstall(input.sourcePath, input.runCommand)
await maybeRunNpmBuild(input.sourcePath, input.runCommand)
}
const targetPath = join(input.codexHome, "plugins", "cache", input.marketplaceName, input.name, input.version)
await replaceDirectory(input.sourcePath, targetPath)
@@ -0,0 +1,18 @@
import { existsSync } from "node:fs"
import { readFile } from "node:fs/promises"
import { join } from "node:path"
const PACKAGED_CODEX_INSTALLER_NAMES = new Set(["@code-yeongyu/lazycodex", "lazycodex", "oh-my-opencode", "oh-my-openagent"])
export async function shouldBuildSourcePackages(repoRoot: string): Promise<boolean> {
if (existsSync(join(repoRoot, "src", "index.ts"))) return true
const packageJsonPath = join(repoRoot, "package.json")
if (!existsSync(packageJsonPath)) return true
const packageJson: unknown = JSON.parse(await readFile(packageJsonPath, "utf8"))
if (!isRecord(packageJson) || typeof packageJson.name !== "string") return true
return !PACKAGED_CODEX_INSTALLER_NAMES.has(packageJson.name)
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
@@ -0,0 +1,40 @@
/// <reference path="../../../bun-test.d.ts" />
/// <reference types="bun-types" />
import { describe, expect, test } from "bun:test"
import { mkdtemp, readFile, stat } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { runCodexInstaller } from "./install-codex"
type CachedMcpManifest = {
readonly mcpServers: {
readonly ast_grep: { readonly args: readonly string[] }
readonly context7: { readonly url: string }
readonly grep_app: { readonly url: string }
}
}
describe("install-codex MCP manifest", () => {
test("#given codex installer #when installing omo #then caches research and structural-search MCPs", async () => {
// given
const codexHome = await mkdtemp(join(tmpdir(), "omo-codex-home-mcp-"))
const binDir = await mkdtemp(join(tmpdir(), "omo-codex-bin-mcp-"))
// when
const result = await runCodexInstaller({
codexHome,
binDir,
repoRoot: process.cwd(),
runCommand: async () => undefined,
})
// then
const pluginPath = result.installed[0]?.path ?? ""
const manifest = JSON.parse(await readFile(join(pluginPath, ".mcp.json"), "utf8")) as CachedMcpManifest
expect(manifest.mcpServers.grep_app.url).toBe("https://mcp.grep.app")
expect(manifest.mcpServers.context7.url).toBe("https://mcp.context7.com/mcp")
expect(manifest.mcpServers.ast_grep.args[0]).toBe(join(pluginPath, "components", "ast-grep-mcp", "dist", "cli.js"))
expect((await stat(manifest.mcpServers.ast_grep.args[0] ?? "")).isFile()).toBe(true)
})
})
@@ -0,0 +1,72 @@
/// <reference path="../../../bun-test.d.ts" />
/// <reference types="bun-types" />
import { expect, test } from "bun:test"
import { mkdir, mkdtemp, readFile, readlink, stat, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { runCodexInstaller } from "./install-codex"
test("#given packaged lazycodex tarball layout #when installing Codex plugin #then uses bundled artifacts without source builds", async () => {
// given
const repoRoot = await mkdtemp(join(tmpdir(), "omo-codex-packaged-root-"))
const codexHome = await mkdtemp(join(tmpdir(), "omo-codex-packaged-home-"))
const binDir = await mkdtemp(join(tmpdir(), "omo-codex-packaged-bin-"))
const codexPackageRoot = join(repoRoot, "packages", "omo-codex")
const pluginRoot = join(codexPackageRoot, "plugin")
const lspRuntimeRoot = join(repoRoot, "packages", "lsp-tools-mcp")
const commands: Array<readonly [string, string, string]> = []
await writeFile(join(repoRoot, "package.json"), JSON.stringify({ name: "oh-my-opencode", version: "4.5.12" }))
await mkdir(join(pluginRoot, ".codex-plugin"), { recursive: true })
await mkdir(join(pluginRoot, "dist"), { recursive: true })
await mkdir(join(lspRuntimeRoot, "dist"), { recursive: true })
await writeFile(
join(codexPackageRoot, "marketplace.json"),
JSON.stringify({ name: "sisyphuslabs", plugins: [{ name: "omo", source: "./plugin" }] }),
)
await writeFile(
join(pluginRoot, ".codex-plugin", "plugin.json"),
JSON.stringify({ name: "omo", version: "0.1.0", hooks: "hooks/hooks.json" }),
)
await writeFile(
join(pluginRoot, "package.json"),
JSON.stringify({
name: "@sisyphuslabs/omo-codex-plugin",
version: "0.1.0",
bin: { omo: "dist/cli.js" },
scripts: { build: "exit 42" },
}),
)
await writeFile(
join(pluginRoot, ".mcp.json"),
JSON.stringify({ mcpServers: { lsp: { command: "node", args: ["../../lsp-tools-mcp/dist/cli.js", "mcp"], cwd: "." } } }),
)
await writeFile(join(pluginRoot, "dist", "cli.js"), "#!/usr/bin/env node\n")
await writeFile(join(lspRuntimeRoot, "dist", "cli.js"), "#!/usr/bin/env node\n")
// when
const result = await runCodexInstaller({
codexHome,
binDir,
repoRoot,
platform: "linux",
runCommand: async (command, args, options) => {
commands.push([command, args.join(" "), options.cwd])
},
})
// then
const pluginPath = result.installed[0]?.path ?? ""
const cachedMcp = JSON.parse(await readFile(join(pluginPath, ".mcp.json"), "utf8")) as {
readonly mcpServers: { readonly lsp: { readonly args: readonly string[]; readonly cwd?: string } }
}
const cachedLspCli = join(pluginPath, "components", "lsp-tools-mcp", "dist", "cli.js")
expect(commands).toEqual([["npm", "install --omit=dev", pluginPath]])
expect(cachedMcp.mcpServers.lsp.cwd).toBeUndefined()
expect(cachedMcp.mcpServers.lsp.args).toEqual([cachedLspCli, "mcp"])
expect(cachedMcp.mcpServers.lsp.args[0]).not.toBe(join(lspRuntimeRoot, "dist", "cli.js"))
expect((await stat(cachedLspCli)).isFile()).toBe(true)
expect(await readlink(join(binDir, "omo"))).toBe(join(pluginPath, "dist", "cli.js"))
})
+7 -4
View File
@@ -3,6 +3,7 @@ import { join, resolve } from "node:path"
import { existsSync } from "node:fs"
import { mkdir, writeFile } from "node:fs/promises"
import { installCachedPlugin, linkCachedPluginBins, pruneMarketplaceCache, pruneMarketplacePluginCaches } from "./codex-cache"
import { shouldBuildSourcePackages } from "./codex-package-layout"
import { updateCodexConfig } from "./codex-config-toml"
import { trustedHookStatesForPlugin } from "./codex-hook-trust"
import { prepareGitBashForInstall, resolveGitBashForCurrentProcess } from "./git-bash"
@@ -22,6 +23,7 @@ export async function runCodexInstaller(options: CodexInstallOptions = {}): Prom
const binDir = resolveCodexInstallerBinDir({ binDir: options.binDir, codexHome, env })
const runCommand = options.runCommand ?? defaultRunCommand
const log = options.log ?? (() => undefined)
const buildSource = await shouldBuildSourcePackages(repoRoot)
const gitBashResolution = await prepareGitBashForInstall({
platform,
@@ -58,6 +60,7 @@ export async function runCodexInstaller(options: CodexInstallOptions = {}): Prom
log(`Building ${entry.name}@${version}`)
const plugin = await installCachedPlugin({
buildSource,
codexHome,
marketplaceName: marketplace.name,
name: entry.name,
@@ -210,10 +213,6 @@ function legacyCacheMarketplaces(marketplaceName: string): readonly string[] {
return marketplaceName === "sisyphuslabs" ? SISYPHUS_LEGACY_CACHE_MARKETPLACES : []
}
function codexMarketplaceSource(marketplaceRoot: string): CodexMarketplaceSource {
return { sourceType: "local", source: marketplaceRoot }
}
export function findRepoRootFromImporter(importerDir: string): string {
let current = importerDir
for (let depth = 0; depth <= 5; depth += 1) {
@@ -248,6 +247,10 @@ function existsSyncLike(path: string): boolean {
return existsSync(path)
}
function codexMarketplaceSource(marketplaceRoot: string): CodexMarketplaceSource {
return { sourceType: "local", source: marketplaceRoot }
}
async function trackCodexInstallTelemetry(): Promise<void> {
try {
const { createInstallPostHog, getPostHogDistinctId } = await import("@oh-my-opencode/omo-codex/telemetry")