From 72119b3aa3a5820d990ef0a8b985edc933bb8e09 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 25 May 2026 17:18:59 +0900 Subject: [PATCH] fix(package): ship dot-directory command assets Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- package.json | 4 + script/package-layout.test.ts | 134 ++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 script/package-layout.test.ts diff --git a/package.json b/package.json index 5013879c8..f3d345e43 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,10 @@ "dist", "bin", "postinstall.mjs", + ".opencode/command", + ".opencode/skills", + ".agents/command", + ".agents/skills", "packages/lsp-tools-mcp/dist", "packages/ast-grep-mcp/dist" ], diff --git a/script/package-layout.test.ts b/script/package-layout.test.ts new file mode 100644 index 000000000..a4f3c18fd --- /dev/null +++ b/script/package-layout.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, test } from "bun:test" +import { existsSync, readdirSync } from "node:fs" +import { join, relative, sep } from "node:path" +import { fileURLToPath } from "node:url" + +const repositoryRoot = fileURLToPath(new URL("..", import.meta.url)) +const commandRoots = [".opencode/command", ".agents/command"] as const +const skillRoots = [".opencode/skills", ".agents/skills"] as const + +class PackDryRunError extends Error { + constructor(readonly exitCode: number, readonly stderr: string) { + super(`bun pm pack --dry-run failed with exit code ${exitCode}: ${stderr}`) + this.name = "PackDryRunError" + } +} + +function toPackagePath(filePath: string): string { + return relative(repositoryRoot, filePath).split(sep).join("/") +} + +function collectPackagePathsRecursively(rootPath: string): string[] { + const collectedPaths: string[] = [] + const directories = [rootPath] + + while (directories.length > 0) { + const currentDirectory = directories.pop() + if (!currentDirectory) { + continue + } + + for (const entry of readdirSync(currentDirectory, { withFileTypes: true })) { + const entryPath = join(currentDirectory, entry.name) + if (entry.isDirectory()) { + directories.push(entryPath) + continue + } + + if (entry.isFile()) { + collectedPaths.push(toPackagePath(entryPath)) + } + } + } + + return collectedPaths +} + +function collectCommandAssetPaths(rootRelativePath: string): string[] { + const rootPath = join(repositoryRoot, rootRelativePath) + if (!existsSync(rootPath)) { + return [] + } + + return collectPackagePathsRecursively(rootPath) + .filter((packagePath) => packagePath.endsWith(".md")) + .sort() +} + +function collectSkillAssetPaths(rootRelativePath: string): string[] { + const rootPath = join(repositoryRoot, rootRelativePath) + if (!existsSync(rootPath)) { + return [] + } + + const expectedPaths: string[] = [] + + for (const entry of readdirSync(rootPath, { withFileTypes: true })) { + const skillPath = join(rootPath, entry.name) + const skillManifestPath = join(skillPath, "SKILL.md") + if (entry.isDirectory() && existsSync(skillManifestPath)) { + expectedPaths.push(...collectPackagePathsRecursively(skillPath)) + } + } + + return expectedPaths.sort() +} + +function collectExpectedAssetPaths(): string[] { + return [ + ...commandRoots.flatMap(collectCommandAssetPaths), + ...skillRoots.flatMap(collectSkillAssetPaths), + ].sort() +} + +function parsePackedPaths(output: string): Set { + const packedPaths = new Set() + const packedPathPattern = /^packed\s+\S+\s+(.+)$/ + + for (const line of output.split("\n")) { + const match = packedPathPattern.exec(line) + const packedPath = match?.at(1) + if (packedPath) { + packedPaths.add(packedPath) + } + } + + return packedPaths +} + +async function packDryRunPaths(): Promise> { + const packProcess = Bun.spawn({ + cmd: ["bun", "pm", "pack", "--dry-run"], + cwd: repositoryRoot, + stdout: "pipe", + stderr: "pipe", + }) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(packProcess.stdout).text(), + new Response(packProcess.stderr).text(), + packProcess.exited, + ]) + + if (exitCode !== 0) { + throw new PackDryRunError(exitCode, stderr) + } + + return parsePackedPaths(stdout) +} + +describe("published package layout", () => { + test("#given dot-directory command and skill assets #when packing package #then slash-command discovery assets ship", async () => { + // given + const expectedAssetPaths = collectExpectedAssetPaths() + expect(expectedAssetPaths).toContain(".opencode/command/security-research.md") + expect(expectedAssetPaths).toContain(".agents/command/security-research.md") + expect(expectedAssetPaths).toContain(".agents/skills/security-research/SKILL.md") + + // when + const packedPaths = await packDryRunPaths() + + // then + const missingPaths = expectedAssetPaths.filter((expectedPath) => !packedPaths.has(expectedPath)) + expect(missingPaths).toEqual([]) + }) +})