import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; const steps = [ { id: "dev-cleanup-safety", command: npmCommand, args: ["run", "dev-cleanup:test"] }, { id: "native-helper-safety", command: npmCommand, args: ["run", "helper:test"] }, { id: "lint", command: npmCommand, args: ["run", "lint"] }, { id: "tests", command: npmCommand, args: ["test"] }, { id: "ocr-eval", command: npmCommand, args: ["run", "eval"] }, { id: "assessment-self-test", command: npmCommand, args: ["run", "scan:assessment:test"] }, { id: "saved-native-validation", command: npmCommand, args: ["run", "scan:native:validate:saved"] }, { id: "package-offline-check", command: npmCommand, args: ["run", "package:offline-check"] }, { id: "dependency-audit", command: npmCommand, args: ["audit"] }, { id: "production-dependency-audit", command: npmCommand, args: ["audit", "--omit=dev"] }, { id: "git-diff-check", command: "git", args: ["diff", "--check"] }, ]; const startedAt = new Date(); const results = []; for (const step of steps) { const stepStartedAt = Date.now(); console.log(`\n=== Offline acceptance: ${step.id} ===`); const invocation = commandInvocation(step); const result = spawnSync(invocation.command, invocation.args, { cwd: projectRoot, encoding: "utf8", stdio: "inherit", windowsHide: true, }); results.push({ id: step.id, ok: result.status === 0 && !result.error, exitCode: result.status, signal: result.signal, error: result.error?.message ?? null, elapsedMs: Date.now() - stepStartedAt, }); } const outputDir = path.join(projectRoot, "outputs", "offline-acceptance"); fs.mkdirSync(outputDir, { recursive: true }); const reportPath = path.join(outputDir, "offline-acceptance-report.json"); const report = { version: "offline-acceptance-v2", startedAt: startedAt.toISOString(), completedAt: new Date().toISOString(), ok: results.every((result) => result.ok), projectRoot, results, }; fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8"); console.log(`\nOffline acceptance: ${report.ok ? "PASS" : "FAIL"}`); for (const result of results) { console.log(`${result.ok ? "PASS" : "FAIL"} ${result.id} (${result.elapsedMs} ms)`); } console.log(`Report: ${reportPath}`); if (!report.ok) process.exitCode = 1; function commandInvocation(step) { if (process.platform !== "win32" || !step.command.endsWith(".cmd")) return step; const tokens = [step.command, ...step.args]; if (tokens.some((token) => !/^[A-Za-z0-9:._=@/-]+$/.test(token))) { throw new Error(`Unsafe Windows command token in offline acceptance step ${step.id}.`); } return { command: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", tokens.join(" ")], }; }