feat(omo-codex): install through sisyphuslabs marketplace
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user