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 const packDryRunTimeoutMs = 15_000 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([]) }, packDryRunTimeoutMs) })