fix(codex): support packaged lazycodex installs
This commit is contained in:
@@ -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"))
|
||||
})
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user