mirror of
https://github.com/aphrody-labs/bun-distribution.git
synced 2026-10-14 07:59:17 +00:00
958 lines
42 KiB
TypeScript
958 lines
42 KiB
TypeScript
#!/usr/bin/env bun
|
|
import { lstat, mkdir, open, readdir, realpath, rename, rm, rmdir, unlink } from "node:fs/promises";
|
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
import { assertRuntimeFacts, currentRuntimeFacts, hashFile, loadConfig, repositoryRoot } from "@aphrody/bun-workspace";
|
|
import { runProcess } from "@aphrody/bun-workspace/process";
|
|
|
|
const packageSpecs = {
|
|
paths: { directory: "paths", name: "@aphrody/paths" },
|
|
ovh: { directory: "ovh", name: "@aphrody/ovh" },
|
|
"kernel-client": { directory: "kernel-client", name: "@aphrody/os" },
|
|
} as const;
|
|
|
|
type PackageId = keyof typeof packageSpecs;
|
|
type Diagnostic = { code: string; message: string; path?: string };
|
|
type PackagePlan = {
|
|
id: PackageId;
|
|
name: string;
|
|
status: "ready" | "blocked";
|
|
packagePath: string;
|
|
outputPath: string;
|
|
entries: Array<{ export: string; source: string; sha256: string; typesSource: string; typesSHA256: string }>;
|
|
diagnostics: Diagnostic[];
|
|
existing?: { schemaVersion: number; treeHash: string; identityKey?: string };
|
|
disposition?: "compile" | "recompile" | "reuse";
|
|
};
|
|
|
|
const usage = `Usage: bun scripts/build-packages.ts <plan|build> --package <paths|ovh|kernel-client> [--package ...]
|
|
Select every package explicitly. Outputs are written only to build/packages/<package>.
|
|
Build records compiled JavaScript only; it never executes or deploys the output.`;
|
|
const maxSnapshotFileBytes = 50 * 1024 * 1024;
|
|
const maxSnapshotBytes = 512 * 1024 * 1024;
|
|
const excludedDirectories = new Set([".git", "node_modules", "dist", "build"]);
|
|
|
|
function inside(base: string, target: string, allowEqual = false): boolean {
|
|
const local = relative(resolve(base), resolve(target));
|
|
return (
|
|
(allowEqual && local === "") || (!!local && local !== ".." && !local.startsWith(`..${sep}`) && !isAbsolute(local))
|
|
);
|
|
}
|
|
|
|
function relativePath(root: string, path: string): string {
|
|
return relative(root, path).split(sep).join("/");
|
|
}
|
|
|
|
function fail(code: string, message: string, path?: string): Diagnostic {
|
|
return path ? { code, message, path } : { code, message };
|
|
}
|
|
|
|
async function assertNoSymlinkComponents(root: string, path: string): Promise<void> {
|
|
if (!inside(root, path, true)) throw new Error(`Path escapes the checkout: ${path}`);
|
|
const local = relative(root, path);
|
|
let current = root;
|
|
for (const part of local.split(sep).filter(Boolean)) {
|
|
current = join(current, part);
|
|
try {
|
|
if ((await lstat(current)).isSymbolicLink()) throw new Error(`Symlink path is not allowed: ${current}`);
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.startsWith("Symlink path")) throw error;
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function workspaceMemberPaths(root: string, patterns: string[]): Promise<Set<string>> {
|
|
const included = new Set<string>();
|
|
const excluded: Bun.Glob[] = [];
|
|
for (const pattern of patterns) {
|
|
const omit = pattern.startsWith("!");
|
|
const normalized = (omit ? pattern.slice(1) : pattern).replaceAll("\\", "/").replace(/\/$/, "");
|
|
if (!normalized || normalized.startsWith("/") || normalized.split("/").includes(".."))
|
|
throw new Error(`Invalid Bun workspace pattern: ${pattern}`);
|
|
const manifestGlob = new Bun.Glob(`${normalized}/package.json`);
|
|
if (omit) {
|
|
excluded.push(manifestGlob);
|
|
continue;
|
|
}
|
|
for await (const manifest of manifestGlob.scan({ cwd: root, onlyFiles: true })) {
|
|
included.add(manifest.replaceAll("\\", "/").slice(0, -"/package.json".length));
|
|
}
|
|
}
|
|
for (const pattern of excluded) {
|
|
for await (const manifest of pattern.scan({ cwd: root, onlyFiles: true })) {
|
|
included.delete(manifest.replaceAll("\\", "/").slice(0, -"/package.json".length));
|
|
}
|
|
}
|
|
return included;
|
|
}
|
|
|
|
function record(value: unknown): value is Record<string, unknown> {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function exportTargets(value: unknown): { runtime?: string; types?: string } {
|
|
if (typeof value === "string") return { runtime: value, types: value };
|
|
if (!record(value)) return {};
|
|
const types = typeof value.types === "string" ? value.types : undefined;
|
|
for (const condition of ["bun", "import", "node", "default"]) {
|
|
if (!(condition in value)) continue;
|
|
const nested = exportTargets(value[condition]);
|
|
if (nested.runtime) return { runtime: nested.runtime, types: types ?? nested.types };
|
|
}
|
|
return types ? { types } : {};
|
|
}
|
|
|
|
function exportRows(exportsValue: unknown): Array<[string, unknown]> {
|
|
if (typeof exportsValue === "string") return [[".", exportsValue]];
|
|
if (!record(exportsValue)) throw new Error("package.json exports must be a string or object.");
|
|
const entries = Object.entries(exportsValue);
|
|
if (entries.some(([key]) => key === "." || key.startsWith("./"))) return entries;
|
|
return [[".", exportsValue]];
|
|
}
|
|
|
|
async function resolveSource(packageDir: string, target: string, label: string): Promise<string> {
|
|
if (!target.startsWith("./") || target.includes("\\") || target.includes("\0"))
|
|
throw new Error(`${label} must be a package-relative export path: ${target}`);
|
|
const path = resolve(packageDir, target.slice(2));
|
|
if (!inside(packageDir, path)) throw new Error(`${label} escapes its package: ${target}`);
|
|
await assertNoSymlinkComponents(packageDir, path);
|
|
const real = await realpath(path);
|
|
if (!inside(packageDir, real)) throw new Error(`${label} resolves outside its package: ${target}`);
|
|
if (!/\.(?:ts|tsx|mts|js|jsx|mjs|cjs)$/.test(real))
|
|
throw new Error(`${label} must point to TypeScript or JavaScript: ${target}`);
|
|
return real;
|
|
}
|
|
|
|
async function packagePlan(root: string, rootPackage: Record<string, unknown>, id: PackageId): Promise<PackagePlan> {
|
|
const spec = packageSpecs[id];
|
|
const packagePath = `packages/${spec.directory}`;
|
|
const packageDir = resolve(root, packagePath);
|
|
const outputPath = `build/packages/${spec.directory}`;
|
|
const outputDir = resolve(root, outputPath);
|
|
const diagnostics: Diagnostic[] = [];
|
|
const result: PackagePlan = {
|
|
id,
|
|
name: spec.name,
|
|
status: "blocked",
|
|
packagePath,
|
|
outputPath,
|
|
entries: [],
|
|
diagnostics,
|
|
};
|
|
|
|
const workspacePatterns = rootPackage.workspaces;
|
|
if (!Array.isArray(workspacePatterns) || workspacePatterns.some(pattern => typeof pattern !== "string")) {
|
|
diagnostics.push(fail("ROOT_WORKSPACES_INVALID", "Root package.json must declare Bun workspace patterns."));
|
|
return result;
|
|
}
|
|
const members = await workspaceMemberPaths(root, workspacePatterns as string[]);
|
|
if (!members.has(packagePath))
|
|
diagnostics.push(fail("WORKSPACE_MEMBER_MISSING", `${packagePath} is not an actual root Bun workspace member.`));
|
|
|
|
try {
|
|
await assertNoSymlinkComponents(root, packageDir);
|
|
const manifestPath = join(packageDir, "package.json");
|
|
await assertNoSymlinkComponents(packageDir, manifestPath);
|
|
const packageJSON = await Bun.file(manifestPath).json();
|
|
if (!record(packageJSON)) throw new Error("Package manifest must be a JSON object.");
|
|
if (packageJSON.name !== spec.name) throw new Error(`Expected package name ${spec.name}.`);
|
|
if (packageJSON.version !== rootPackage.version)
|
|
throw new Error("Package version differs from the root product version.");
|
|
const rows = exportRows(packageJSON.exports);
|
|
const seenEntries = new Set<string>();
|
|
for (const [key, value] of rows) {
|
|
if (key !== "." && !key.startsWith("./")) throw new Error(`Unsupported package export key: ${key}`);
|
|
const targets = exportTargets(value);
|
|
const runtime = targets.runtime ?? targets.types;
|
|
if (!runtime) throw new Error(`Export ${key} has no Bun/import/default JavaScript entry.`);
|
|
const source = await resolveSource(packageDir, runtime, `Export ${key}`);
|
|
const typesSource = await resolveSource(packageDir, targets.types ?? runtime, `Types export ${key}`);
|
|
if (!/\.(?:ts|tsx|mts)$/.test(typesSource))
|
|
throw new Error(`Types export ${key} must point to a TypeScript source.`);
|
|
if (seenEntries.has(source)) throw new Error(`Two exports resolve to the same build entry: ${runtime}`);
|
|
seenEntries.add(source);
|
|
result.entries.push({
|
|
export: key,
|
|
source,
|
|
sha256: await hashFile(source, { maxBytes: 20 * 1024 * 1024 }),
|
|
typesSource,
|
|
typesSHA256: await hashFile(typesSource, { maxBytes: 20 * 1024 * 1024 }),
|
|
});
|
|
}
|
|
if (result.entries.length === 0) throw new Error("Package exports no JavaScript entrypoints.");
|
|
} catch (error) {
|
|
diagnostics.push(
|
|
fail("PACKAGE_PREFLIGHT_FAILED", error instanceof Error ? error.message : String(error), packagePath),
|
|
);
|
|
}
|
|
|
|
try {
|
|
await assertNoSymlinkComponents(root, outputDir);
|
|
if (!(await missing(outputDir))) {
|
|
const owned = await inspectOwnedOutput(root, result, outputDir);
|
|
result.existing = {
|
|
schemaVersion: owned.receipt.schemaVersion,
|
|
treeHash: owned.treeHash,
|
|
identityKey: owned.receipt.identity?.key,
|
|
};
|
|
}
|
|
} catch (error) {
|
|
diagnostics.push(fail("OUTPUT_PATH_INVALID", error instanceof Error ? error.message : String(error), outputPath));
|
|
}
|
|
|
|
result.status = diagnostics.length === 0 ? "ready" : "blocked";
|
|
if (result.status === "ready") {
|
|
try {
|
|
const identity = await captureIdentity(root, result);
|
|
result.disposition = !result.existing
|
|
? "compile"
|
|
: result.existing.schemaVersion === 2 && result.existing.identityKey === identity.key
|
|
? "reuse"
|
|
: "recompile";
|
|
} catch (error) {
|
|
diagnostics.push(
|
|
fail("BUILD_IDENTITY_INVALID", error instanceof Error ? error.message : String(error), packagePath),
|
|
);
|
|
result.status = "blocked";
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
type SourceSnapshot = Map<string, { sha256: string; bytes: number }>;
|
|
type FileProof = { path: string; sha256: string; bytes: number };
|
|
type ReceiptEntry = { export: string; path: string; sha256: string; typesPath: string; typesSHA256: string };
|
|
const buildOptions = {
|
|
naming: { entry: "[dir]/[name].js", chunk: "chunk-[hash].js", asset: "asset-[name]-[hash]" },
|
|
target: "bun",
|
|
packages: "external",
|
|
metafile: true,
|
|
sourcemap: "none",
|
|
} as const;
|
|
const rootInputFiles = [
|
|
"package.json",
|
|
"bun.lock",
|
|
"tsconfig.json",
|
|
"aphrody.config.json",
|
|
"bunfig.toml",
|
|
"scripts/build-packages.ts",
|
|
];
|
|
type BuildIdentity = {
|
|
key: string;
|
|
inputs: {
|
|
packageSnapshot: FileProof[];
|
|
ownerSnapshot: FileProof[];
|
|
rootFiles: Array<{ path: string; sha256: string | null; bytes: number | null }>;
|
|
runtime: { facts: ReturnType<typeof currentRuntimeFacts>; sha256: string };
|
|
entries: ReceiptEntry[];
|
|
options: typeof buildOptions;
|
|
};
|
|
};
|
|
type Receipt = {
|
|
schemaVersion: 1 | 2;
|
|
package: { id: PackageId; name: string; version: string };
|
|
source: { packagePath: string; entries: ReceiptEntry[]; inputs: FileProof[] };
|
|
outputs: Array<FileProof & { kind: string }>;
|
|
qualification: { compiledJS: boolean; executed: false; deployed: false };
|
|
createdAt: string;
|
|
identity?: BuildIdentity;
|
|
};
|
|
|
|
function canonical(value: unknown): string {
|
|
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
if (record(value))
|
|
return `{${Object.keys(value)
|
|
.sort()
|
|
.map(key => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
.join(",")}}`;
|
|
return JSON.stringify(value);
|
|
}
|
|
function digest(value: unknown): string {
|
|
return new Bun.CryptoHasher("sha256").update(canonical(value)).digest("hex");
|
|
}
|
|
function exact(value: unknown, keys: string[]): asserts value is Record<string, unknown> {
|
|
if (!record(value) || canonical(Object.keys(value).sort()) !== canonical(keys.sort()))
|
|
throw new Error("Receipt has unknown or missing fields.");
|
|
}
|
|
function sha(value: unknown): value is string {
|
|
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
}
|
|
function validPath(path: unknown, prefix: string): path is string {
|
|
return (
|
|
typeof path === "string" &&
|
|
path.startsWith(`${prefix}/`) &&
|
|
!path.includes("\\") &&
|
|
!path.includes("\0") &&
|
|
path.split("/").every(part => part && part !== "." && part !== "..")
|
|
);
|
|
}
|
|
function rows(value: unknown, prefix: string, output = false): void {
|
|
if (!Array.isArray(value) || !value.length || value.length > 4096) throw new Error("Receipt file ledger is invalid.");
|
|
const seen = new Set<string>();
|
|
for (const item of value) {
|
|
exact(item, output ? ["path", "sha256", "bytes", "kind"] : ["path", "sha256", "bytes"]);
|
|
if (
|
|
!validPath(item.path, prefix) ||
|
|
!sha(item.sha256) ||
|
|
typeof item.bytes !== "number" ||
|
|
!Number.isSafeInteger(item.bytes) ||
|
|
item.bytes < 0 ||
|
|
seen.has(item.path)
|
|
)
|
|
throw new Error("Receipt file proof is invalid.");
|
|
if (output && !["entry-point", "chunk", "asset", "sourcemap"].includes(String(item.kind)))
|
|
throw new Error("Unknown output artifact kind.");
|
|
seen.add(item.path);
|
|
}
|
|
}
|
|
function entries(value: unknown, prefix: string): void {
|
|
if (!Array.isArray(value) || !value.length || value.length > 256) throw new Error("Receipt exports are invalid.");
|
|
const seen = new Set<string>();
|
|
for (const item of value) {
|
|
exact(item, ["export", "path", "sha256", "typesPath", "typesSHA256"]);
|
|
if (
|
|
typeof item.export !== "string" ||
|
|
(item.export !== "." && !item.export.startsWith("./")) ||
|
|
seen.has(item.export) ||
|
|
!validPath(item.path, prefix) ||
|
|
!validPath(item.typesPath, prefix) ||
|
|
!sha(item.sha256) ||
|
|
!sha(item.typesSHA256)
|
|
)
|
|
throw new Error("Receipt export proof is invalid.");
|
|
seen.add(item.export);
|
|
}
|
|
}
|
|
function validateReceipt(value: unknown, plan: PackagePlan): Receipt {
|
|
if (!record(value) || (value.schemaVersion !== 1 && value.schemaVersion !== 2))
|
|
throw new Error("Unknown output receipt version.");
|
|
exact(value, [
|
|
"schemaVersion",
|
|
"package",
|
|
"source",
|
|
"outputs",
|
|
"qualification",
|
|
"createdAt",
|
|
...(value.schemaVersion === 2 ? ["identity"] : []),
|
|
]);
|
|
exact(value.package, ["id", "name", "version"]);
|
|
if (value.package.id !== plan.id || value.package.name !== plan.name || value.package.version !== "1.4.4")
|
|
throw new Error("Receipt belongs to another package owner.");
|
|
exact(value.source, ["packagePath", "entries", "inputs"]);
|
|
if (value.source.packagePath !== plan.packagePath) throw new Error("Receipt source owner differs.");
|
|
entries(value.source.entries, plan.packagePath);
|
|
rows(value.source.inputs, plan.packagePath);
|
|
rows(value.outputs, plan.outputPath, true);
|
|
exact(value.qualification, ["compiledJS", "executed", "deployed"]);
|
|
if (
|
|
value.qualification.compiledJS !== true ||
|
|
value.qualification.executed !== false ||
|
|
value.qualification.deployed !== false ||
|
|
typeof value.createdAt !== "string" ||
|
|
!Number.isFinite(Date.parse(value.createdAt))
|
|
)
|
|
throw new Error("Receipt has no valid prior compilation attestation.");
|
|
const parsed = value as unknown as Receipt;
|
|
const expectedEntries = parsed.source.entries
|
|
.map(
|
|
entry =>
|
|
`${plan.outputPath}/${entry.path.slice(plan.packagePath.length + 1).replace(/\.(?:ts|tsx|mts|js|jsx|mjs|cjs)$/, ".js")}`,
|
|
)
|
|
.sort();
|
|
if (
|
|
canonical(expectedEntries) !==
|
|
canonical(
|
|
parsed.outputs
|
|
.filter(item => item.kind === "entry-point")
|
|
.map(item => item.path)
|
|
.sort(),
|
|
)
|
|
)
|
|
throw new Error("Receipt entry outputs do not match its exports.");
|
|
for (const entry of parsed.source.entries)
|
|
if (!parsed.source.inputs.some(input => input.path === entry.path && input.sha256 === entry.sha256))
|
|
throw new Error("Receipt entry lacks a compiler input proof.");
|
|
if (value.schemaVersion === 2) {
|
|
exact(value.identity, ["key", "inputs"]);
|
|
if (!sha(value.identity.key)) throw new Error("Invalid build identity hash.");
|
|
exact(value.identity.inputs, ["packageSnapshot", "ownerSnapshot", "rootFiles", "runtime", "entries", "options"]);
|
|
const identityInputs = value.identity.inputs;
|
|
rows(identityInputs.packageSnapshot, plan.packagePath);
|
|
rows(identityInputs.ownerSnapshot, "packages/bun-workspace");
|
|
entries(identityInputs.entries, plan.packagePath);
|
|
if (
|
|
canonical(identityInputs.entries) !== canonical(value.source.entries) ||
|
|
canonical(identityInputs.options) !== canonical(buildOptions)
|
|
)
|
|
throw new Error("Receipt compiler options or entries differ.");
|
|
if (!Array.isArray(identityInputs.rootFiles) || identityInputs.rootFiles.length !== rootInputFiles.length)
|
|
throw new Error("Root input proofs are incomplete.");
|
|
const rootPaths = new Set<string>();
|
|
for (const row of identityInputs.rootFiles) {
|
|
exact(row, ["path", "sha256", "bytes"]);
|
|
if (
|
|
typeof row.path !== "string" ||
|
|
!rootInputFiles.includes(row.path) ||
|
|
rootPaths.has(row.path) ||
|
|
!(
|
|
(sha(row.sha256) && typeof row.bytes === "number" && Number.isSafeInteger(row.bytes) && row.bytes >= 0) ||
|
|
(row.sha256 === null && row.bytes === null)
|
|
)
|
|
)
|
|
throw new Error("Root input proof is invalid.");
|
|
rootPaths.add(row.path);
|
|
}
|
|
exact(identityInputs.runtime, ["facts", "sha256"]);
|
|
exact(identityInputs.runtime.facts, ["version", "revision", "sourceUrl", "executable"]);
|
|
const facts = identityInputs.runtime.facts;
|
|
if (
|
|
!sha(identityInputs.runtime.sha256) ||
|
|
facts.version !== "1.4.4" ||
|
|
typeof facts.revision !== "string" ||
|
|
!/^[a-f0-9]{40}$/.test(facts.revision) ||
|
|
typeof facts.executable !== "string" ||
|
|
!isAbsolute(facts.executable) ||
|
|
(facts.sourceUrl !== null && typeof facts.sourceUrl !== "string") ||
|
|
digest(identityInputs) !== value.identity.key
|
|
)
|
|
throw new Error("Receipt runtime or identity is invalid.");
|
|
const sourceRows = parsed.identity!.inputs.packageSnapshot;
|
|
for (const input of parsed.source.inputs)
|
|
if (!sourceRows.some(row => canonical(row) === canonical(input)))
|
|
throw new Error("Compiler input is outside the identity snapshot.");
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
async function captureIdentity(root: string, plan: PackagePlan): Promise<BuildIdentity> {
|
|
const config = await loadConfig(root);
|
|
const facts = currentRuntimeFacts();
|
|
assertRuntimeFacts(config.runtime, facts);
|
|
const ledger = (snapshot: SourceSnapshot): FileProof[] =>
|
|
[...snapshot]
|
|
.map(([path, item]) => ({ path: relativePath(root, path), ...item }))
|
|
.sort((a, b) => a.path.localeCompare(b.path));
|
|
const rootFiles = [];
|
|
for (const path of rootInputFiles) {
|
|
const absolute = resolve(root, path);
|
|
await assertNoSymlinkComponents(root, absolute);
|
|
try {
|
|
const metadata = await lstat(absolute);
|
|
if (!metadata.isFile()) throw new Error("Root compiler input is not a regular file.");
|
|
rootFiles.push({
|
|
path,
|
|
sha256: await hashFile(absolute, { maxBytes: maxSnapshotFileBytes, expectedBytes: metadata.size }),
|
|
bytes: metadata.size,
|
|
});
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT" || path !== "bunfig.toml") throw error;
|
|
rootFiles.push({ path, sha256: null, bytes: null });
|
|
}
|
|
}
|
|
const inputs: BuildIdentity["inputs"] = {
|
|
packageSnapshot: ledger(await snapshotPackageSources(resolve(root, plan.packagePath))),
|
|
ownerSnapshot: ledger(await snapshotPackageSources(join(root, "packages/bun-workspace"))),
|
|
rootFiles,
|
|
runtime: { facts, sha256: await hashFile(facts.executable, { maxBytes: 512 * 1024 * 1024 }) },
|
|
entries: plan.entries.map(entry => ({
|
|
export: entry.export,
|
|
path: relativePath(root, entry.source),
|
|
sha256: entry.sha256,
|
|
typesPath: relativePath(root, entry.typesSource),
|
|
typesSHA256: entry.typesSHA256,
|
|
})),
|
|
options: buildOptions,
|
|
};
|
|
for (const entry of inputs.entries) {
|
|
if (
|
|
!inputs.packageSnapshot.some(row => row.path === entry.path && row.sha256 === entry.sha256) ||
|
|
!inputs.packageSnapshot.some(row => row.path === entry.typesPath && row.sha256 === entry.typesSHA256)
|
|
)
|
|
throw new Error("Entry source changed after planning.");
|
|
}
|
|
return { key: digest(inputs), inputs };
|
|
}
|
|
|
|
async function inspectOwnedOutput(
|
|
root: string,
|
|
plan: PackagePlan,
|
|
directory: string,
|
|
): Promise<{ receipt: Receipt; treeHash: string }> {
|
|
await assertNoSymlinkComponents(root, directory);
|
|
if (!(await lstat(directory)).isDirectory() || !inside(root, await realpath(directory)))
|
|
throw new Error("Output directory is not a real owned directory.");
|
|
const receiptPath = join(directory, "build-receipt.json");
|
|
await assertNoSymlinkComponents(root, receiptPath);
|
|
const receiptFile = Bun.file(receiptPath);
|
|
if (receiptFile.size > 2 * 1024 * 1024) throw new Error("Output receipt exceeds its limit.");
|
|
const receiptBytes = await receiptFile.arrayBuffer();
|
|
if (receiptBytes.byteLength > 2 * 1024 * 1024) throw new Error("Output receipt exceeds its limit.");
|
|
const receiptHash = new Bun.CryptoHasher("sha256").update(receiptBytes).digest("hex");
|
|
const receipt = validateReceipt(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(receiptBytes)), plan);
|
|
const expected = new Map(receipt.outputs.map(item => [item.path.slice(plan.outputPath.length + 1), item]));
|
|
if (expected.has("build-receipt.json")) throw new Error("Receipt cannot declare itself as an artifact.");
|
|
const expectedDirectories = new Set<string>();
|
|
for (const path of expected.keys())
|
|
for (let parent = dirname(path); parent !== "."; parent = dirname(parent))
|
|
expectedDirectories.add(parent.replaceAll("\\", "/"));
|
|
const found: FileProof[] = [];
|
|
const pending = [directory];
|
|
while (pending.length) {
|
|
const current = pending.pop()!;
|
|
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
const path = join(current, entry.name);
|
|
await assertNoSymlinkComponents(root, path);
|
|
const metadata = await lstat(path);
|
|
const local = relativePath(directory, path);
|
|
if (metadata.isDirectory()) {
|
|
if (!expectedDirectories.has(local)) throw new Error("Output tree contains an undeclared directory.");
|
|
pending.push(path);
|
|
} else if (metadata.isFile()) {
|
|
const item = expected.get(local);
|
|
if (local !== "build-receipt.json" && !item) throw new Error("Output tree contains a foreign file.");
|
|
const hash = await hashFile(path, { maxBytes: 200 * 1024 * 1024, expectedBytes: item?.bytes ?? metadata.size });
|
|
if (item && hash !== item.sha256) throw new Error("Output artifact hash differs from its receipt.");
|
|
if (local === "build-receipt.json" && (hash !== receiptHash || metadata.size !== receiptBytes.byteLength))
|
|
throw new Error("Receipt changed during output verification.");
|
|
found.push({ path: local, sha256: hash, bytes: metadata.size });
|
|
} else throw new Error("Output tree contains a non-regular node.");
|
|
}
|
|
}
|
|
if (found.length !== expected.size + 1) throw new Error("Output tree is incomplete.");
|
|
return { receipt, treeHash: digest(found.sort((a, b) => a.path.localeCompare(b.path))) };
|
|
}
|
|
|
|
async function snapshotPackageSources(packageDir: string): Promise<SourceSnapshot> {
|
|
const snapshot: SourceSnapshot = new Map();
|
|
let totalBytes = 0;
|
|
const pending = [packageDir];
|
|
while (pending.length) {
|
|
const directory = pending.pop()!;
|
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
if (excludedDirectories.has(entry.name)) continue;
|
|
if (/^(?:\.env(?:\..*)?|\.ovh\.conf|credentials(?:\..*)?|profiles\.json)$/i.test(entry.name))
|
|
throw new Error("Protected configuration is outside the package compiler snapshot contract.");
|
|
const path = join(directory, entry.name);
|
|
const metadata = await lstat(path);
|
|
if (metadata.isSymbolicLink()) throw new Error(`Package source tree contains a symlink: ${path}`);
|
|
if (metadata.isDirectory()) {
|
|
pending.push(path);
|
|
continue;
|
|
}
|
|
if (!metadata.isFile()) continue;
|
|
const sha256 = await hashFile(path, { maxBytes: maxSnapshotFileBytes, expectedBytes: metadata.size });
|
|
totalBytes += metadata.size;
|
|
if (totalBytes > maxSnapshotBytes) throw new Error("Package source snapshot exceeds the 512 MiB limit.");
|
|
snapshot.set(await realpath(path), { sha256, bytes: metadata.size });
|
|
}
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
async function resolveMetafileInput(
|
|
root: string,
|
|
packageDir: string,
|
|
input: string,
|
|
snapshot: SourceSnapshot,
|
|
): Promise<string> {
|
|
const candidates = isAbsolute(input) ? [resolve(input)] : [resolve(root, input), resolve(packageDir, input)];
|
|
for (const candidate of candidates) {
|
|
if (!inside(packageDir, candidate)) continue;
|
|
try {
|
|
await assertNoSymlinkComponents(packageDir, candidate);
|
|
const canonical = await realpath(candidate);
|
|
if (snapshot.has(canonical)) return canonical;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
}
|
|
}
|
|
throw new Error(`Bundler input is not a pre-snapshotted file inside its package: ${input}`);
|
|
}
|
|
|
|
async function resolveBuildOutput(root: string, outputDir: string, output: string): Promise<string> {
|
|
const candidates = isAbsolute(output) ? [resolve(output)] : [resolve(outputDir, output), resolve(root, output)];
|
|
for (const candidate of candidates) {
|
|
if (!inside(outputDir, candidate)) continue;
|
|
try {
|
|
await assertNoSymlinkComponents(root, candidate);
|
|
const canonical = await realpath(candidate);
|
|
if (inside(outputDir, canonical)) return canonical;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
}
|
|
}
|
|
throw new Error(`Bundler output is not a file inside its package output directory: ${output}`);
|
|
}
|
|
|
|
function diagnosticsFromBuild(
|
|
logs: Bun.BuildOutput["logs"],
|
|
): Array<Diagnostic & { level?: string; position?: unknown }> {
|
|
return logs.map(log => ({
|
|
code: "code" in log ? log.code : "BUN_BUILD_DIAGNOSTIC",
|
|
message: log.message,
|
|
level: log.level,
|
|
...(log.position ? { position: log.position } : {}),
|
|
}));
|
|
}
|
|
|
|
async function compilePackage(
|
|
root: string,
|
|
plan: PackagePlan,
|
|
outputDir: string,
|
|
expectedIdentity: string,
|
|
): Promise<Record<string, unknown>> {
|
|
const packageDir = resolve(root, plan.packagePath);
|
|
if (resolve(process.cwd()) !== root)
|
|
throw new Error("Run package builds from the Bun OS repository root so metafile paths have a stable base.");
|
|
const identity = await captureIdentity(root, plan);
|
|
if (identity.key !== expectedIdentity) throw new Error("Compiler inputs changed before the bounded worker started.");
|
|
const inputSnapshot = await snapshotPackageSources(packageDir);
|
|
for (const entry of plan.entries) {
|
|
const initial = inputSnapshot.get(entry.source);
|
|
const typeInitial = inputSnapshot.get(entry.typesSource);
|
|
if (!initial || initial.sha256 !== entry.sha256 || !typeInitial || typeInitial.sha256 !== entry.typesSHA256)
|
|
throw new Error(`Package entry changed after planning: ${entry.export}`);
|
|
}
|
|
await assertNoSymlinkComponents(root, dirname(outputDir));
|
|
await mkdir(outputDir);
|
|
await assertNoSymlinkComponents(root, outputDir);
|
|
const result = await Bun.build({
|
|
entrypoints: plan.entries.map(entry => entry.source),
|
|
outdir: outputDir,
|
|
root: packageDir,
|
|
...buildOptions,
|
|
});
|
|
const diagnostics = diagnosticsFromBuild(result.logs);
|
|
if (!result.success || !result.metafile) {
|
|
return {
|
|
id: plan.id,
|
|
name: plan.name,
|
|
status: "failed",
|
|
diagnostics,
|
|
qualification: { compiledJS: false, executed: false, deployed: false },
|
|
};
|
|
}
|
|
|
|
const inputHashes: Array<{ path: string; sha256: string; bytes: number }> = [];
|
|
for (const [input, metadata] of Object.entries(result.metafile.inputs)) {
|
|
const source = await resolveMetafileInput(root, packageDir, input, inputSnapshot);
|
|
const before = inputSnapshot.get(source)!;
|
|
if (before.bytes !== metadata.bytes) throw new Error(`Bundler input size changed after snapshot: ${input}`);
|
|
const sha256 = await hashFile(source, { maxBytes: maxSnapshotFileBytes, expectedBytes: metadata.bytes });
|
|
if (sha256 !== before.sha256) throw new Error(`Bundler input changed while compiling: ${input}`);
|
|
inputHashes.push({ path: relativePath(root, source), sha256, bytes: metadata.bytes });
|
|
}
|
|
|
|
const outputs: Array<{ path: string; sha256: string; bytes: number; kind: string }> = [];
|
|
let compiledJS = false;
|
|
for (const artifact of result.outputs) {
|
|
const path = await resolveBuildOutput(root, outputDir, artifact.path);
|
|
const sha256 = await hashFile(path, { maxBytes: 200 * 1024 * 1024 });
|
|
const bytes = Bun.file(path).size;
|
|
if (artifact.kind === "entry-point" && /\.(?:js|mjs|cjs)$/.test(path)) compiledJS = true;
|
|
outputs.push({ path: `${plan.outputPath}/${relativePath(outputDir, path)}`, sha256, bytes, kind: artifact.kind });
|
|
}
|
|
if (!compiledJS) throw new Error("Bun.build produced no JavaScript entry-point artifact.");
|
|
|
|
const receipt = {
|
|
schemaVersion: 2,
|
|
package: { id: plan.id, name: plan.name, version: "1.4.4" },
|
|
source: {
|
|
packagePath: plan.packagePath,
|
|
entries: plan.entries.map(entry => ({
|
|
export: entry.export,
|
|
path: relativePath(root, entry.source),
|
|
sha256: entry.sha256,
|
|
typesPath: relativePath(root, entry.typesSource),
|
|
typesSHA256: entry.typesSHA256,
|
|
})),
|
|
inputs: inputHashes,
|
|
},
|
|
outputs,
|
|
identity,
|
|
qualification: { compiledJS, executed: false, deployed: false },
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
validateReceipt(receipt, plan);
|
|
if ((await captureIdentity(root, plan)).key !== identity.key)
|
|
throw new Error("Build inputs changed while compiling.");
|
|
const receiptPath = join(outputDir, "build-receipt.json");
|
|
await assertNoSymlinkComponents(root, receiptPath);
|
|
const receiptHandle = await open(receiptPath, "wx", 0o600);
|
|
try {
|
|
await receiptHandle.writeFile(`${JSON.stringify(receipt, null, 2)}\n`);
|
|
} finally {
|
|
await receiptHandle.close();
|
|
}
|
|
await inspectOwnedOutput(root, plan, outputDir);
|
|
return {
|
|
id: plan.id,
|
|
name: plan.name,
|
|
status: "built",
|
|
receipt: `${plan.outputPath}/build-receipt.json`,
|
|
outputs,
|
|
diagnostics,
|
|
qualification: receipt.qualification,
|
|
};
|
|
}
|
|
|
|
async function guardOwnedMutation(root: string, id: PackageId, path: string): Promise<void> {
|
|
const base = resolve(root, "build/packages");
|
|
const local = relativePath(base, resolve(path));
|
|
const stage = new RegExp(
|
|
`^\\.stage-${id}-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}(?:/(?:payload|prior|lease.json))?$`,
|
|
);
|
|
if (local !== id && local !== `.${id}.lock` && !stage.test(local))
|
|
throw new Error("Mutation is outside this package owner's exact paths.");
|
|
await assertNoSymlinkComponents(root, path);
|
|
const parent = await realpath(dirname(path));
|
|
const actualBase = await realpath(base);
|
|
if (!inside(root, actualBase) || !inside(actualBase, parent, true))
|
|
throw new Error("Mutation parent resolves outside root build/packages.");
|
|
}
|
|
async function missing(path: string): Promise<boolean> {
|
|
try {
|
|
await lstat(path);
|
|
return false;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
|
|
throw error;
|
|
}
|
|
}
|
|
async function moveOwned(root: string, id: PackageId, from: string, to: string): Promise<void> {
|
|
await guardOwnedMutation(root, id, from);
|
|
await guardOwnedMutation(root, id, to);
|
|
if (!(await missing(to))) throw new Error("Refusing to overwrite an existing move destination.");
|
|
await rename(from, to);
|
|
await guardOwnedMutation(root, id, to);
|
|
}
|
|
async function removeOwnedOutput(root: string, plan: PackagePlan, path: string, expectedTree: string): Promise<void> {
|
|
await guardOwnedMutation(root, plan.id, path);
|
|
if ((await inspectOwnedOutput(root, plan, path)).treeHash !== expectedTree)
|
|
throw new Error("Owned output changed before removal.");
|
|
await guardOwnedMutation(root, plan.id, path);
|
|
await rm(path, { recursive: true });
|
|
}
|
|
async function removeEmptyStage(root: string, plan: PackagePlan, stage: string, lease: string): Promise<void> {
|
|
await guardOwnedMutation(root, plan.id, stage);
|
|
const names = await readdir(stage);
|
|
if (names.length !== 1 || names[0] !== "lease.json" || (await Bun.file(join(stage, "lease.json")).text()) !== lease)
|
|
throw new Error("Staging contains unqualified files; preservation is required.");
|
|
await guardOwnedMutation(root, plan.id, join(stage, "lease.json"));
|
|
await unlink(join(stage, "lease.json"));
|
|
await guardOwnedMutation(root, plan.id, stage);
|
|
// The sole owned lease has been removed; no foreign or previous outputs remain.
|
|
if ((await readdir(stage)).length) throw new Error("Staging changed before empty removal.");
|
|
await rmdir(stage);
|
|
}
|
|
async function releasePackageLock(root: string, plan: PackagePlan, lockPath: string, nonce: string): Promise<void> {
|
|
await guardOwnedMutation(root, plan.id, lockPath);
|
|
if ((await Bun.file(lockPath).text()) !== nonce) throw new Error("Package lock ownership changed; lock preserved.");
|
|
await unlink(lockPath);
|
|
}
|
|
async function buildPackage(root: string, plan: PackagePlan): Promise<Record<string, unknown>> {
|
|
if (resolve(process.cwd()) !== root) throw new Error("Run package builds from the repository root.");
|
|
const outputDir = resolve(root, plan.outputPath);
|
|
await assertNoSymlinkComponents(root, dirname(outputDir));
|
|
await mkdir(dirname(outputDir), { recursive: true });
|
|
const lockPath = join(dirname(outputDir), `.${plan.id}.lock`);
|
|
await guardOwnedMutation(root, plan.id, lockPath);
|
|
const lock = await open(lockPath, "wx", 0o600);
|
|
const nonce = crypto.randomUUID();
|
|
await lock.writeFile(nonce);
|
|
let stage: string | undefined;
|
|
let lease: string | undefined;
|
|
let outcome: Record<string, unknown> | undefined;
|
|
let failure: Error | undefined;
|
|
try {
|
|
outcome = await (async () => {
|
|
const identity = await captureIdentity(root, plan);
|
|
const exists = !(await missing(outputDir));
|
|
const old = exists ? await inspectOwnedOutput(root, plan, outputDir) : undefined;
|
|
if (old?.treeHash !== plan.existing?.treeHash) throw new Error("Output changed after planning.");
|
|
if (old?.receipt.schemaVersion === 2 && old.receipt.identity?.key === identity.key) {
|
|
if (
|
|
(await captureIdentity(root, plan)).key !== identity.key ||
|
|
(await inspectOwnedOutput(root, plan, outputDir)).treeHash !== old.treeHash
|
|
)
|
|
throw new Error("Reuse inputs or outputs changed during verification.");
|
|
return {
|
|
id: plan.id,
|
|
name: plan.name,
|
|
status: "reused",
|
|
compiledThisRun: false,
|
|
receipt: `${plan.outputPath}/build-receipt.json`,
|
|
attestedAt: old.receipt.createdAt,
|
|
outputs: old.receipt.outputs,
|
|
qualification: old.receipt.qualification,
|
|
};
|
|
}
|
|
stage = join(dirname(outputDir), `.stage-${plan.id}-${crypto.randomUUID()}`);
|
|
await guardOwnedMutation(root, plan.id, stage);
|
|
await mkdir(stage);
|
|
lease = JSON.stringify({ schemaVersion: 1, id: plan.id, identityKey: identity.key });
|
|
const leasePath = join(stage, "lease.json");
|
|
await guardOwnedMutation(root, plan.id, leasePath);
|
|
const leaseHandle = await open(leasePath, "wx", 0o600);
|
|
try {
|
|
await leaseHandle.writeFile(lease);
|
|
} finally {
|
|
await leaseHandle.close();
|
|
}
|
|
const worker = await runProcess(
|
|
[process.execPath, import.meta.path, "_compile-package", plan.id, stage, identity.key],
|
|
root,
|
|
{ timeoutMs: 30000, maxOutputBytes: 2 * 1024 * 1024 },
|
|
);
|
|
if (worker.code !== 0)
|
|
throw new Error("Bounded package compiler failed; unqualified staging is preserved for review.");
|
|
const payload = join(stage, "payload");
|
|
const candidate = await inspectOwnedOutput(root, plan, payload);
|
|
if (candidate.receipt.identity?.key !== identity.key || (await captureIdentity(root, plan)).key !== identity.key)
|
|
throw new Error("Staged output does not match stable compiler inputs.");
|
|
if (old) {
|
|
if ((await inspectOwnedOutput(root, plan, outputDir)).treeHash !== old.treeHash)
|
|
throw new Error("Previous output changed before replacement.");
|
|
await moveOwned(root, plan.id, outputDir, join(stage, "prior"));
|
|
} else if (!(await missing(outputDir))) throw new Error("Foreign output appeared before installation.");
|
|
await moveOwned(root, plan.id, payload, outputDir);
|
|
const installed = await inspectOwnedOutput(root, plan, outputDir);
|
|
if (
|
|
installed.treeHash !== candidate.treeHash ||
|
|
installed.receipt.identity?.key !== identity.key ||
|
|
(await captureIdentity(root, plan)).key !== identity.key
|
|
)
|
|
throw new Error("Output switch failed verification; prior transient output is preserved.");
|
|
if (old) await removeOwnedOutput(root, plan, join(stage, "prior"), old.treeHash);
|
|
await removeEmptyStage(root, plan, stage, lease);
|
|
stage = undefined;
|
|
return {
|
|
id: plan.id,
|
|
name: plan.name,
|
|
status: "built",
|
|
compiledThisRun: true,
|
|
receipt: `${plan.outputPath}/build-receipt.json`,
|
|
outputs: installed.receipt.outputs,
|
|
qualification: installed.receipt.qualification,
|
|
};
|
|
})();
|
|
} catch (error) {
|
|
const preservedStaging = stage ? relativePath(root, stage) : undefined;
|
|
failure = new Error(
|
|
`${error instanceof Error ? error.message : String(error)}${preservedStaging ? ` Preserved transient staging: ${preservedStaging}` : ""}`,
|
|
);
|
|
} finally {
|
|
try {
|
|
await lock.close();
|
|
await releasePackageLock(root, plan, lockPath, nonce);
|
|
} catch (error) {
|
|
const cleanup = error instanceof Error ? error.message : "Package lock cleanup failed.";
|
|
failure = new Error(failure ? `${failure.message} ${cleanup}` : cleanup);
|
|
}
|
|
}
|
|
if (failure) throw failure;
|
|
if (!outcome) throw new Error("Package build produced no result.");
|
|
return outcome;
|
|
}
|
|
|
|
async function compileWorker(root: string, args: string[], rootPackage: Record<string, unknown>): Promise<number> {
|
|
if (args.length !== 3 || !Object.hasOwn(packageSpecs, args[0]!) || !sha(args[2]))
|
|
throw new Error("Invalid internal compiler selection.");
|
|
const id = args[0] as PackageId;
|
|
const stage = resolve(args[1]!);
|
|
const identityKey = args[2]!;
|
|
await guardOwnedMutation(root, id, stage);
|
|
if (
|
|
!relativePath(resolve(root, "build/packages"), stage).startsWith(`.stage-${id}-`) ||
|
|
!(await lstat(stage)).isDirectory()
|
|
)
|
|
throw new Error("Worker requires its exclusive stage directory.");
|
|
await guardOwnedMutation(root, id, join(stage, "lease.json"));
|
|
const leaseMetadata = await lstat(join(stage, "lease.json"));
|
|
if (!leaseMetadata.isFile() || leaseMetadata.size > 4096)
|
|
throw new Error("Worker lease is not a bounded regular file.");
|
|
const lease = await Bun.file(join(stage, "lease.json")).json();
|
|
exact(lease, ["schemaVersion", "id", "identityKey"]);
|
|
if (
|
|
lease.schemaVersion !== 1 ||
|
|
lease.id !== id ||
|
|
lease.identityKey !== identityKey ||
|
|
canonical((await readdir(stage)).sort()) !== canonical(["lease.json"])
|
|
)
|
|
throw new Error("Worker stage lease is invalid or contains foreign files.");
|
|
const plan = await packagePlan(root, rootPackage, id);
|
|
if (plan.status !== "ready") throw new Error("Worker package preflight failed.");
|
|
const result = await compilePackage(root, plan, join(stage, "payload"), identityKey);
|
|
return result.status === "built" ? 0 : 1;
|
|
}
|
|
|
|
function parseArgs(args: string[]): { action: "plan" | "build"; packages: PackageId[] } {
|
|
const action = args.shift();
|
|
if (action !== "plan" && action !== "build") throw new Error(usage);
|
|
const selections: string[] = [];
|
|
while (args.length) {
|
|
const flag = args.shift();
|
|
if (flag !== "--package") throw new Error(`Unexpected argument: ${flag ?? ""}\n${usage}`);
|
|
const value = args.shift();
|
|
if (!value || value.startsWith("--")) throw new Error(`--package requires an explicit selection.\n${usage}`);
|
|
selections.push(
|
|
...value
|
|
.split(",")
|
|
.map(item => item.trim())
|
|
.filter(Boolean),
|
|
);
|
|
}
|
|
if (selections.length === 0) throw new Error(`Select at least one package explicitly.\n${usage}`);
|
|
if (new Set(selections).size !== selections.length) throw new Error("Duplicate package selection.");
|
|
const unknown = selections.filter(value => !Object.hasOwn(packageSpecs, value));
|
|
if (unknown.length) throw new Error(`Unknown package selection: ${unknown.join(", ")}\n${usage}`);
|
|
return { action, packages: selections as PackageId[] };
|
|
}
|
|
|
|
async function main(): Promise<number> {
|
|
const root = await realpath(repositoryRoot);
|
|
const rootPackage = await Bun.file(join(root, "package.json")).json();
|
|
if (!record(rootPackage) || rootPackage.version !== "1.4.4")
|
|
throw new Error("Root Bun package version must be 1.4.4.");
|
|
if (Bun.argv[2] === "_compile-package") return compileWorker(root, Bun.argv.slice(3), rootPackage);
|
|
const { action, packages } = parseArgs(Bun.argv.slice(2));
|
|
const plans = await Promise.all(packages.map(id => packagePlan(root, rootPackage, id)));
|
|
if (action === "plan") {
|
|
console.log(
|
|
JSON.stringify(
|
|
{ schemaVersion: 1, action, root, complete: plans.every(plan => plan.status === "ready"), packages: plans },
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
return 0;
|
|
}
|
|
const blocked = plans.filter(plan => plan.status !== "ready");
|
|
if (blocked.length) {
|
|
console.log(JSON.stringify({ schemaVersion: 1, action, root, complete: false, packages: plans }, null, 2));
|
|
return 1;
|
|
}
|
|
const config = await loadConfig(root);
|
|
assertRuntimeFacts(config.runtime, currentRuntimeFacts());
|
|
const results: Array<Record<string, unknown>> = [];
|
|
for (const plan of plans) results.push(await buildPackage(root, plan));
|
|
const complete = results.every(result => result.status === "built" || result.status === "reused");
|
|
console.log(JSON.stringify({ schemaVersion: 1, action, root, complete, packages: results }, null, 2));
|
|
return complete ? 0 : 1;
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
main()
|
|
.then(code => (process.exitCode = code))
|
|
.catch(error => {
|
|
console.error(
|
|
JSON.stringify({
|
|
code: "PACKAGE_BUILD_OWNER_FAILED",
|
|
message: error instanceof Error ? error.message : String(error),
|
|
}),
|
|
);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|