|
| 1 | +#!/usr/bin/env bun |
| 2 | + |
| 3 | +import { existsSync } from "node:fs"; |
| 4 | + |
| 5 | +interface PackedFile { |
| 6 | + path: string; |
| 7 | + size: number; |
| 8 | +} |
| 9 | + |
| 10 | +interface PackResult { |
| 11 | + name: string; |
| 12 | + version: string; |
| 13 | + filename: string; |
| 14 | + entryCount: number; |
| 15 | + files: PackedFile[]; |
| 16 | +} |
| 17 | + |
| 18 | +const proc = Bun.spawnSync(["npm", "pack", "--dry-run", "--json"], { |
| 19 | + cwd: process.cwd(), |
| 20 | + stdin: "ignore", |
| 21 | + stdout: "pipe", |
| 22 | + stderr: "pipe", |
| 23 | + env: process.env, |
| 24 | +}); |
| 25 | + |
| 26 | +const stdout = Buffer.from(proc.stdout).toString("utf8").trim(); |
| 27 | +const stderr = Buffer.from(proc.stderr).toString("utf8").trim(); |
| 28 | + |
| 29 | +if (proc.exitCode !== 0) { |
| 30 | + throw new Error(stderr || stdout || "npm pack --dry-run failed"); |
| 31 | +} |
| 32 | + |
| 33 | +const jsonMatch = stdout.match(/(\[\s*\{[\s\S]*\}\s*\])\s*$/); |
| 34 | +const jsonText = jsonMatch?.[1]; |
| 35 | + |
| 36 | +if (!jsonText) { |
| 37 | + throw new Error(`Could not find npm pack JSON output. Full stdout:\n${stdout}`); |
| 38 | +} |
| 39 | + |
| 40 | +const parsed = JSON.parse(jsonText) as PackResult[]; |
| 41 | +const pack = parsed[0]; |
| 42 | + |
| 43 | +if (!pack) { |
| 44 | + throw new Error("npm pack --dry-run returned no pack result."); |
| 45 | +} |
| 46 | + |
| 47 | +const publishedPaths = new Set(pack.files.map((file) => file.path)); |
| 48 | +const requiredPaths = ["dist/npm/main.js", "README.md", "LICENSE", "package.json"]; |
| 49 | +const optionalPaths = ["CONTRIBUTING.md", "SECURITY.md"]; |
| 50 | + |
| 51 | +for (const path of requiredPaths) { |
| 52 | + if (!publishedPaths.has(path)) { |
| 53 | + throw new Error(`Expected npm package to include ${path}.`); |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +for (const path of optionalPaths) { |
| 58 | + if (existsSync(path) && !publishedPaths.has(path)) { |
| 59 | + throw new Error(`Expected npm package to include ${path} when it exists in the repo.`); |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +const forbiddenPrefixes = [".github/", "src/", "test/", "scripts/", "tmp/"]; |
| 64 | +const forbiddenPaths = ["AGENTS.md", "autoresearch.checks.sh", "autoresearch.sh", "bun.lock"]; |
| 65 | + |
| 66 | +for (const file of pack.files) { |
| 67 | + if (forbiddenPrefixes.some((prefix) => file.path.startsWith(prefix)) || forbiddenPaths.includes(file.path)) { |
| 68 | + throw new Error(`Unexpected file in npm package: ${file.path}`); |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +if (pack.name !== "hunkdiff") { |
| 73 | + throw new Error(`Expected npm package name to be hunkdiff, got ${pack.name}.`); |
| 74 | +} |
| 75 | + |
| 76 | +console.log(`Verified npm pack output for ${pack.name}@${pack.version} (${pack.entryCount} files).`); |
0 commit comments