diff --git a/packages/lsp-tools-mcp b/packages/lsp-tools-mcp index 83e634172..605ecfe3b 160000 --- a/packages/lsp-tools-mcp +++ b/packages/lsp-tools-mcp @@ -1 +1 @@ -Subproject commit 83e634172ebc79662c6e29d8b3ae89f927b10364 +Subproject commit 605ecfe3bcb93dfbd502624d59d24c2f7ec75145 diff --git a/packages/omo-codex/scripts/install-local.mjs b/packages/omo-codex/scripts/install-local.mjs index 2671f5971..cb52e5740 100644 --- a/packages/omo-codex/scripts/install-local.mjs +++ b/packages/omo-codex/scripts/install-local.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { mkdir, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -99,10 +100,17 @@ export async function installMarketplaceLocally(options = {}) { for (const legacyMarketplaceName of legacyCacheMarketplaces(marketplace.name)) { await pruneMarketplacePluginCaches({ codexHome, marketplaceName: legacyMarketplaceName, pluginNames }); } + const marketplaceRoot = join(codexHome, "plugins", "cache", marketplace.name); + await writeCachedMarketplaceManifest({ + marketplaceName: marketplace.name, + marketplaceRoot, + plugins: installed, + }); await updateCodexConfig({ configPath: join(codexHome, "config.toml"), repoRoot: codexPackageRoot, marketplaceName: marketplace.name, + marketplaceSource: { sourceType: "local", source: marketplaceRoot }, pluginNames, trustedHookStates, agentConfigs: [...agentConfigs.values()].sort((left, right) => left.name.localeCompare(right.name)), @@ -119,6 +127,25 @@ function agentNameFromToml(fileName) { return fileName.endsWith(".toml") ? fileName.slice(0, -".toml".length) : fileName; } +async function writeCachedMarketplaceManifest({ marketplaceName, marketplaceRoot, plugins }) { + const marketplaceDir = join(marketplaceRoot, ".agents", "plugins"); + await mkdir(marketplaceDir, { recursive: true }); + await writeFile( + join(marketplaceDir, "marketplace.json"), + `${JSON.stringify( + { + name: marketplaceName, + plugins: plugins.map((plugin) => ({ + name: plugin.name, + source: { source: "local", path: `./${plugin.name}/${plugin.version}` }, + })), + }, + null, + "\t", + )}\n`, + ); +} + function nonEmptyEnvValue(env, key) { const value = env[key]; if (typeof value !== "string") return undefined; diff --git a/packages/omo-codex/scripts/install-local.test.mjs b/packages/omo-codex/scripts/install-local.test.mjs index 310297754..b66d51093 100644 --- a/packages/omo-codex/scripts/install-local.test.mjs +++ b/packages/omo-codex/scripts/install-local.test.mjs @@ -206,7 +206,7 @@ test("#given local marketplace #when installing #then copies versioned plugins a assert.doesNotMatch(config, /stale@debug-marketplace/); }); -test("#given sisyphuslabs marketplace #when installing #then registers sisyphuslabs omo git source", async () => { +test("#given sisyphuslabs marketplace #when installing #then registers the local built marketplace cache", async () => { const repoRoot = await makeTempDir(); const codexHome = await makeTempDir(); const codexPackageRoot = join(repoRoot, "packages", "omo-codex"); @@ -216,6 +216,17 @@ test("#given sisyphuslabs marketplace #when installing #then registers sisyphusl plugins: [{ name: "omo", source: "./plugins/omo" }], }); await writePluginAt(join(codexPackageRoot, "plugin"), "omo", "0.1.0"); + await mkdir(join(codexPackageRoot, "plugin", "components", "lsp", "dist"), { recursive: true }); + await writeFile(join(codexPackageRoot, "plugin", "components", "lsp", "dist", "cli.js"), "#!/usr/bin/env node\n"); + await writeJson(join(codexPackageRoot, "plugin", ".mcp.json"), { + mcpServers: { + lsp: { + command: "node", + args: ["./components/lsp/dist/cli.js", "mcp"], + cwd: ".", + }, + }, + }); await mkdir(join(codexHome, "plugins", "cache", legacyCodexPluginMarketplace, "omo", "0.1.0"), { recursive: true, }); @@ -257,14 +268,25 @@ test("#given sisyphuslabs marketplace #when installing #then registers sisyphusl const config = await readFile(join(codexHome, "config.toml"), "utf8"); assert.match(config, /\[marketplaces\.sisyphuslabs\]/); - assert.match(config, /source_type = "git"/); - assert.match(config, /source = "https:\/\/github\.com\/sisyphuslabs\/omo\.git"/); - assert.match(config, /ref = "main"/); + assert.match(config, /source_type = "local"/); + assert.match(config, new RegExp(`source = ${JSON.stringify(join(codexHome, "plugins", "cache", "sisyphuslabs")).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`)); + assert.doesNotMatch(config, /ref = "main"/); assert.match(config, /\[plugins\."omo@sisyphuslabs"\]\nenabled = true/); assert.doesNotMatch(config, /\[marketplaces\.lazycodex\]/); assert.doesNotMatch(config, new RegExp(legacyCodexPluginMarketplace)); assert.doesNotMatch(config, /lazycodex\.git/); - assert.doesNotMatch(config, /source_type = "local"/); + const marketplace = JSON.parse( + await readFile(join(codexHome, "plugins", "cache", "sisyphuslabs", ".agents", "plugins", "marketplace.json"), "utf8"), + ); + assert.deepEqual(marketplace.plugins, [{ name: "omo", source: { source: "local", path: "./omo/0.1.0" } }]); + const cachedMcp = JSON.parse( + await readFile(join(codexHome, "plugins", "cache", "sisyphuslabs", "omo", "0.1.0", ".mcp.json"), "utf8"), + ); + assert.equal( + cachedMcp.mcpServers.lsp.args[0], + join(codexHome, "plugins", "cache", "sisyphuslabs", "omo", "0.1.0", "components", "lsp", "dist", "cli.js"), + ); + assert.equal((await stat(cachedMcp.mcpServers.lsp.args[0])).isFile(), true); await assert.rejects( stat(join(codexHome, "plugins", "cache", legacyCodexPluginMarketplace, "omo")), /code: 'ENOENT'|ENOENT/, diff --git a/src/cli/install-codex/install-codex.test.ts b/src/cli/install-codex/install-codex.test.ts index da5e4c818..d24e3abc0 100644 --- a/src/cli/install-codex/install-codex.test.ts +++ b/src/cli/install-codex/install-codex.test.ts @@ -68,9 +68,9 @@ describe("install-codex", () => { const configContent = await readFile(join(codexHome, "config.toml"), "utf8") expect(configContent).toContain("[features]") expect(configContent).toContain("[marketplaces.sisyphuslabs]") - expect(configContent).toContain('source_type = "git"') - expect(configContent).toContain('source = "https://github.com/code-yeongyu/lazycodex.git"') - expect(configContent).toContain('ref = "main"') + expect(configContent).toContain('source_type = "local"') + expect(configContent).toContain(`source = "${join(codexHome, "plugins", "cache", "sisyphuslabs")}"`) + expect(configContent).not.toContain('ref = "main"') expect(configContent).toContain("[plugins.\"omo@sisyphuslabs\"]") expect(configContent).toContain("[hooks.state.") expect(configContent).toContain("[agents.explorer]") @@ -87,9 +87,18 @@ describe("install-codex", () => { expect(pluginPath).toContain(join("plugins", "cache", "sisyphuslabs", "omo")) const stats = await stat(pluginPath ?? "") expect(stats.isDirectory()).toBe(true) + const mcpManifest = JSON.parse(await readFile(join(pluginPath ?? "", ".mcp.json"), "utf8")) as { + mcpServers: { lsp: { args: string[] } } + } + expect(mcpManifest.mcpServers.lsp.args[0]).toBe(join(pluginPath ?? "", "components", "lsp", "dist", "cli.js")) + expect((await stat(mcpManifest.mcpServers.lsp.args[0] ?? "")).isFile()).toBe(true) expect((await stat(join(codexHome, "agents", "explorer.toml"))).isFile()).toBe(true) expect((await stat(join(codexHome, "agents", "librarian.toml"))).isFile()).toBe(true) expect((await stat(join(codexHome, "agents", "plan.toml"))).isFile()).toBe(true) + const marketplace = JSON.parse( + await readFile(join(codexHome, "plugins", "cache", "sisyphuslabs", ".agents", "plugins", "marketplace.json"), "utf8"), + ) as { plugins: Array<{ name: string; source: { source: string; path: string } }> } + expect(marketplace.plugins).toEqual([{ name: "omo", source: { source: "local", path: "./omo/0.1.0" } }]) await expect(stat(join(codexHome, "plugins", "cache", "code-yeongyu-codex-plugins", "omo"))).rejects.toThrow() }) }) diff --git a/src/cli/install-codex/install-codex.ts b/src/cli/install-codex/install-codex.ts index ec9d341c3..60d16da5a 100644 --- a/src/cli/install-codex/install-codex.ts +++ b/src/cli/install-codex/install-codex.ts @@ -1,6 +1,7 @@ import { homedir } from "node:os" 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 { updateCodexConfig } from "./codex-config-toml" import { trustedHookStatesForPlugin } from "./codex-hook-trust" @@ -9,11 +10,6 @@ import { readMarketplace, readPluginManifest, resolvePluginSource, validatePathS import { defaultRunCommand } from "./codex-process" import type { CodexInstallOptions, CodexInstallResult, InstalledPlugin } from "./types" -const LAZYCODEX_MARKETPLACE_SOURCE = { - sourceType: "git", - 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 { @@ -90,12 +86,19 @@ export async function runCodexInstaller(options: CodexInstallOptions = {}): Prom }) } + const marketplaceRoot = join(codexHome, "plugins", "cache", marketplace.name) + await writeCachedMarketplaceManifest({ + marketplaceName: marketplace.name, + marketplaceRoot, + plugins: installed, + }) + const configPath = join(codexHome, "config.toml") await updateCodexConfig({ configPath, repoRoot: codexPackageRoot, marketplaceName: marketplace.name, - marketplaceSource: LAZYCODEX_MARKETPLACE_SOURCE, + marketplaceSource: { sourceType: "local", source: marketplaceRoot }, pluginNames: marketplace.plugins.map((plugin) => plugin.name), trustedHookStates, agentConfigs: [...agentConfigs.values()].sort((left, right) => left.name.localeCompare(right.name)), @@ -131,6 +134,29 @@ function agentNameFromToml(fileName: string): string { return fileName.endsWith(".toml") ? fileName.slice(0, -".toml".length) : fileName } +async function writeCachedMarketplaceManifest(input: { + readonly marketplaceName: string + readonly marketplaceRoot: string + readonly plugins: readonly InstalledPlugin[] +}): Promise { + const marketplaceDir = join(input.marketplaceRoot, ".agents", "plugins") + await mkdir(marketplaceDir, { recursive: true }) + await writeFile( + join(marketplaceDir, "marketplace.json"), + `${JSON.stringify( + { + name: input.marketplaceName, + plugins: input.plugins.map((plugin) => ({ + name: plugin.name, + source: { source: "local", path: `./${plugin.name}/${plugin.version}` }, + })), + }, + null, + "\t", + )}\n`, + ) +} + function legacyCacheMarketplaces(marketplaceName: string): readonly string[] { return marketplaceName === "sisyphuslabs" ? SISYPHUS_LEGACY_CACHE_MARKETPLACES : [] }