fix(omo-codex): reuse shared lsp mcp

This commit is contained in:
YeonGyu-Kim
2026-05-28 15:30:24 +09:00
parent 4f75a56ce5
commit 615ed40ba3
79 changed files with 378 additions and 5510 deletions
+15
View File
@@ -42,6 +42,21 @@ export async function pruneMarketplaceCache(input: {
}
}
export async function pruneMarketplacePluginCaches(input: {
readonly codexHome: string
readonly marketplaceName: string
readonly pluginNames: readonly string[]
}): Promise<void> {
const cacheRoot = join(input.codexHome, "plugins", "cache", input.marketplaceName)
if (!(await exists(cacheRoot))) return
for (const pluginName of input.pluginNames) {
await rm(join(cacheRoot, pluginName), { recursive: true, force: true })
}
if ((await readdir(cacheRoot)).length === 0) {
await rm(cacheRoot, { recursive: true, force: true })
}
}
export async function linkCachedPluginBins(input: {
readonly binDir: string
readonly pluginRoot: string
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { mkdtemp, readFile } from "node:fs/promises"
import { mkdtemp, readFile, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { updateCodexConfig } from "./codex-config-toml"
@@ -9,6 +9,25 @@ describe("codex-config-toml", () => {
// given
const root = await mkdtemp(join(tmpdir(), "omo-codex-config-"))
const configPath = join(root, "config.toml")
await writeFile(
configPath,
[
"[marketplaces.code-yeongyu-codex-plugins]",
'last_updated = "2026-05-01T00:00:00Z"',
'source_type = "git"',
'source = "https://github.com/code-yeongyu/codex-plugins.git"',
"",
'[plugins."omo@code-yeongyu-codex-plugins"]',
"enabled = true",
"",
'[plugins."omo@code-yeongyu-codex-plugins".mcp_servers.lsp]',
"enabled = true",
"",
'[hooks.state."omo@code-yeongyu-codex-plugins:hooks/hooks.json:post_tool_use:0:0"]',
'trusted_hash = "sha256:old"',
"",
].join("\n"),
)
// when
await updateCodexConfig({
+36 -3
View File
@@ -2,6 +2,8 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"
import { dirname } from "node:path"
import type { CodexMarketplaceSource, TrustedHookState } from "./types"
const SISYPHUS_LEGACY_MARKETPLACES = ["lazycodex", "code-yeongyu-codex-plugins"] as const
export async function updateCodexConfig(input: {
readonly configPath: string
readonly repoRoot: string
@@ -15,6 +17,11 @@ export async function updateCodexConfig(input: {
if (await exists(input.configPath)) config = await readFile(input.configPath, "utf8")
const pluginSet = new Set(input.pluginNames)
for (const legacyMarketplaceName of legacyMarketplaceNames(input.marketplaceName)) {
config = removeMarketplaceBlock(config, legacyMarketplaceName)
config = removeStaleMarketplacePluginBlocks(config, legacyMarketplaceName, new Set())
config = removeStaleMarketplaceHookStateBlocks(config, legacyMarketplaceName, new Set())
}
config = removeStaleMarketplacePluginBlocks(config, input.marketplaceName, pluginSet)
config = removeStaleMarketplaceHookStateBlocks(config, input.marketplaceName, pluginSet)
config = ensureFeatureEnabled(config, "plugins")
@@ -30,9 +37,17 @@ export async function updateCodexConfig(input: {
await writeFile(input.configPath, `${config.trimEnd()}\n`)
}
function legacyMarketplaceNames(marketplaceName: string): readonly string[] {
return marketplaceName === "sisyphuslabs" ? SISYPHUS_LEGACY_MARKETPLACES : []
}
function removeMarketplaceBlock(config: string, marketplaceName: string): string {
return removeTomlSections(config, (header) => header === `marketplaces.${marketplaceName}`)
}
function removeStaleMarketplacePluginBlocks(config: string, marketplaceName: string, keepPluginNames: Set<string>): string {
return removeTomlSections(config, (header) => {
const pluginKey = parseQuotedPluginHeader(header)
const pluginKey = parsePluginHeaderKey(header)
if (pluginKey === null) return false
const suffix = `@${marketplaceName}`
if (!pluginKey.endsWith(suffix)) return false
@@ -158,10 +173,28 @@ function parseTomlHeader(line: string): string | null {
return trimmed.slice(1, -1)
}
function parseQuotedPluginHeader(header: string): string | null {
function parsePluginHeaderKey(header: string): string | null {
const prefix = "plugins."
if (!header.startsWith(prefix)) return null
return parseJsonString(header.slice(prefix.length))
return parseLeadingJsonString(header.slice(prefix.length))
}
function parseLeadingJsonString(value: string): string | null {
if (!value.startsWith('"')) return parseJsonString(value)
let escaped = false
for (let index = 1; index < value.length; index += 1) {
const char = value[index]
if (escaped) {
escaped = false
continue
}
if (char === "\\") {
escaped = true
continue
}
if (char === '"') return parseJsonString(value.slice(0, index + 1))
}
return null
}
function parseJsonString(value: string): string | null {
+5 -1
View File
@@ -2,7 +2,7 @@
/// <reference types="bun-types" />
import { describe, expect, test } from "bun:test"
import { mkdtemp, readFile, stat } from "node:fs/promises"
import { mkdir, mkdtemp, readFile, stat, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { runCodexInstaller } from "./install-codex"
@@ -13,6 +13,9 @@ describe("install-codex", () => {
const codexHome = await mkdtemp(join(tmpdir(), "omo-codex-home-"))
const binDir = await mkdtemp(join(tmpdir(), "omo-codex-bin-"))
const repoRoot = process.cwd()
const legacyCacheRoot = join(codexHome, "plugins", "cache", "code-yeongyu-codex-plugins", "omo", "0.1.0")
await mkdir(legacyCacheRoot, { recursive: true })
await writeFile(join(legacyCacheRoot, ".mcp.json"), JSON.stringify({ mcpServers: { lsp: { args: ["old-lsp"] } } }))
// when
const first = await runCodexInstaller({ codexHome, binDir, repoRoot, runCommand: async () => undefined })
@@ -37,5 +40,6 @@ describe("install-codex", () => {
expect(pluginPath).toContain(join("plugins", "cache", "sisyphuslabs", "omo"))
const stats = await stat(pluginPath ?? "")
expect(stats.isDirectory()).toBe(true)
await expect(stat(join(codexHome, "plugins", "cache", "code-yeongyu-codex-plugins", "omo"))).rejects.toThrow()
})
})
+13 -1
View File
@@ -1,7 +1,7 @@
import { homedir } from "node:os"
import { join, resolve } from "node:path"
import { existsSync } from "node:fs"
import { installCachedPlugin, linkCachedPluginBins, pruneMarketplaceCache } from "./codex-cache"
import { installCachedPlugin, linkCachedPluginBins, pruneMarketplaceCache, pruneMarketplacePluginCaches } from "./codex-cache"
import { updateCodexConfig } from "./codex-config-toml"
import { trustedHookStatesForPlugin } from "./codex-hook-trust"
import { linkCachedPluginAgents } from "./link-cached-plugin-agents"
@@ -14,6 +14,7 @@ const LAZYCODEX_MARKETPLACE_SOURCE = {
source: "https://github.com/code-yeongyu/lazycodex.git",
ref: "main",
} as const
const SISYPHUS_LEGACY_CACHE_MARKETPLACES = ["lazycodex", "code-yeongyu-codex-plugins"] as const
export async function runCodexInstaller(options: CodexInstallOptions = {}): Promise<CodexInstallResult> {
const repoRoot = resolve(options.repoRoot ?? findRepoRootFromImporter(import.meta.dir))
@@ -78,6 +79,13 @@ export async function runCodexInstaller(options: CodexInstallOptions = {}): Prom
marketplaceName: marketplace.name,
keepPluginNames: marketplace.plugins.map((plugin) => plugin.name),
})
for (const legacyMarketplaceName of legacyCacheMarketplaces(marketplace.name)) {
await pruneMarketplacePluginCaches({
codexHome,
marketplaceName: legacyMarketplaceName,
pluginNames: marketplace.plugins.map((plugin) => plugin.name),
})
}
const configPath = join(codexHome, "config.toml")
await updateCodexConfig({
@@ -99,6 +107,10 @@ export async function runCodexInstaller(options: CodexInstallOptions = {}): Prom
}
}
function legacyCacheMarketplaces(marketplaceName: string): readonly string[] {
return marketplaceName === "sisyphuslabs" ? SISYPHUS_LEGACY_CACHE_MARKETPLACES : []
}
function findRepoRootFromImporter(importerDir: string): string {
let current = importerDir
for (let depth = 0; depth <= 5; depth += 1) {