Files
tutti/tools/scripts/stage-claude-code-binaries.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

244 lines
8.1 KiB
JavaScript

#!/usr/bin/env node
// Stages zstd-compressed Claude Code native binaries for CDN publishing.
//
// The desktop bundle no longer vendors @anthropic-ai/claude-agent-sdk-<platform>
// (~230MB per platform); tuttid downloads the binary at runtime from
// <cdn>/claude-code/<claudeVersion>/claude-<platform>.zst (see
// services/tuttid/service/agentstatus/claude_binary.go). This script prepares
// exactly that layout from the npm packages pinned by the sidecar:
//
// 1. Resolve the pinned @anthropic-ai/claude-agent-sdk version from
// packages/agent/claude-sdk-sidecar/package.json.
// 2. npm-pack the SDK, verify the tarball against the sha512 integrity
// recorded in pnpm-lock.yaml (so a registry cannot substitute bytes the
// repository did not pin), then read manifest.json (claude version +
// per-platform sha256/size of the raw binaries).
// 3. For each platform package: npm-pack, verify against pnpm-lock.yaml,
// extract the binary, verify its sha256 and size against the SDK
// manifest, compress with zstd.
// 4. Emit <out>/<claudeVersion>/claude-<platform>.zst plus a copy of the
// SDK manifest for observability.
//
// Usage: node tools/scripts/stage-claude-code-binaries.mjs [--out <dir>] [--platforms a,b,c]
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
statSync
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const DEFAULT_PLATFORMS = [
"darwin-arm64",
"darwin-x64",
"linux-x64",
"linux-arm64",
"win32-x64",
"win32-arm64"
];
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = join(__dirname, "..", "..");
function log(message) {
process.stderr.write(`[stage-claude-code-binaries] ${message}\n`);
}
function argValue(flag) {
const index = process.argv.indexOf(flag);
if (index < 0) {
return undefined;
}
const value = process.argv[index + 1];
if (value === undefined || value === "" || value.startsWith("--")) {
throw new Error(`${flag} requires a value`);
}
return value;
}
function sha256File(path) {
return createHash("sha256").update(readFileSync(path)).digest("hex");
}
// Verifies a packed tarball against the sha512 integrity pinned in
// pnpm-lock.yaml, closing the loop where the SDK manifest and the binaries
// would otherwise both come from the same unpinned registry source.
function verifyTarballAgainstLockfile(tarballPath, packageName, version) {
const lockfile = readFileSync(join(repoRoot, "pnpm-lock.yaml"), "utf8");
const escaped = `${packageName}@${version}`.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&"
);
const match = lockfile.match(
new RegExp(
`"${escaped}":\\s*\\n\\s*resolution:[\\s\\S]{0,200}?integrity: (sha512-[A-Za-z0-9+/=]+)`
)
);
if (!match) {
throw new Error(
`pnpm-lock.yaml has no integrity entry for ${packageName}@${version}; ` +
`refusing to publish unpinned bytes`
);
}
const expected = match[1];
const actual =
"sha512-" +
createHash("sha512").update(readFileSync(tarballPath)).digest("base64");
if (actual !== expected) {
throw new Error(
`tarball integrity mismatch for ${packageName}@${version}: ` +
`got ${actual}, pnpm-lock.yaml pins ${expected}`
);
}
}
function npmPack(packageSpec, destination) {
const output = execFileSync(
"npm",
["pack", packageSpec, "--json", "--pack-destination", destination],
{ encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] }
);
const filename = JSON.parse(output)[0]?.filename;
if (typeof filename !== "string" || filename.length === 0) {
throw new Error(`npm pack did not return a filename for ${packageSpec}`);
}
return join(destination, filename);
}
function extractFromTarball(tarball, memberPath, destinationDir) {
execFileSync(
"tar",
["-xzf", tarball, "--strip-components=1", "-C", destinationDir, memberPath],
{ stdio: "inherit" }
);
}
const sidecarPackage = JSON.parse(
readFileSync(
join(repoRoot, "packages", "agent", "claude-sdk-sidecar", "package.json"),
"utf8"
)
);
const sdkVersion =
sidecarPackage.dependencies?.["@anthropic-ai/claude-agent-sdk"];
if (typeof sdkVersion !== "string" || !/^\d+\.\d+\.\d+$/.test(sdkVersion)) {
throw new Error(
`sidecar must pin an exact @anthropic-ai/claude-agent-sdk version, got: ${sdkVersion}`
);
}
const outRoot = resolve(
argValue("--out") ?? join(repoRoot, "dist", "claude-code")
);
const platforms = (argValue("--platforms") ?? DEFAULT_PLATFORMS.join(","))
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
const stagingDir = mkdtempSync(join(tmpdir(), "claude-code-stage-"));
try {
log(`packing @anthropic-ai/claude-agent-sdk@${sdkVersion} for its manifest`);
const sdkTarball = npmPack(
`@anthropic-ai/claude-agent-sdk@${sdkVersion}`,
stagingDir
);
verifyTarballAgainstLockfile(
sdkTarball,
"@anthropic-ai/claude-agent-sdk",
sdkVersion
);
const sdkDir = join(stagingDir, "sdk");
mkdirSync(sdkDir, { recursive: true });
extractFromTarball(sdkTarball, "package/manifest.json", sdkDir);
const manifestPath = join(sdkDir, "manifest.json");
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
const claudeVersion = manifest.version;
// The version becomes a CDN path segment (and an S3 key in the publish
// workflow); only a plain semver is acceptable.
if (
typeof claudeVersion !== "string" ||
!/^\d+\.\d+\.\d+$/.test(claudeVersion)
) {
throw new Error(
`SDK manifest.json does not declare a plain semver claude version: ${claudeVersion}`
);
}
const versionDir = join(outRoot, claudeVersion);
mkdirSync(versionDir, { recursive: true });
copyFileSync(manifestPath, join(versionDir, "manifest.json"));
const staged = [];
for (const platform of platforms) {
const platformManifest = manifest.platforms?.[platform];
if (
!platformManifest?.binary ||
!platformManifest?.checksum ||
!Number.isInteger(platformManifest?.size) ||
platformManifest.size <= 0
) {
throw new Error(
`SDK manifest has no complete platform entry for ${platform}`
);
}
const packageName = `@anthropic-ai/claude-agent-sdk-${platform}`;
const packageSpec = `${packageName}@${sdkVersion}`;
log(`packing ${packageSpec}`);
const tarball = npmPack(packageSpec, stagingDir);
verifyTarballAgainstLockfile(tarball, packageName, sdkVersion);
const extractDir = join(stagingDir, platform);
mkdirSync(extractDir, { recursive: true });
extractFromTarball(
tarball,
`package/${platformManifest.binary}`,
extractDir
);
const binaryPath = join(extractDir, platformManifest.binary);
// tuttid enforces the manifest size exactly at download time
// (decompressZstdFile); reject a drifting manifest here rather than
// shipping an artifact every client would refuse.
const binarySize = statSync(binaryPath).size;
if (binarySize !== platformManifest.size) {
throw new Error(
`size mismatch for ${platform}: binary is ${binarySize} bytes, manifest says ${platformManifest.size}`
);
}
const checksum = sha256File(binaryPath);
if (checksum.toLowerCase() !== platformManifest.checksum.toLowerCase()) {
throw new Error(
`sha256 mismatch for ${platform}: got ${checksum}, manifest says ${platformManifest.checksum}`
);
}
const outputPath = join(versionDir, `claude-${platform}.zst`);
log(`compressing ${platform} binary`);
execFileSync("zstd", ["-19", "-T0", "-f", "-o", outputPath, binaryPath], {
stdio: "inherit"
});
rmSync(tarball, { force: true });
rmSync(extractDir, { recursive: true, force: true });
staged.push({ platform, path: outputPath });
}
process.stdout.write(
JSON.stringify(
{
sdkVersion,
claudeVersion,
outRoot,
files: staged.map(({ platform, path }) => ({ platform, path }))
},
null,
2
) + "\n"
);
} finally {
rmSync(stagingDir, { recursive: true, force: true });
}