fix(omo-codex): reuse shared lsp mcp
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user