feat(omo-codex): install through sisyphuslabs marketplace

This commit is contained in:
YeonGyu-Kim
2026-05-27 18:48:33 +09:00
parent a3567056c0
commit 56f39d60fc
35 changed files with 620 additions and 305 deletions
+18
View File
@@ -58,4 +58,22 @@ describe("test workflows", () => {
expect(hasCodexCommand, "Codex compatibility job must run the shared Codex test script").toBe(true)
expect(buildNeedsCodexMatrix, "Build must wait for Codex compatibility checks").toBe(true)
})
test("syncs the LazyCodex Codex marketplace bundle during release", () => {
// #given
const workflow = readFileSync(new URL("../.github/workflows/publish.yml", import.meta.url), "utf8")
// #when
const appliesCodexPluginVersion = workflow.includes("packages/omo-codex/plugin/.codex-plugin/plugin.json")
const syncsLazycodexMarketplace = workflow.includes("bun run script/sync-lazycodex-marketplace.ts")
const pushesLazycodexMarketplace = workflow.includes("code-yeongyu/lazycodex")
const requiresLazycodexSyncToken = workflow.includes("secrets.LAZYCODEX_SYNC_TOKEN == ''") &&
workflow.includes("token: ${{ secrets.LAZYCODEX_SYNC_TOKEN }}")
// #then
expect(appliesCodexPluginVersion, "release must version the Codex plugin manifest before marketplace sync").toBe(true)
expect(syncsLazycodexMarketplace, "release must sync the LazyCodex marketplace bundle").toBe(true)
expect(pushesLazycodexMarketplace, "release must target the LazyCodex repository").toBe(true)
expect(requiresLazycodexSyncToken, "release must require a cross-repo token for LazyCodex push").toBe(true)
})
})
+59
View File
@@ -0,0 +1,59 @@
/// <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 writeFile(join(sourceRoot, "packages", "omo-codex", "plugin", "README.md"), "omo\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("copies the Codex marketplace manifest and clean plugin bundle", 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" })
await expect(stat(join(lazycodexRoot, "plugins", "omo", "node_modules"))).rejects.toThrow()
})
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 / then
await expect(syncLazycodexMarketplace({ sourceRoot, lazycodexRoot })).rejects.toThrow("missing Codex plugin manifest")
})
})
+101
View File
@@ -0,0 +1,101 @@
import { cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"
import { dirname, join, resolve, sep } from "node:path"
const MARKETPLACE_SOURCE_PATH = join("packages", "omo-codex", "marketplace.json")
const PLUGIN_SOURCE_PATH = join("packages", "omo-codex", "plugin")
const MARKETPLACE_DESTINATION_PATH = join(".agents", "plugins", "marketplace.json")
const PLUGIN_DESTINATION_PATH = join("plugins", "omo")
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),
})
}
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
}
}
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 })
}
+1 -1
View File
@@ -11,5 +11,5 @@
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["./publish-workflow.test.ts", "./package-layout.test.ts"]
"include": ["./publish-workflow.test.ts", "./package-layout.test.ts", "./sync-lazycodex-marketplace.ts", "./sync-lazycodex-marketplace.test.ts"]
}