test(script): batch 28 (3 files)
This commit is contained in:
@@ -0,0 +1,123 @@
|
|||||||
|
import { readFile, readdir, stat } from "node:fs/promises"
|
||||||
|
import { dirname, join, resolve, sep } from "node:path"
|
||||||
|
|
||||||
|
export async function validateLazycodexPluginBundle(pluginRoot: string): Promise<void> {
|
||||||
|
await validatePluginMcpManifest(pluginRoot)
|
||||||
|
await validatePluginHookCommands(pluginRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validatePluginMcpManifest(pluginRoot: string): Promise<void> {
|
||||||
|
const manifestPath = join(pluginRoot, ".mcp.json")
|
||||||
|
if (!(await isFile(manifestPath))) return
|
||||||
|
|
||||||
|
const parsed: unknown = JSON.parse(await readFile(manifestPath, "utf8"))
|
||||||
|
if (!isRecord(parsed) || !isRecord(parsed.mcpServers)) return
|
||||||
|
|
||||||
|
for (const [serverName, server] of Object.entries(parsed.mcpServers)) {
|
||||||
|
if (!isRecord(server) || !Array.isArray(server.args)) continue
|
||||||
|
for (const arg of server.args) {
|
||||||
|
if (typeof arg !== "string" || !isPluginRuntimePathArg(arg)) continue
|
||||||
|
await validateRelativeBundleFile(pluginRoot, pluginRoot, arg, `missing MCP runtime path for ${serverName}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validatePluginHookCommands(pluginRoot: string): Promise<void> {
|
||||||
|
const hookManifestPaths = await findHookManifestPaths(pluginRoot)
|
||||||
|
for (const hookManifestPath of hookManifestPaths) {
|
||||||
|
const parsed: unknown = JSON.parse(await readFile(hookManifestPath, "utf8"))
|
||||||
|
const commands: string[] = []
|
||||||
|
const hookPluginRoot = dirname(dirname(hookManifestPath))
|
||||||
|
collectHookCommands(parsed, commands)
|
||||||
|
for (const command of commands) {
|
||||||
|
for (const relativePath of extractPluginRootPaths(command)) {
|
||||||
|
const hookCommandRoot = relativePath.startsWith("components/") ? pluginRoot : hookPluginRoot
|
||||||
|
await validateRelativeBundleFile(pluginRoot, hookCommandRoot, relativePath, "missing hook command target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findHookManifestPaths(root: string): Promise<string[]> {
|
||||||
|
const entries = await readdir(root, { withFileTypes: true })
|
||||||
|
const paths: string[] = []
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.name === "node_modules" || entry.name === ".git") continue
|
||||||
|
const entryPath = join(root, entry.name)
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
paths.push(...await findHookManifestPaths(entryPath))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (entry.isFile() && entry.name === "hooks.json" && root.endsWith(`${sep}hooks`)) {
|
||||||
|
paths.push(entryPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectHookCommands(value: unknown, commands: string[]): void {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
collectHookCommands(item, commands)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRecord(value)) return
|
||||||
|
if (value.type === "command" && typeof value.command === "string") {
|
||||||
|
commands.push(value.command)
|
||||||
|
}
|
||||||
|
for (const child of Object.values(value)) {
|
||||||
|
collectHookCommands(child, commands)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPluginRootPaths(command: string): string[] {
|
||||||
|
const paths: string[] = []
|
||||||
|
const pluginRootPathPattern = /\$\{PLUGIN_ROOT\}\/([^"'\s]+)/g
|
||||||
|
let match = pluginRootPathPattern.exec(command)
|
||||||
|
while (match) {
|
||||||
|
const relativePath = match[1]
|
||||||
|
if (relativePath) {
|
||||||
|
paths.push(relativePath)
|
||||||
|
}
|
||||||
|
match = pluginRootPathPattern.exec(command)
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPluginRuntimePathArg(arg: string): boolean {
|
||||||
|
return (arg.startsWith("./") || arg.startsWith("../")) && arg.endsWith("/dist/cli.js")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateRelativeBundleFile(
|
||||||
|
bundleRoot: string,
|
||||||
|
baseRoot: string,
|
||||||
|
relativePath: string,
|
||||||
|
message: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const targetPath = resolve(baseRoot, relativePath)
|
||||||
|
const bundleRootPath = resolve(bundleRoot)
|
||||||
|
const bundleRootPrefix = bundleRootPath.endsWith(sep) ? bundleRootPath : `${bundleRootPath}${sep}`
|
||||||
|
if (targetPath !== bundleRootPath && !targetPath.startsWith(bundleRootPrefix)) {
|
||||||
|
throw new Error(`${message}: ${relativePath} escapes plugin root`)
|
||||||
|
}
|
||||||
|
if (!(await isFile(targetPath))) {
|
||||||
|
throw new Error(`${message}: ${relativePath}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isFile(path: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
return (await stat(path)).isFile()
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) return false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { mkdir, mkdtemp, readFile, stat, writeFile } from "node:fs/promises"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
|
import { dirname, join } from "node:path"
|
||||||
|
import { syncLazycodexMarketplace } from "./sync-lazycodex-marketplace"
|
||||||
|
|
||||||
|
async function writeJson(path: string, value: unknown): Promise<void> {
|
||||||
|
await mkdir(dirname(path), { recursive: true })
|
||||||
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writePluginFixture(sourceRoot: string): Promise<void> {
|
||||||
|
await writeJson(join(sourceRoot, "packages", "omo-codex", "marketplace.json"), {
|
||||||
|
name: "sisyphuslabs",
|
||||||
|
plugins: [{ name: "omo", source: "./plugins/omo" }],
|
||||||
|
})
|
||||||
|
await writeJson(join(sourceRoot, "packages", "omo-codex", "plugin", ".codex-plugin", "plugin.json"), {
|
||||||
|
name: "omo",
|
||||||
|
version: "1.2.3",
|
||||||
|
})
|
||||||
|
await writeJson(join(sourceRoot, "packages", "omo-codex", "plugin", ".mcp.json"), {
|
||||||
|
mcpServers: {
|
||||||
|
ast_grep: { command: "node", args: ["../../ast-grep-mcp/dist/cli.js", "mcp"], cwd: "." },
|
||||||
|
lsp: { command: "node", args: ["../../lsp-tools-mcp/dist/cli.js", "mcp"], cwd: "." },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await writeFile(join(sourceRoot, "packages", "omo-codex", "plugin", "README.md"), "omo\n")
|
||||||
|
await mkdir(join(sourceRoot, "packages", "omo-codex", "plugin", "components", "lsp", "dist"), { recursive: true })
|
||||||
|
await writeFile(join(sourceRoot, "packages", "omo-codex", "plugin", "components", "lsp", "dist", "cli.js"), "#!/usr/bin/env node\n")
|
||||||
|
await mkdir(join(sourceRoot, "packages", "ast-grep-mcp", "dist"), { recursive: true })
|
||||||
|
await writeFile(join(sourceRoot, "packages", "ast-grep-mcp", "dist", "cli.js"), "#!/usr/bin/env node\n")
|
||||||
|
await mkdir(join(sourceRoot, "packages", "lsp-tools-mcp", "dist"), { recursive: true })
|
||||||
|
await writeFile(join(sourceRoot, "packages", "lsp-tools-mcp", "dist", "cli.js"), "#!/usr/bin/env node\n")
|
||||||
|
await mkdir(join(sourceRoot, "packages", "omo-codex", "plugin", "node_modules", "ignored"), { recursive: true })
|
||||||
|
await writeFile(join(sourceRoot, "packages", "omo-codex", "plugin", "node_modules", "ignored", "file.txt"), "ignored\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("sync-lazycodex-marketplace", () => {
|
||||||
|
test("#given marketplace sync #when copying plugin bundle #then emits self-contained mcp paths", async () => {
|
||||||
|
// given
|
||||||
|
const sourceRoot = await mkdtemp(join(tmpdir(), "omo-sync-source-"))
|
||||||
|
const lazycodexRoot = await mkdtemp(join(tmpdir(), "omo-sync-lazycodex-"))
|
||||||
|
await writePluginFixture(sourceRoot)
|
||||||
|
|
||||||
|
// when
|
||||||
|
await syncLazycodexMarketplace({ sourceRoot, lazycodexRoot })
|
||||||
|
|
||||||
|
// then
|
||||||
|
const marketplace = JSON.parse(await readFile(join(lazycodexRoot, ".agents", "plugins", "marketplace.json"), "utf8"))
|
||||||
|
expect(marketplace.name).toBe("sisyphuslabs")
|
||||||
|
expect(marketplace.plugins[0].source).toBe("./plugins/omo")
|
||||||
|
const manifest = JSON.parse(await readFile(join(lazycodexRoot, "plugins", "omo", ".codex-plugin", "plugin.json"), "utf8"))
|
||||||
|
expect(manifest).toMatchObject({ name: "omo", version: "1.2.3" })
|
||||||
|
const mcpManifest = JSON.parse(await readFile(join(lazycodexRoot, "plugins", "omo", ".mcp.json"), "utf8"))
|
||||||
|
expect(mcpManifest.mcpServers.ast_grep.args[0]).toBe("./components/ast-grep-mcp/dist/cli.js")
|
||||||
|
expect(mcpManifest.mcpServers.lsp.args[0]).toBe("./components/lsp-tools-mcp/dist/cli.js")
|
||||||
|
expect((await stat(join(lazycodexRoot, "plugins", "omo", "components", "ast-grep-mcp", "dist", "cli.js"))).isFile()).toBe(true)
|
||||||
|
expect((await stat(join(lazycodexRoot, "plugins", "omo", "components", "lsp-tools-mcp", "dist", "cli.js"))).isFile()).toBe(true)
|
||||||
|
let nodeModulesMissing = false
|
||||||
|
try {
|
||||||
|
await stat(join(lazycodexRoot, "plugins", "omo", "node_modules"))
|
||||||
|
} catch (error) {
|
||||||
|
nodeModulesMissing = error instanceof Error
|
||||||
|
}
|
||||||
|
expect(nodeModulesMissing).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects a source tree without a Codex plugin manifest", async () => {
|
||||||
|
// given
|
||||||
|
const sourceRoot = await mkdtemp(join(tmpdir(), "omo-sync-bad-source-"))
|
||||||
|
const lazycodexRoot = await mkdtemp(join(tmpdir(), "omo-sync-bad-lazycodex-"))
|
||||||
|
await writeJson(join(sourceRoot, "packages", "omo-codex", "marketplace.json"), {
|
||||||
|
name: "sisyphuslabs",
|
||||||
|
plugins: [{ name: "omo", source: "./plugins/omo" }],
|
||||||
|
})
|
||||||
|
|
||||||
|
// when
|
||||||
|
let message = ""
|
||||||
|
try {
|
||||||
|
await syncLazycodexMarketplace({ sourceRoot, lazycodexRoot })
|
||||||
|
} catch (error) {
|
||||||
|
message = error instanceof Error ? error.message : String(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(message).toContain("missing Codex plugin manifest")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given stale mcp runtime path #when syncing marketplace #then rejects the broken bundle", async () => {
|
||||||
|
// given
|
||||||
|
const sourceRoot = await mkdtemp(join(tmpdir(), "omo-sync-stale-mcp-source-"))
|
||||||
|
const lazycodexRoot = await mkdtemp(join(tmpdir(), "omo-sync-stale-mcp-lazycodex-"))
|
||||||
|
await writePluginFixture(sourceRoot)
|
||||||
|
await writeJson(join(sourceRoot, "packages", "omo-codex", "plugin", ".mcp.json"), {
|
||||||
|
mcpServers: {
|
||||||
|
lsp: { command: "node", args: ["./components/lsp/packages/lsp-tools-mcp/dist/cli.js", "mcp"], cwd: "." },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// when
|
||||||
|
let message = ""
|
||||||
|
try {
|
||||||
|
await syncLazycodexMarketplace({ sourceRoot, lazycodexRoot })
|
||||||
|
} catch (error) {
|
||||||
|
message = error instanceof Error ? error.message : String(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(message).toContain("missing MCP runtime path")
|
||||||
|
expect(message).toContain("components/lsp/packages/lsp-tools-mcp/dist/cli.js")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given missing hook command target #when syncing marketplace #then rejects the broken bundle", async () => {
|
||||||
|
// given
|
||||||
|
const sourceRoot = await mkdtemp(join(tmpdir(), "omo-sync-missing-hook-source-"))
|
||||||
|
const lazycodexRoot = await mkdtemp(join(tmpdir(), "omo-sync-missing-hook-lazycodex-"))
|
||||||
|
await writePluginFixture(sourceRoot)
|
||||||
|
await writeJson(join(sourceRoot, "packages", "omo-codex", "plugin", "hooks", "hooks.json"), {
|
||||||
|
hooks: {
|
||||||
|
SessionStart: [
|
||||||
|
{
|
||||||
|
hooks: [
|
||||||
|
{
|
||||||
|
type: "command",
|
||||||
|
command: "node \"${PLUGIN_ROOT}/components/rules/dist/cli.js\" hook session-start",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// when
|
||||||
|
let message = ""
|
||||||
|
try {
|
||||||
|
await syncLazycodexMarketplace({ sourceRoot, lazycodexRoot })
|
||||||
|
} catch (error) {
|
||||||
|
message = error instanceof Error ? error.message : String(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(message).toContain("missing hook command target")
|
||||||
|
expect(message).toContain("components/rules/dist/cli.js")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"
|
||||||
|
import { dirname, join, resolve, sep } from "node:path"
|
||||||
|
import { validateLazycodexPluginBundle } from "./lazycodex-marketplace-validation"
|
||||||
|
|
||||||
|
const MARKETPLACE_SOURCE_PATH = join("packages", "omo-codex", "marketplace.json")
|
||||||
|
const PLUGIN_SOURCE_PATH = join("packages", "omo-codex", "plugin")
|
||||||
|
const AST_GREP_MCP_DIST_SOURCE_PATH = join("packages", "ast-grep-mcp", "dist")
|
||||||
|
const LSP_TOOLS_MCP_DIST_SOURCE_PATH = join("packages", "lsp-tools-mcp", "dist")
|
||||||
|
const MARKETPLACE_DESTINATION_PATH = join(".agents", "plugins", "marketplace.json")
|
||||||
|
const PLUGIN_DESTINATION_PATH = join("plugins", "omo")
|
||||||
|
const AST_GREP_MCP_DIST_DESTINATION_PATH = join(PLUGIN_DESTINATION_PATH, "components", "ast-grep-mcp", "dist")
|
||||||
|
const LSP_TOOLS_MCP_DIST_DESTINATION_PATH = join(PLUGIN_DESTINATION_PATH, "components", "lsp-tools-mcp", "dist")
|
||||||
|
const AST_GREP_MCP_SOURCE_ARG = "../../ast-grep-mcp/dist/cli.js"
|
||||||
|
const AST_GREP_MCP_PLUGIN_ARG = "./components/ast-grep-mcp/dist/cli.js"
|
||||||
|
const LSP_TOOLS_MCP_SOURCE_ARG = "../../lsp-tools-mcp/dist/cli.js"
|
||||||
|
const LSP_TOOLS_MCP_PLUGIN_ARG = "./components/lsp-tools-mcp/dist/cli.js"
|
||||||
|
|
||||||
|
const BUNDLED_MCP_DISTS = [
|
||||||
|
{
|
||||||
|
label: "ast-grep MCP",
|
||||||
|
sourcePath: AST_GREP_MCP_DIST_SOURCE_PATH,
|
||||||
|
destinationPath: AST_GREP_MCP_DIST_DESTINATION_PATH,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "LSP MCP",
|
||||||
|
sourcePath: LSP_TOOLS_MCP_DIST_SOURCE_PATH,
|
||||||
|
destinationPath: LSP_TOOLS_MCP_DIST_DESTINATION_PATH,
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const MCP_ARG_REWRITES = [
|
||||||
|
[AST_GREP_MCP_SOURCE_ARG, AST_GREP_MCP_PLUGIN_ARG],
|
||||||
|
[LSP_TOOLS_MCP_SOURCE_ARG, LSP_TOOLS_MCP_PLUGIN_ARG],
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export interface SyncLazycodexMarketplaceInput {
|
||||||
|
readonly sourceRoot: string
|
||||||
|
readonly lazycodexRoot: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MarketplaceManifest {
|
||||||
|
readonly name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginManifest {
|
||||||
|
readonly name: string
|
||||||
|
readonly version?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncLazycodexMarketplace(input: SyncLazycodexMarketplaceInput): Promise<void> {
|
||||||
|
const sourceRoot = resolve(input.sourceRoot)
|
||||||
|
const lazycodexRoot = resolve(input.lazycodexRoot)
|
||||||
|
const marketplacePath = join(sourceRoot, MARKETPLACE_SOURCE_PATH)
|
||||||
|
const pluginRoot = join(sourceRoot, PLUGIN_SOURCE_PATH)
|
||||||
|
const pluginManifestPath = join(pluginRoot, ".codex-plugin", "plugin.json")
|
||||||
|
|
||||||
|
const marketplace = await readMarketplaceManifest(marketplacePath)
|
||||||
|
if (marketplace.name !== "sisyphuslabs") {
|
||||||
|
throw new Error(`Sisyphus Labs marketplace manifest must be named sisyphuslabs, got ${marketplace.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginManifest = await readPluginManifest(pluginManifestPath)
|
||||||
|
if (pluginManifest.name !== "omo") {
|
||||||
|
throw new Error(`Sisyphus Labs plugin manifest must be named omo, got ${pluginManifest.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const destinationMarketplacePath = join(lazycodexRoot, MARKETPLACE_DESTINATION_PATH)
|
||||||
|
await mkdir(dirname(destinationMarketplacePath), { recursive: true })
|
||||||
|
await writeFile(destinationMarketplacePath, await readFile(marketplacePath, "utf8"))
|
||||||
|
|
||||||
|
const destinationPluginRoot = join(lazycodexRoot, PLUGIN_DESTINATION_PATH)
|
||||||
|
await rm(destinationPluginRoot, { recursive: true, force: true })
|
||||||
|
await mkdir(dirname(destinationPluginRoot), { recursive: true })
|
||||||
|
await cp(pluginRoot, destinationPluginRoot, {
|
||||||
|
recursive: true,
|
||||||
|
filter: (path) => shouldCopyPluginPath(path, pluginRoot),
|
||||||
|
})
|
||||||
|
await copyBundledMcpDists(sourceRoot, lazycodexRoot)
|
||||||
|
await rewritePluginMcpManifest(destinationPluginRoot)
|
||||||
|
await validateLazycodexPluginBundle(destinationPluginRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readMarketplaceManifest(path: string): Promise<MarketplaceManifest> {
|
||||||
|
const parsed = JSON.parse(await readFile(path, "utf8"))
|
||||||
|
if (isRecord(parsed) && typeof parsed.name === "string") {
|
||||||
|
return { name: parsed.name }
|
||||||
|
}
|
||||||
|
throw new Error("invalid Sisyphus Labs marketplace manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readPluginManifest(path: string): Promise<PluginManifest> {
|
||||||
|
if (!(await isFile(path))) {
|
||||||
|
throw new Error(`missing Codex plugin manifest at ${path}`)
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(await readFile(path, "utf8"))
|
||||||
|
if (isRecord(parsed) && typeof parsed.name === "string") {
|
||||||
|
return {
|
||||||
|
name: parsed.name,
|
||||||
|
version: typeof parsed.version === "string" ? parsed.version : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("invalid Codex plugin manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isFile(path: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
return (await stat(path)).isFile()
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) return false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isDirectory(path: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
return (await stat(path)).isDirectory()
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) return false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyBundledMcpDists(sourceRoot: string, lazycodexRoot: string): Promise<void> {
|
||||||
|
for (const mcpDist of BUNDLED_MCP_DISTS) {
|
||||||
|
await copyBundledMcpDist(sourceRoot, lazycodexRoot, mcpDist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyBundledMcpDist(
|
||||||
|
sourceRoot: string,
|
||||||
|
lazycodexRoot: string,
|
||||||
|
mcpDist: (typeof BUNDLED_MCP_DISTS)[number],
|
||||||
|
): Promise<void> {
|
||||||
|
const sourcePath = join(sourceRoot, mcpDist.sourcePath)
|
||||||
|
if (!(await isDirectory(sourcePath))) {
|
||||||
|
throw new Error(`missing built ${mcpDist.label} dist at ${sourcePath}`)
|
||||||
|
}
|
||||||
|
const destinationPath = join(lazycodexRoot, mcpDist.destinationPath)
|
||||||
|
await mkdir(dirname(destinationPath), { recursive: true })
|
||||||
|
await cp(sourcePath, destinationPath, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rewritePluginMcpManifest(pluginRoot: string): Promise<void> {
|
||||||
|
const manifestPath = join(pluginRoot, ".mcp.json")
|
||||||
|
if (!(await isFile(manifestPath))) return
|
||||||
|
const parsed: unknown = JSON.parse(await readFile(manifestPath, "utf8"))
|
||||||
|
if (!isRecord(parsed) || !isRecord(parsed.mcpServers)) return
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
for (const server of Object.values(parsed.mcpServers)) {
|
||||||
|
if (!isRecord(server) || !Array.isArray(server.args)) continue
|
||||||
|
const currentArgs = server.args
|
||||||
|
const nextArgs = currentArgs.map(rewriteMcpArg)
|
||||||
|
if (nextArgs.some((arg, index) => arg !== currentArgs[index])) {
|
||||||
|
server.args = nextArgs
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) await writeFile(manifestPath, `${JSON.stringify(parsed, null, "\t")}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteMcpArg(arg: unknown): unknown {
|
||||||
|
if (typeof arg !== "string") return arg
|
||||||
|
const rewrite = MCP_ARG_REWRITES.find(([sourceArg]) => sourceArg === arg)
|
||||||
|
return rewrite?.[1] ?? arg
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldCopyPluginPath(path: string, root: string): boolean {
|
||||||
|
const relative = path === root ? "" : path.slice(root.length + sep.length)
|
||||||
|
if (relative.length === 0) return true
|
||||||
|
return !relative.split(sep).some((part) => part === ".git" || part === "node_modules")
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
const sourceRoot = process.argv[2] ?? process.cwd()
|
||||||
|
const lazycodexRoot = process.argv[3]
|
||||||
|
if (lazycodexRoot === undefined) {
|
||||||
|
throw new Error("Usage: bun run script/sync-lazycodex-marketplace.ts <source-root> <lazycodex-root>")
|
||||||
|
}
|
||||||
|
await syncLazycodexMarketplace({ sourceRoot, lazycodexRoot })
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user