#!/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 { execFileSync } from "node:child_process"; const root = process.cwd(); const sourceRoot = path.join(root, "freecad-source"); const docsRoot = path.join(root, "docs"); const outputDir = path.join(docsRoot, "p6"); const manifestPath = path.join(outputDir, "baseline.json"); const logPath = path.join(outputDir, "baseline.log"); fs.mkdirSync(outputDir, { recursive: true }); const log = []; function command(file, args, cwd = root) { const rendered = [file, ...args].join(" "); try { const stdout = execFileSync(file, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); log.push(`$ (cd ${cwd} && ${rendered})\n${stdout}`); return { command: rendered, stdout, exitCode: 0 }; } catch (error) { const stdout = String(error.stdout ?? "").trim(); const stderr = String(error.stderr ?? "").trim(); log.push(`$ (cd ${cwd} && ${rendered})\nexit=${error.status ?? 1}\n${stdout}\n${stderr}`.trim()); return { command: rendered, stdout, stderr, exitCode: error.status ?? 1 }; } } function sha256(file) { const hash = crypto.createHash("sha256"); hash.update(fs.readFileSync(file)); return hash.digest("hex"); } function existing(paths) { return paths.find((candidate) => fs.existsSync(candidate)) ?? null; } const revision = command("git", ["rev-parse", "HEAD"], sourceRoot); const status = command("git", ["status", "--porcelain=v1"], sourceRoot); const diffStat = command("git", ["diff", "--stat"], sourceRoot); const node = command(process.execPath, ["--version"]); const npm = command("npm", ["--version"]); const cmake = command("cmake", ["--version"]); const chromeCandidates = [ "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/opt/google/chrome/google-chrome", ]; const chromePath = existing(chromeCandidates); const chromeVersion = chromePath ? command(chromePath, ["--version"]) : { command: "chrome --version", stdout: "", exitCode: 127 }; if (!chromePath) log.push("chrome executable not found; no alternate browser was attempted"); const cacheCandidates = [ path.join(sourceRoot, "build-web", "configure", "CMakeCache.txt"), path.join(sourceRoot, "build-web", "CMakeCache.txt"), ]; const cachePath = existing(cacheCandidates); const cache = cachePath ? fs.readFileSync(cachePath, "utf8") : ""; const cacheKeys = ["FREECAD_WEB:BOOL", "CMAKE_BUILD_TYPE:STRING", "CMAKE_TOOLCHAIN_FILE:FILEPATH", "CMAKE_GENERATOR:INTERNAL"]; const cacheValues = Object.fromEntries(cacheKeys.map((key) => [key, cache.match(new RegExp(`^${key.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}=([^\\r\\n]*)`, "m"))?.[1] ?? null])); const inputRelPaths = [ "docs/web-parity-matrix.json", "docs/workbench-inventory.json", "docs/object-inventory.json", "docs/format-api-inventory.json", "docs/freecad-web-release-gate.json", "docs/current/FREECAD_WEB_MIGRATION_ANALYSIS.md", "docs/current/FREECAD_WEB_EXECUTION_PLAN.md", "docs/current/FREECAD_WEB_PROGRESS.md", ]; const inputs = inputRelPaths.map((relativePath) => { const absolutePath = path.join(root, relativePath); return { path: relativePath, exists: fs.existsSync(absolutePath), size: fs.existsSync(absolutePath) ? fs.statSync(absolutePath).size : null, sha256: fs.existsSync(absolutePath) ? sha256(absolutePath) : null, }; }); const result = chromePath && revision.exitCode === 0 && inputs.every((item) => item.exists) ? "pass" : "blocked"; const report = { schema: "freecad-web-p6-baseline/v1", result, generatedAt: new Date().toISOString(), host: { platform: process.platform, arch: process.arch, osRelease: os.release(), cwd: root, }, source: { root: "FreeCAD/freecad-source", revision: revision.stdout || null, dirty: status.stdout.length > 0, statusLines: status.stdout ? status.stdout.split("\n") : [], diffStat: diffStat.stdout, }, inputs, build: { configureDirectory: cachePath ? path.relative(root, path.dirname(cachePath)) : null, cache: cacheValues, }, tools: { node: node.stdout || null, npm: npm.stdout || null, cmake: cmake.stdout.split("\n")[0] || null, }, chrome: { path: chromePath, version: chromeVersion.stdout || null, executableExitCode: chromeVersion.exitCode, mode: "headless", viewport: { width: 1440, height: 900, deviceScaleFactor: 1 }, network: "intercept-and-require-zero-external-requests", }, policy: { browser: "Google Chrome only", nonChromeBrowsersAttempted: false, dirtyWorktreePreserved: true, }, }; fs.writeFileSync(manifestPath, `${JSON.stringify(report, null, 2)}\n`); fs.writeFileSync(logPath, `${log.join("\n\n")}\n\nresult=${result}\nmanifest=${path.relative(root, manifestPath)}\n`); console.log(JSON.stringify({ schema: report.schema, result, revision: report.source.revision, chrome: report.chrome.version, inputCount: inputs.length })); process.exitCode = result === "pass" ? 0 : 1;