639 lines
19 KiB
JavaScript
639 lines
19 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
import crypto from "node:crypto";
|
||
import fs from "node:fs";
|
||
import os from "node:os";
|
||
import path from "node:path";
|
||
import process from "node:process";
|
||
import { execFileSync } from "node:child_process";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const DEFAULT_GUARD =
|
||
"/Volumes/JZAO/HoloLake/persona-runtime/continuity-guard/scripts/continuity-lane.mjs";
|
||
const DEFAULT_STORE =
|
||
"/Volumes/JZAO/HoloLake/persona-runtime/continuity-memory/collaboration";
|
||
const CODE_CHANNEL_HOST = "guanghulab.com";
|
||
const CODE_CHANNEL_PREFIX = "/code/bingshuo/";
|
||
const CODE_CHANNEL_REPOSITORIES = new Set([
|
||
"guanghu-ice-heart",
|
||
"hololake-system-architecture",
|
||
"grok-build-upstream-mirror",
|
||
]);
|
||
const SAFE_CACHE_RELATIVE = [
|
||
/^target$/u,
|
||
/^coverage$/u,
|
||
/^\.nyc_output$/u,
|
||
/^\.pytest_cache$/u,
|
||
/^\.mypy_cache$/u,
|
||
/^\.ruff_cache$/u,
|
||
/^\.cache$/u,
|
||
/^\.next\/cache$/u,
|
||
/^node_modules\/\.cache$/u,
|
||
/(?:^|\/)__pycache__$/u,
|
||
];
|
||
const DEFAULT_CACHE_CANDIDATES = [
|
||
"target",
|
||
"coverage",
|
||
".nyc_output",
|
||
".pytest_cache",
|
||
".mypy_cache",
|
||
".ruff_cache",
|
||
".cache",
|
||
".next/cache",
|
||
"node_modules/.cache",
|
||
];
|
||
|
||
export function parseArgs(argv) {
|
||
const args = { receipts: [], cleanupPaths: [] };
|
||
for (let index = 0; index < argv.length; index += 1) {
|
||
const key = argv[index];
|
||
if (key === "--self-test") args.selfTest = true;
|
||
else if (key === "--receipt") args.receipts.push(argv[++index]);
|
||
else if (key === "--cleanup-path") args.cleanupPaths.push(argv[++index]);
|
||
else if (key.startsWith("--")) {
|
||
const name = key.slice(2).replaceAll(/-([a-z])/gu, (_, letter) =>
|
||
letter.toUpperCase(),
|
||
);
|
||
args[name] = argv[++index];
|
||
} else {
|
||
throw new Error(`UNKNOWN_ARGUMENT:${key}`);
|
||
}
|
||
}
|
||
return args;
|
||
}
|
||
|
||
export function normalizeCodeChannelRepository(value) {
|
||
let url;
|
||
try {
|
||
url = new URL(String(value || "").trim().replace(/^repo:\/\//u, "https://"));
|
||
} catch {
|
||
throw new Error("INVALID_CODE_CHANNEL_REPOSITORY");
|
||
}
|
||
if (url.username || url.password) {
|
||
throw new Error("CODE_CHANNEL_CREDENTIALS_FORBIDDEN");
|
||
}
|
||
const pathname = url.pathname.replace(/\.git$/u, "").replace(/\/+$/u, "");
|
||
const slug = pathname.startsWith(CODE_CHANNEL_PREFIX)
|
||
? pathname.slice(CODE_CHANNEL_PREFIX.length)
|
||
: "";
|
||
if (
|
||
url.protocol !== "https:" ||
|
||
url.hostname !== CODE_CHANNEL_HOST ||
|
||
!CODE_CHANNEL_REPOSITORIES.has(slug)
|
||
) {
|
||
throw new Error("NON_CANONICAL_CODE_CHANNEL_REPOSITORY");
|
||
}
|
||
return {
|
||
identity: `repo://${CODE_CHANNEL_HOST}${CODE_CHANNEL_PREFIX}${slug}`,
|
||
cloneUrl: `https://${CODE_CHANNEL_HOST}${CODE_CHANNEL_PREFIX}${slug}.git`,
|
||
slug,
|
||
};
|
||
}
|
||
|
||
export function normalizeBranch(value) {
|
||
const branch = String(value || "").trim();
|
||
if (
|
||
!branch ||
|
||
!/^(?!\/)(?!.*(?:\.\.|\/\/|@\{|[~^:?*\[\\]))[A-Za-z0-9._/-]+$/u.test(
|
||
branch,
|
||
) ||
|
||
branch.endsWith("/") ||
|
||
branch.endsWith(".")
|
||
) {
|
||
throw new Error("INVALID_TARGET_BRANCH");
|
||
}
|
||
return branch;
|
||
}
|
||
|
||
export function normalizeReceipt(value) {
|
||
const relative = String(value || "").replaceAll("\\", "/").replace(/^\.\/+/u, "");
|
||
if (
|
||
!relative.startsWith("deployment/receipts/") ||
|
||
relative.includes("/../") ||
|
||
relative.endsWith("/") ||
|
||
!/^[A-Za-z0-9._/-]+$/u.test(relative)
|
||
) {
|
||
throw new Error("INVALID_DEPLOYMENT_RECEIPT_PATH");
|
||
}
|
||
return relative;
|
||
}
|
||
|
||
export function safeCacheRelative(worktree, candidate) {
|
||
const root = fs.realpathSync(worktree);
|
||
const absolute = path.resolve(root, candidate);
|
||
const relative = path.relative(root, absolute).replaceAll(path.sep, "/");
|
||
if (
|
||
!relative ||
|
||
relative.startsWith("../") ||
|
||
path.isAbsolute(relative) ||
|
||
!SAFE_CACHE_RELATIVE.some((pattern) => pattern.test(relative))
|
||
) {
|
||
throw new Error(`CACHE_PATH_NOT_ALLOWLISTED:${candidate}`);
|
||
}
|
||
return { absolute, relative };
|
||
}
|
||
|
||
export function isJsonRecordName(name) {
|
||
return String(name).endsWith(".json") && !String(name).startsWith("._");
|
||
}
|
||
|
||
function run(file, args, options = {}) {
|
||
return execFileSync(file, args, {
|
||
cwd: options.cwd,
|
||
encoding: "utf8",
|
||
stdio: options.stdio || ["ignore", "pipe", "pipe"],
|
||
maxBuffer: 16 * 1024 * 1024,
|
||
}).trim();
|
||
}
|
||
|
||
function git(worktree, args) {
|
||
return run("/usr/bin/git", ["-C", worktree, ...args]);
|
||
}
|
||
|
||
function guard(guardScript, args) {
|
||
const output = run("/usr/bin/node", [guardScript, ...args]);
|
||
return output ? JSON.parse(output) : null;
|
||
}
|
||
|
||
function readJson(filePath) {
|
||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||
}
|
||
|
||
function sha256(value) {
|
||
return crypto.createHash("sha256").update(value).digest("hex");
|
||
}
|
||
|
||
function listOwnedLeases(storeRoot, developmentId) {
|
||
const directory = path.join(storeRoot, "leases");
|
||
if (!fs.existsSync(directory)) return [];
|
||
return fs
|
||
.readdirSync(directory)
|
||
.filter(isJsonRecordName)
|
||
.map((name) => readJson(path.join(directory, name)))
|
||
.filter((lease) => lease.development_id === developmentId);
|
||
}
|
||
|
||
function ensureLane(storeRoot, developmentId, worktree) {
|
||
const lanePath = path.join(storeRoot, "lanes", `${developmentId}.json`);
|
||
const lane = readJson(lanePath);
|
||
if (lane.status !== "active" && lane.status !== "waiting") {
|
||
throw new Error(`LANE_NOT_RUNNABLE:${lane.status}`);
|
||
}
|
||
const owned = listOwnedLeases(storeRoot, developmentId);
|
||
const resolvedWorktree = fs.realpathSync(worktree);
|
||
if (
|
||
!owned.some((lease) => {
|
||
if (!path.isAbsolute(lease.resource)) return false;
|
||
try {
|
||
const resource = fs.realpathSync(lease.resource);
|
||
return (
|
||
resolvedWorktree === resource ||
|
||
resolvedWorktree.startsWith(`${resource}${path.sep}`) ||
|
||
resource.startsWith(`${resolvedWorktree}${path.sep}`)
|
||
);
|
||
} catch {
|
||
return false;
|
||
}
|
||
})
|
||
) {
|
||
throw new Error("WORKTREE_WRITE_LEASE_REQUIRED");
|
||
}
|
||
return lane;
|
||
}
|
||
|
||
function verifyWorktree(worktree, receipts) {
|
||
const root = fs.realpathSync(git(worktree, ["rev-parse", "--show-toplevel"]));
|
||
if (root !== fs.realpathSync(worktree)) {
|
||
throw new Error("WORKTREE_MUST_BE_GIT_ROOT");
|
||
}
|
||
if (git(root, ["status", "--porcelain=v1"])) {
|
||
throw new Error("WORKTREE_NOT_CLEAN");
|
||
}
|
||
const head = git(root, ["rev-parse", "HEAD"]);
|
||
if (!/^[0-9a-f]{40}$/u.test(head)) throw new Error("INVALID_LOCAL_HEAD");
|
||
const branch = normalizeBranch(git(root, ["branch", "--show-current"]));
|
||
const receiptEvidence = receipts.map((receipt) => {
|
||
const relative = normalizeReceipt(receipt);
|
||
const content = git(root, ["show", `HEAD:${relative}`]);
|
||
if (!content) throw new Error(`RECEIPT_NOT_COMMITTED:${relative}`);
|
||
return {
|
||
path: relative,
|
||
sha256: sha256(Buffer.from(content)),
|
||
bytes: Buffer.byteLength(content),
|
||
};
|
||
});
|
||
return { root, head, branch, receiptEvidence };
|
||
}
|
||
|
||
function startPublish({
|
||
guardScript,
|
||
developmentId,
|
||
repository,
|
||
branch,
|
||
}) {
|
||
const requested = guard(guardScript, [
|
||
"publish-request",
|
||
"--development-id",
|
||
developmentId,
|
||
"--repository",
|
||
repository.identity,
|
||
"--target",
|
||
branch,
|
||
"--priority",
|
||
"normal",
|
||
]);
|
||
guard(guardScript, ["publish-dispatch"]);
|
||
guard(guardScript, [
|
||
"publish-start",
|
||
"--development-id",
|
||
developmentId,
|
||
"--request-id",
|
||
requested.request_id,
|
||
]);
|
||
return requested.request_id;
|
||
}
|
||
|
||
function finishPublish(guardScript, developmentId, requestId, outcome, receipt) {
|
||
return guard(guardScript, [
|
||
"publish-finish",
|
||
"--development-id",
|
||
developmentId,
|
||
"--request-id",
|
||
requestId,
|
||
"--outcome",
|
||
outcome,
|
||
"--receipt",
|
||
receipt,
|
||
]);
|
||
}
|
||
|
||
function pushAndReadBack(worktree, repository, branch, head, receipts) {
|
||
run("/usr/bin/git", [
|
||
"-C",
|
||
worktree,
|
||
"fetch",
|
||
"--quiet",
|
||
"origin",
|
||
`refs/heads/${branch}`,
|
||
]);
|
||
const remoteBefore = git(worktree, [
|
||
"rev-parse",
|
||
`refs/remotes/origin/${branch}`,
|
||
]);
|
||
try {
|
||
git(worktree, ["merge-base", "--is-ancestor", remoteBefore, head]);
|
||
} catch {
|
||
throw new Error(`NON_FAST_FORWARD_REFUSED:${remoteBefore}`);
|
||
}
|
||
run("/usr/bin/git", [
|
||
"-C",
|
||
worktree,
|
||
"push",
|
||
"origin",
|
||
`HEAD:refs/heads/${branch}`,
|
||
]);
|
||
const remoteLine = run("/usr/bin/git", [
|
||
"ls-remote",
|
||
repository.cloneUrl,
|
||
`refs/heads/${branch}`,
|
||
]);
|
||
const remoteHead = remoteLine.split(/\s+/u)[0] || "";
|
||
if (remoteHead !== head) {
|
||
throw new Error(`REMOTE_HEAD_MISMATCH:${remoteHead || "missing"}`);
|
||
}
|
||
|
||
const readbackRoot = fs.mkdtempSync(
|
||
path.join(os.tmpdir(), "hololake-finalize-readback-"),
|
||
);
|
||
try {
|
||
const clone = path.join(readbackRoot, "repo");
|
||
run("/usr/bin/git", [
|
||
"clone",
|
||
"--quiet",
|
||
"--depth",
|
||
"1",
|
||
"--branch",
|
||
branch,
|
||
repository.cloneUrl,
|
||
clone,
|
||
]);
|
||
const cloneHead = git(clone, ["rev-parse", "HEAD"]);
|
||
if (cloneHead !== head) throw new Error(`FRESH_CLONE_HEAD_MISMATCH:${cloneHead}`);
|
||
git(clone, ["fsck", "--full"]);
|
||
for (const receipt of receipts) {
|
||
const remoteContent = git(clone, ["show", `HEAD:${receipt.path}`]);
|
||
if (sha256(Buffer.from(remoteContent)) !== receipt.sha256) {
|
||
throw new Error(`REMOTE_RECEIPT_MISMATCH:${receipt.path}`);
|
||
}
|
||
}
|
||
} finally {
|
||
fs.rmSync(readbackRoot, { recursive: true, force: true });
|
||
}
|
||
return { remoteBefore, remoteHead };
|
||
}
|
||
|
||
function activeProcessFor(absolute) {
|
||
const output = run("/bin/ps", ["-axo", "pid=,command="]);
|
||
return output
|
||
.split("\n")
|
||
.map((line) => line.trim())
|
||
.filter(Boolean)
|
||
.some((line) => {
|
||
const match = line.match(/^(\d+)\s+(.*)$/u);
|
||
if (!match) return false;
|
||
const pid = Number(match[1]);
|
||
return pid !== process.pid && pid !== process.ppid && match[2].includes(absolute);
|
||
});
|
||
}
|
||
|
||
function directoryBytes(root) {
|
||
let total = 0;
|
||
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
||
const candidate = path.join(root, entry.name);
|
||
const stat = fs.lstatSync(candidate);
|
||
if (stat.isSymbolicLink()) continue;
|
||
if (stat.isDirectory()) total += directoryBytes(candidate);
|
||
else if (stat.isFile()) total += stat.size;
|
||
}
|
||
return total;
|
||
}
|
||
|
||
function collectPythonCaches(root, limit = 256) {
|
||
const found = [];
|
||
const walk = (directory, depth) => {
|
||
if (depth > 6 || found.length >= limit) return;
|
||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||
if (found.length >= limit) return;
|
||
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
||
const candidate = path.join(directory, entry.name);
|
||
if (entry.name === "__pycache__") {
|
||
found.push(path.relative(root, candidate));
|
||
continue;
|
||
}
|
||
if ([".git", "node_modules", "target", "build", "dist"].includes(entry.name)) {
|
||
continue;
|
||
}
|
||
walk(candidate, depth + 1);
|
||
}
|
||
};
|
||
walk(root, 0);
|
||
return found;
|
||
}
|
||
|
||
function cleanupCaches(worktree, explicitPaths) {
|
||
const candidates = [
|
||
...DEFAULT_CACHE_CANDIDATES,
|
||
...collectPythonCaches(worktree),
|
||
...explicitPaths,
|
||
];
|
||
const unique = new Map();
|
||
for (const candidate of candidates) {
|
||
const safe = safeCacheRelative(worktree, candidate);
|
||
unique.set(safe.absolute, safe);
|
||
}
|
||
const results = [];
|
||
for (const item of unique.values()) {
|
||
if (!fs.existsSync(item.absolute)) {
|
||
results.push({ path: item.relative, result: "SKIPPED_NOT_PRESENT", bytes: 0 });
|
||
continue;
|
||
}
|
||
const stat = fs.lstatSync(item.absolute);
|
||
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
||
throw new Error(`CACHE_TARGET_NOT_REAL_DIRECTORY:${item.relative}`);
|
||
}
|
||
if (git(worktree, ["ls-files", "--", item.relative])) {
|
||
throw new Error(`TRACKED_CACHE_REFUSED:${item.relative}`);
|
||
}
|
||
try {
|
||
git(worktree, ["check-ignore", "--no-index", "-q", "--", item.relative]);
|
||
} catch {
|
||
throw new Error(`UNIGNORED_CACHE_REFUSED:${item.relative}`);
|
||
}
|
||
if (activeProcessFor(item.absolute)) {
|
||
results.push({ path: item.relative, result: "SKIPPED_ACTIVE", bytes: 0 });
|
||
continue;
|
||
}
|
||
const bytes = directoryBytes(item.absolute);
|
||
fs.rmSync(item.absolute, { recursive: true, force: true });
|
||
results.push({ path: item.relative, result: "REMOVED", bytes });
|
||
}
|
||
return results;
|
||
}
|
||
|
||
function writeFinalizationReceipt(storeRoot, developmentId, payload) {
|
||
const directory = path.join(storeRoot, "finalization-receipts");
|
||
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||
const filePath = path.join(
|
||
directory,
|
||
`${developmentId}-${new Date().toISOString().replaceAll(/[:.]/gu, "-")}.json`,
|
||
);
|
||
const temporary = `${filePath}.tmp-${process.pid}`;
|
||
fs.writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`, {
|
||
mode: 0o600,
|
||
});
|
||
fs.renameSync(temporary, filePath);
|
||
return filePath;
|
||
}
|
||
|
||
function releaseAndComplete({
|
||
guardScript,
|
||
storeRoot,
|
||
developmentId,
|
||
summary,
|
||
}) {
|
||
for (const lease of listOwnedLeases(storeRoot, developmentId)) {
|
||
guard(guardScript, [
|
||
"release",
|
||
"--development-id",
|
||
developmentId,
|
||
"--resource",
|
||
lease.resource,
|
||
]);
|
||
}
|
||
const lane = guard(guardScript, [
|
||
"complete",
|
||
"--development-id",
|
||
developmentId,
|
||
"--summary",
|
||
summary,
|
||
]);
|
||
const actions = guard(guardScript, ["completion-actions"]);
|
||
const current = actions.find(
|
||
(item) => item.development_id === developmentId,
|
||
);
|
||
if (!current) throw new Error("COMPLETION_ACTION_MISSING");
|
||
if ((current.delete_automation_ids || []).length === 0) {
|
||
guard(guardScript, [
|
||
"mark-completion-stopped",
|
||
"--development-id",
|
||
developmentId,
|
||
]);
|
||
return { lane_status: lane.status, notification_state: "stopped" };
|
||
}
|
||
return {
|
||
lane_status: lane.status,
|
||
notification_state: "stop_requested",
|
||
delete_automation_ids: current.delete_automation_ids,
|
||
};
|
||
}
|
||
|
||
export async function finalize(options) {
|
||
const developmentId = String(options.developmentId || "");
|
||
if (!/^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+$/u.test(developmentId)) {
|
||
throw new Error("INVALID_DEVELOPMENT_ID");
|
||
}
|
||
const guardScript = options.guardScript || DEFAULT_GUARD;
|
||
const storeRoot = options.storeRoot || DEFAULT_STORE;
|
||
const worktree = fs.realpathSync(options.worktree || "");
|
||
if (!options.receipts?.length) throw new Error("AT_LEAST_ONE_RECEIPT_REQUIRED");
|
||
ensureLane(storeRoot, developmentId, worktree);
|
||
const verified = verifyWorktree(worktree, options.receipts);
|
||
const repository = normalizeCodeChannelRepository(
|
||
options.repository || git(worktree, ["config", "--get", "remote.origin.url"]),
|
||
);
|
||
const branch = normalizeBranch(options.target || verified.branch);
|
||
if (branch !== verified.branch) throw new Error("CHECKED_OUT_BRANCH_MISMATCH");
|
||
|
||
let publishRequestId = null;
|
||
let publishStarted = false;
|
||
try {
|
||
publishRequestId = startPublish({
|
||
guardScript,
|
||
developmentId,
|
||
repository,
|
||
branch,
|
||
});
|
||
publishStarted = true;
|
||
const remote = pushAndReadBack(
|
||
worktree,
|
||
repository,
|
||
branch,
|
||
verified.head,
|
||
verified.receiptEvidence,
|
||
);
|
||
const publishReceipt =
|
||
`PASS_100 remote=${repository.identity}#${branch} ` +
|
||
`head=${remote.remoteHead} receipts=${verified.receiptEvidence
|
||
.map((item) => `${item.path}:${item.sha256}`)
|
||
.join(",")}`;
|
||
finishPublish(
|
||
guardScript,
|
||
developmentId,
|
||
publishRequestId,
|
||
"completed",
|
||
publishReceipt,
|
||
);
|
||
publishStarted = false;
|
||
|
||
const cleanup = cleanupCaches(worktree, options.cleanupPaths || []);
|
||
const removedBytes = cleanup
|
||
.filter((item) => item.result === "REMOVED")
|
||
.reduce((total, item) => total + item.bytes, 0);
|
||
const payload = {
|
||
schema: "hololake.development-finalization-receipt/v1",
|
||
development_id: developmentId,
|
||
result: "PASS_100",
|
||
code_channel: {
|
||
repository: repository.identity,
|
||
branch,
|
||
remote_head: remote.remoteHead,
|
||
fresh_clone_readback: 100,
|
||
git_fsck: 100,
|
||
receipts: verified.receiptEvidence,
|
||
},
|
||
cleanup: {
|
||
scope: "CURRENT_WORKTREE_IGNORED_ALLOWLISTED_CACHES_ONLY",
|
||
removed_bytes: removedBytes,
|
||
items: cleanup,
|
||
},
|
||
recorded_at: new Date().toISOString(),
|
||
};
|
||
const finalizationReceipt = writeFinalizationReceipt(
|
||
storeRoot,
|
||
developmentId,
|
||
payload,
|
||
);
|
||
const completion = releaseAndComplete({
|
||
guardScript,
|
||
storeRoot,
|
||
developmentId,
|
||
summary:
|
||
`自动收口PASS_100:第五域代码频道${repository.identity}#${branch}` +
|
||
`远端回读${remote.remoteHead};${verified.receiptEvidence.length}份回执一致;` +
|
||
`本车道安全缓存清理${removedBytes}字节;回执${finalizationReceipt}`,
|
||
});
|
||
return { ...payload, finalization_receipt: finalizationReceipt, completion };
|
||
} catch (error) {
|
||
if (publishStarted && publishRequestId) {
|
||
try {
|
||
finishPublish(
|
||
guardScript,
|
||
developmentId,
|
||
publishRequestId,
|
||
"failed",
|
||
`FAIL_0 ${String(error.message || error).slice(0, 400)}`,
|
||
);
|
||
} catch {
|
||
// The original finalization failure remains authoritative.
|
||
}
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
export function selfTest() {
|
||
const canonical = normalizeCodeChannelRepository(
|
||
"https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git",
|
||
);
|
||
if (
|
||
canonical.identity !==
|
||
"repo://guanghulab.com/code/bingshuo/guanghu-ice-heart"
|
||
) {
|
||
throw new Error("SELF_TEST_REPOSITORY_NORMALIZATION_FAILED");
|
||
}
|
||
for (const invalid of [
|
||
"https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git",
|
||
"https://user:secret@guanghulab.com/code/bingshuo/guanghu-ice-heart.git",
|
||
]) {
|
||
let rejected = false;
|
||
try {
|
||
normalizeCodeChannelRepository(invalid);
|
||
} catch {
|
||
rejected = true;
|
||
}
|
||
if (!rejected) throw new Error("SELF_TEST_INVALID_REPOSITORY_ACCEPTED");
|
||
}
|
||
if (
|
||
normalizeReceipt(
|
||
"deployment/receipts/GLW-CHJH-ROUTER-CONTROLLER-JD-20260805.json",
|
||
) !==
|
||
"deployment/receipts/GLW-CHJH-ROUTER-CONTROLLER-JD-20260805.json"
|
||
) {
|
||
throw new Error("SELF_TEST_RECEIPT_NORMALIZATION_FAILED");
|
||
}
|
||
return { result: "PASS_100" };
|
||
}
|
||
|
||
async function main() {
|
||
const args = parseArgs(process.argv.slice(2));
|
||
const result = args.selfTest
|
||
? selfTest()
|
||
: await finalize({
|
||
developmentId: args.developmentId,
|
||
worktree: args.worktree,
|
||
repository: args.repository,
|
||
target: args.target,
|
||
receipts: args.receipts,
|
||
cleanupPaths: args.cleanupPaths,
|
||
guardScript: args.guardScript,
|
||
storeRoot: args.storeRoot,
|
||
});
|
||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||
}
|
||
|
||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||
main().catch((error) => {
|
||
process.stderr.write(`FINALIZATION_FAIL_0:${String(error.message || error)}\n`);
|
||
process.exitCode = 1;
|
||
});
|
||
}
|