feat(script): add lazyclaudecode marketplace syncer with CC paths and filter
Forks sync-lazycodex-marketplace: reads .claude-plugin/plugin.json, writes .claude-plugin/marketplace.json + plugins/omo, asserts sisyphuslabs/omo identity, excludes src/test/tsconfig*/.omo from the shipped tree. RED->GREEN test (6 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
/// <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 { syncLazyclaudecodeMarketplace } from "./sync-lazyclaudecode-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 writeText(path: string, text: string): Promise<void> {
|
||||||
|
await mkdir(dirname(path), { recursive: true })
|
||||||
|
await writeFile(path, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginFixtureOptions {
|
||||||
|
readonly marketplaceName?: string
|
||||||
|
readonly pluginName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writePluginFixture(sourceRoot: string, options: PluginFixtureOptions = {}): Promise<void> {
|
||||||
|
const marketplaceName = options.marketplaceName ?? "sisyphuslabs"
|
||||||
|
const pluginName = options.pluginName ?? "omo"
|
||||||
|
const pluginRoot = join(sourceRoot, "packages", "omo-claude", "plugin")
|
||||||
|
|
||||||
|
await writeJson(join(sourceRoot, "packages", "omo-claude", "marketplace.json"), {
|
||||||
|
name: marketplaceName,
|
||||||
|
owner: { name: "Yeongyu Kim", url: "https://github.com/code-yeongyu" },
|
||||||
|
plugins: [{ name: pluginName, source: "./plugins/omo" }],
|
||||||
|
})
|
||||||
|
await writeJson(join(pluginRoot, ".claude-plugin", "plugin.json"), {
|
||||||
|
name: pluginName,
|
||||||
|
version: "1.2.3",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Runtime artifacts that MUST be shipped.
|
||||||
|
await writeText(join(pluginRoot, "README.md"), "omo\n")
|
||||||
|
await writeJson(join(pluginRoot, ".mcp.json"), { mcpServers: {} })
|
||||||
|
await writeText(join(pluginRoot, "hooks", "hooks.json"), "{}\n")
|
||||||
|
await writeText(join(pluginRoot, "skills", "demo", "SKILL.md"), "# demo\n")
|
||||||
|
await writeText(join(pluginRoot, "agents", "planner.md"), "# planner\n")
|
||||||
|
await writeText(join(pluginRoot, "components", "rules", "dist", "cli.js"), "// built\n")
|
||||||
|
await writeText(join(pluginRoot, "mcp", "ast-grep", "cli.js"), "// mcp\n")
|
||||||
|
|
||||||
|
// Entries that MUST be excluded by the copy filter.
|
||||||
|
await writeText(join(pluginRoot, "node_modules", "ignored", "file.txt"), "ignored\n")
|
||||||
|
await writeText(join(pluginRoot, "components", "rules", "src", "cli.ts"), "// source\n")
|
||||||
|
await writeText(join(pluginRoot, "components", "rules", "tsconfig.build.json"), "{}\n")
|
||||||
|
await writeText(join(pluginRoot, "components", "rules", "test", "cli.test.ts"), "// test\n")
|
||||||
|
await writeText(join(pluginRoot, "src", "index.ts"), "// source\n")
|
||||||
|
await writeText(join(pluginRoot, "test", "smoke.test.ts"), "// test\n")
|
||||||
|
await writeText(join(pluginRoot, "tsconfig.json"), "{}\n")
|
||||||
|
await writeText(join(pluginRoot, ".omo", "ultragoal", "state.json"), "{}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exists(path: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await stat(path)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("sync-lazyclaudecode-marketplace", () => {
|
||||||
|
test("reads .claude-plugin/plugin.json and writes .claude-plugin/marketplace.json into the dest", async () => {
|
||||||
|
// given
|
||||||
|
const sourceRoot = await mkdtemp(join(tmpdir(), "omo-claude-sync-source-"))
|
||||||
|
const lazyclaudecodeRoot = await mkdtemp(join(tmpdir(), "omo-claude-sync-dest-"))
|
||||||
|
await writePluginFixture(sourceRoot)
|
||||||
|
|
||||||
|
// when
|
||||||
|
await syncLazyclaudecodeMarketplace({ sourceRoot, lazyclaudecodeRoot })
|
||||||
|
|
||||||
|
// then — destination marketplace at the CC path
|
||||||
|
const marketplace = JSON.parse(
|
||||||
|
await readFile(join(lazyclaudecodeRoot, ".claude-plugin", "marketplace.json"), "utf8"),
|
||||||
|
)
|
||||||
|
expect(marketplace.name).toBe("sisyphuslabs")
|
||||||
|
expect(marketplace.plugins[0].source).toBe("./plugins/omo")
|
||||||
|
|
||||||
|
// the plugin manifest came from .claude-plugin/plugin.json and landed under plugins/omo
|
||||||
|
const manifest = JSON.parse(
|
||||||
|
await readFile(join(lazyclaudecodeRoot, "plugins", "omo", ".claude-plugin", "plugin.json"), "utf8"),
|
||||||
|
)
|
||||||
|
expect(manifest).toMatchObject({ name: "omo", version: "1.2.3" })
|
||||||
|
|
||||||
|
// NOT the codex dest path
|
||||||
|
expect(await exists(join(lazyclaudecodeRoot, ".agents", "plugins", "marketplace.json"))).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ships runtime artifacts into plugins/omo", async () => {
|
||||||
|
// given
|
||||||
|
const sourceRoot = await mkdtemp(join(tmpdir(), "omo-claude-sync-source-"))
|
||||||
|
const lazyclaudecodeRoot = await mkdtemp(join(tmpdir(), "omo-claude-sync-dest-"))
|
||||||
|
await writePluginFixture(sourceRoot)
|
||||||
|
|
||||||
|
// when
|
||||||
|
await syncLazyclaudecodeMarketplace({ sourceRoot, lazyclaudecodeRoot })
|
||||||
|
|
||||||
|
// then
|
||||||
|
const pluginDest = join(lazyclaudecodeRoot, "plugins", "omo")
|
||||||
|
expect(await exists(join(pluginDest, ".mcp.json"))).toBe(true)
|
||||||
|
expect(await exists(join(pluginDest, "hooks", "hooks.json"))).toBe(true)
|
||||||
|
expect(await exists(join(pluginDest, "skills", "demo", "SKILL.md"))).toBe(true)
|
||||||
|
expect(await exists(join(pluginDest, "agents", "planner.md"))).toBe(true)
|
||||||
|
expect(await exists(join(pluginDest, "components", "rules", "dist", "cli.js"))).toBe(true)
|
||||||
|
expect(await exists(join(pluginDest, "mcp", "ast-grep", "cli.js"))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("copy filter excludes node_modules, src/, test/, tsconfig*.json, and .omo/", async () => {
|
||||||
|
// given
|
||||||
|
const sourceRoot = await mkdtemp(join(tmpdir(), "omo-claude-sync-source-"))
|
||||||
|
const lazyclaudecodeRoot = await mkdtemp(join(tmpdir(), "omo-claude-sync-dest-"))
|
||||||
|
await writePluginFixture(sourceRoot)
|
||||||
|
|
||||||
|
// when
|
||||||
|
await syncLazyclaudecodeMarketplace({ sourceRoot, lazyclaudecodeRoot })
|
||||||
|
|
||||||
|
// then
|
||||||
|
const pluginDest = join(lazyclaudecodeRoot, "plugins", "omo")
|
||||||
|
expect(await exists(join(pluginDest, "node_modules"))).toBe(false)
|
||||||
|
expect(await exists(join(pluginDest, "src"))).toBe(false)
|
||||||
|
expect(await exists(join(pluginDest, "test"))).toBe(false)
|
||||||
|
expect(await exists(join(pluginDest, "tsconfig.json"))).toBe(false)
|
||||||
|
expect(await exists(join(pluginDest, ".omo"))).toBe(false)
|
||||||
|
// nested src/test/tsconfig under a component are excluded too
|
||||||
|
expect(await exists(join(pluginDest, "components", "rules", "src"))).toBe(false)
|
||||||
|
expect(await exists(join(pluginDest, "components", "rules", "test"))).toBe(false)
|
||||||
|
expect(await exists(join(pluginDest, "components", "rules", "tsconfig.build.json"))).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throws when the marketplace name is not sisyphuslabs", async () => {
|
||||||
|
// given
|
||||||
|
const sourceRoot = await mkdtemp(join(tmpdir(), "omo-claude-sync-source-"))
|
||||||
|
const lazyclaudecodeRoot = await mkdtemp(join(tmpdir(), "omo-claude-sync-dest-"))
|
||||||
|
await writePluginFixture(sourceRoot, { marketplaceName: "wrong" })
|
||||||
|
|
||||||
|
// when / then
|
||||||
|
await expect(syncLazyclaudecodeMarketplace({ sourceRoot, lazyclaudecodeRoot })).rejects.toThrow("sisyphuslabs")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throws when the plugin name is not omo", async () => {
|
||||||
|
// given
|
||||||
|
const sourceRoot = await mkdtemp(join(tmpdir(), "omo-claude-sync-source-"))
|
||||||
|
const lazyclaudecodeRoot = await mkdtemp(join(tmpdir(), "omo-claude-sync-dest-"))
|
||||||
|
await writePluginFixture(sourceRoot, { pluginName: "not-omo" })
|
||||||
|
|
||||||
|
// when / then
|
||||||
|
await expect(syncLazyclaudecodeMarketplace({ sourceRoot, lazyclaudecodeRoot })).rejects.toThrow("omo")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throws when the .claude-plugin/plugin.json manifest is missing", async () => {
|
||||||
|
// given
|
||||||
|
const sourceRoot = await mkdtemp(join(tmpdir(), "omo-claude-sync-source-"))
|
||||||
|
const lazyclaudecodeRoot = await mkdtemp(join(tmpdir(), "omo-claude-sync-dest-"))
|
||||||
|
await writeJson(join(sourceRoot, "packages", "omo-claude", "marketplace.json"), {
|
||||||
|
name: "sisyphuslabs",
|
||||||
|
plugins: [{ name: "omo", source: "./plugins/omo" }],
|
||||||
|
})
|
||||||
|
|
||||||
|
// when / then
|
||||||
|
await expect(syncLazyclaudecodeMarketplace({ sourceRoot, lazyclaudecodeRoot })).rejects.toThrow(
|
||||||
|
".claude-plugin/plugin.json",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
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-claude", "marketplace.json")
|
||||||
|
const PLUGIN_SOURCE_PATH = join("packages", "omo-claude", "plugin")
|
||||||
|
const MARKETPLACE_DESTINATION_PATH = join(".claude-plugin", "marketplace.json")
|
||||||
|
const PLUGIN_DESTINATION_PATH = join("plugins", "omo")
|
||||||
|
|
||||||
|
export interface SyncLazyclaudecodeMarketplaceInput {
|
||||||
|
readonly sourceRoot: string
|
||||||
|
readonly lazyclaudecodeRoot: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MarketplaceManifest {
|
||||||
|
readonly name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginManifest {
|
||||||
|
readonly name: string
|
||||||
|
readonly version?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncLazyclaudecodeMarketplace(input: SyncLazyclaudecodeMarketplaceInput): Promise<void> {
|
||||||
|
const sourceRoot = resolve(input.sourceRoot)
|
||||||
|
const lazyclaudecodeRoot = resolve(input.lazyclaudecodeRoot)
|
||||||
|
const marketplacePath = join(sourceRoot, MARKETPLACE_SOURCE_PATH)
|
||||||
|
const pluginRoot = join(sourceRoot, PLUGIN_SOURCE_PATH)
|
||||||
|
const pluginManifestPath = join(pluginRoot, ".claude-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(lazyclaudecodeRoot, MARKETPLACE_DESTINATION_PATH)
|
||||||
|
await mkdir(dirname(destinationMarketplacePath), { recursive: true })
|
||||||
|
await writeFile(destinationMarketplacePath, await readFile(marketplacePath, "utf8"))
|
||||||
|
|
||||||
|
const destinationPluginRoot = join(lazyclaudecodeRoot, 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 Claude Code plugin manifest at .claude-plugin/plugin.json (${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 Claude Code 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(isExcludedPathPart)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExcludedPathPart(part: string): boolean {
|
||||||
|
if (part === ".git" || part === "node_modules") return true
|
||||||
|
if (part === "src" || part === "test") return true
|
||||||
|
if (part === ".omo") return true
|
||||||
|
return isTsconfigFile(part)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTsconfigFile(part: string): boolean {
|
||||||
|
return part.startsWith("tsconfig") && part.endsWith(".json")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 lazyclaudecodeRoot = process.argv[3]
|
||||||
|
if (lazyclaudecodeRoot === undefined) {
|
||||||
|
throw new Error("Usage: bun run script/sync-lazyclaudecode-marketplace.ts <source-root> <lazyclaudecode-root>")
|
||||||
|
}
|
||||||
|
await syncLazyclaudecodeMarketplace({ sourceRoot, lazyclaudecodeRoot })
|
||||||
|
}
|
||||||
@@ -11,5 +11,5 @@
|
|||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"noEmit": true
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": ["./publish-workflow.test.ts", "./package-layout.test.ts", "./sync-lazycodex-marketplace.ts", "./sync-lazycodex-marketplace.test.ts"]
|
"include": ["./publish-workflow.test.ts", "./package-layout.test.ts", "./sync-lazycodex-marketplace.ts", "./sync-lazycodex-marketplace.test.ts", "./sync-lazyclaudecode-marketplace.ts", "./sync-lazyclaudecode-marketplace.test.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user