Windows ships two tar implementations. `C:\Windows\System32\tar.exe` (bsdtar) terminates every listed member with CRLF, so `verify-package-pack.mjs` splitting the listing on a bare "\n" left a trailing "\r" on each non-final member. `verifyPackedBridge` compares member identities exactly, so it reported `package omitted package/bridge/hopper_bridge.py` although the tarball ships that member (#4128). `parseTarListing` normalizes line endings once, at the listing owner, and preserves every member string beyond its line ending. `verifyPackagePack` is the only tar listing producer in the package verifiers, so its consumers now compare bare member names; the prefix checks in the same file keep their behavior because they never matched a line ending. Covered by `tests/conformance/package/packedTarListing.test.ts`: CRLF and LF listings both yield exact bridge identities, the trailing line ending no longer produces an empty member, member strings keep embedded spaces, and a regression guard pins the previous parse's failure. Real Windows 11 evidence (`package/bridge/hopper_bridge.py` in the listing): | tar | listing | split("\n") | parseTarListing | | ---------------------------------- | ------- | ----------- | --------------- | | Git for Windows GNU tar | LF | true | true | | C:\Windows\System32\tar.exe | CRLF | false | true | Fixes #4128 Co-authored-by: Chow Kim <catch@126.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
2.5 KiB
JavaScript
72 lines
2.5 KiB
JavaScript
import { mkdir } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
|
|
|
|
import { exec, execNpm } from "./lib/verify-package-core.mjs";
|
|
import { parseTarListing } from "./lib/verify-package-listing.mjs";
|
|
import { verifyPackedBridge } from "./verify-packed-bridge.mjs";
|
|
|
|
/** Create a tarball and assert production packaging constraints. */
|
|
export async function verifyPackagePack({ root, workspace }) {
|
|
const packDirectory = join(workspace, "pack");
|
|
await mkdir(packDirectory);
|
|
const tarball = join(
|
|
packDirectory,
|
|
(
|
|
await execNpm(["pack", "--silent", "--pack-destination", packDirectory], {
|
|
cwd: root,
|
|
})
|
|
).stdout.trim(),
|
|
);
|
|
const packedFiles = parseTarListing(
|
|
(await exec("tar", ["-tf", tarball])).stdout,
|
|
);
|
|
const packedManifest = JSON.parse(
|
|
(await exec("tar", ["-xOf", tarball, "package/package.json"])).stdout,
|
|
);
|
|
if (
|
|
packedManifest.scripts?.postinstall !== undefined ||
|
|
packedManifest.dependencies?.["node-pty"] !== undefined ||
|
|
packedManifest.dependencies?.["@lydell/node-pty"] !== "1.1.0"
|
|
)
|
|
throw new Error("package retained a lifecycle-dependent PTY installation");
|
|
if (
|
|
packedManifest.bin?.rea !== "scripts/rea.mjs" ||
|
|
packedManifest.bin?.["rea-agents"] !== "scripts/rea.mjs"
|
|
)
|
|
throw new Error("package did not expose both rea command entry points");
|
|
if (packedManifest.mcpName !== "io.github.morluto/rea")
|
|
throw new Error("package did not retain its MCP Registry ownership marker");
|
|
if (packedFiles.some((path) => path.startsWith("package/.agents/")))
|
|
throw new Error(
|
|
"package included authored skill sources instead of only the generated bundle",
|
|
);
|
|
if (packedFiles.some((path) => path.startsWith("package/.cache/")))
|
|
throw new Error("package included development cache or test metadata");
|
|
const skill = (
|
|
await exec("tar", [
|
|
"-xOf",
|
|
tarball,
|
|
"package/skills/reverse-engineer-anything/SKILL.md",
|
|
])
|
|
).stdout;
|
|
if (
|
|
!skill.includes(
|
|
` tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}\n`,
|
|
) ||
|
|
skill.includes(" catalog_digest:")
|
|
)
|
|
throw new Error(
|
|
"packaged skill metadata did not match the catalog shipped in the package",
|
|
);
|
|
if (
|
|
packedFiles.some(
|
|
(path) => path.includes("__pycache__") || path.endsWith(".pyc"),
|
|
)
|
|
) {
|
|
throw new Error("package contained generated Python bytecode");
|
|
}
|
|
await verifyPackedBridge({ workspace, tarball, packedFiles });
|
|
return { tarball };
|
|
}
|