diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ced2ac82..3498c8fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,6 +38,7 @@ jobs: with: name: Codex-macOS-${{ matrix.arch }} path: out/*.dmg + if-no-files-found: error build-windows: if: inputs.platform == 'all' || inputs.platform == 'windows' @@ -63,6 +64,7 @@ jobs: with: name: Codex-Windows-x64 path: out/*.zip + if-no-files-found: error build-linux: if: inputs.platform == 'all' || inputs.platform == 'linux' @@ -81,7 +83,7 @@ jobs: npm ci - run: | sudo apt-get update - sudo apt-get install -y rpm fakeroot dpkg 7zip + sudo apt-get install -y rpm fakeroot dpkg 7zip build-essential python3 libudev-dev - run: node scripts/sync-upstream.js --force --skip-win - run: node scripts/patch-all.js mac-${{ matrix.arch }} - run: npm run build:linux-${{ matrix.arch }} @@ -92,3 +94,4 @@ jobs: out/make/deb/**/*.deb out/make/rpm/**/*.rpm out/make/zip/**/*.zip + if-no-files-found: error diff --git a/package-lock.json b/package-lock.json index 42e98022..d1e1df08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "codex-rebuild", - "version": "26.506.31421", + "version": "26.609.41114", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codex-rebuild", - "version": "26.506.31421", + "version": "26.609.41114", "dependencies": { "@sentry/electron": "^7.5.0", "@sentry/node": "10.29.0", - "better-sqlite3": "^12.4.6", + "better-sqlite3": "^12.10.1", "electron-context-menu": "^4.1.1", "electron-squirrel-startup": "^1.0.1", "encoding": "^0.1.13", @@ -430,7 +430,6 @@ "version": "1.8.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", @@ -1118,7 +1117,6 @@ "version": "6.0.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", @@ -1344,7 +1342,6 @@ "node_modules/@opentelemetry/api": { "version": "1.9.1", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -1362,7 +1359,6 @@ "node_modules/@opentelemetry/context-async-hooks": { "version": "2.7.1", "license": "Apache-2.0", - "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -1373,7 +1369,6 @@ "node_modules/@opentelemetry/core": { "version": "2.7.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -1387,7 +1382,6 @@ "node_modules/@opentelemetry/instrumentation": { "version": "0.208.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.208.0", "import-in-the-middle": "^2.0.0", @@ -1740,7 +1734,6 @@ "node_modules/@opentelemetry/resources": { "version": "2.7.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -1755,7 +1748,6 @@ "node_modules/@opentelemetry/sdk-trace-base": { "version": "2.7.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", @@ -1771,7 +1763,6 @@ "node_modules/@opentelemetry/semantic-conventions": { "version": "1.40.0", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -1892,7 +1883,6 @@ "node_modules/@sentry/electron/node_modules/@opentelemetry/core": { "version": "2.6.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2836,7 +2826,6 @@ "node_modules/acorn": { "version": "8.16.0", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2900,7 +2889,6 @@ "version": "8.20.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3071,7 +3059,9 @@ } }, "node_modules/better-sqlite3": { - "version": "12.9.0", + "version": "12.10.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.1.tgz", + "integrity": "sha512-HfFtzCqnSfwB3+HroF6PSKzyh+7RfNMGPCzHFUZXRlvrPCb4P3cvxKZNN43Sr7IrkofqQZM+gIvffGpA8VvqgA==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -3079,7 +3069,7 @@ "prebuild-install": "^7.1.1" }, "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" } }, "node_modules/bindings": { @@ -3157,7 +3147,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4496,7 +4485,6 @@ "node_modules/encoding": { "version": "0.1.13", "license": "MIT", - "peer": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -8618,7 +8606,6 @@ "version": "5.106.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/package.json b/package.json index 408ebcdb..7ad8f237 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,16 @@ "build:mac": "npm run build:mac-arm64 && npm run build:mac-x64", "build:win-x64": "node scripts/build-from-upstream.js --platform win", "build:win": "npm run build:win-x64", - "build:linux-x64": "node scripts/prepare-src.js --platform linux-x64 && npm run rebuild:native && node scripts/sync-native-modules.js --platform linux-x64 && rm -rf out && electron-forge make --platform=linux --arch=x64", - "build:linux-arm64": "node scripts/prepare-src.js --platform linux-arm64 && npm run rebuild:native && node scripts/sync-native-modules.js --platform linux-arm64 && rm -rf out && electron-forge make --platform=linux --arch=arm64", + "build:linux-x64": "node scripts/prepare-src.js --platform linux-x64 && npm run rebuild:native:x64 && node scripts/sync-native-modules.js --platform linux-x64 && rm -rf out && node scripts/build-from-upstream-linux.js --platform linux-x64", + "build:linux-arm64": "node scripts/prepare-src.js --platform linux-arm64 && npm run rebuild:native:arm64 && node scripts/sync-native-modules.js --platform linux-arm64 && rm -rf out && node scripts/build-from-upstream-linux.js --platform linux-arm64", "build:linux": "npm run build:linux-x64 && npm run build:linux-arm64", "build:all": "npm run build:mac && npm run build:win && npm run build:linux", "sync": "node scripts/sync-upstream.js", "check-update": "node scripts/check-update.js", "bump-version": "node scripts/bump-version.js", - "rebuild:native": "electron-rebuild" + "rebuild:native": "electron-rebuild", + "rebuild:native:x64": "electron-rebuild --arch=x64", + "rebuild:native:arm64": "electron-rebuild --arch=arm64" }, "devDependencies": { "@electron-forge/cli": "^7.10.2", @@ -46,7 +48,7 @@ "dependencies": { "@sentry/electron": "^7.5.0", "@sentry/node": "10.29.0", - "better-sqlite3": "^12.4.6", + "better-sqlite3": "^12.10.1", "electron-context-menu": "^4.1.1", "electron-squirrel-startup": "^1.0.1", "encoding": "^0.1.13", diff --git a/scripts/build-from-upstream-linux.js b/scripts/build-from-upstream-linux.js new file mode 100644 index 00000000..e5620510 --- /dev/null +++ b/scripts/build-from-upstream-linux.js @@ -0,0 +1,286 @@ +#!/usr/bin/env node +/** + * build-from-upstream-linux.js — Build Linux distributables (.deb/.rpm/.zip) + * + * Avoids @electron/packager entirely. Its dependency extract-zip 2.0.1 + * silently exits the Node process during extraction on Node 24 + Ubuntu CI + * (no error, no stack — process just ends with exit code 0 mid-await). + * Two CI runs reproduced this deterministically with full DEBUG. + * + * Instead this script does the same work via: + * - @electron/get — download the Electron Linux template zip + * - system `unzip` — extract reliably (apt: unzip is preinstalled) + * - @electron/asar — pack src/ into app.asar + * - maker-deb/rpm/zip — programmatic make() + * + * Prereq: prepare-src.js --platform linux-{arch} has already populated src/ + * (app code) and src/mac-{arch}/ (Linux codex/rg vendor binaries). + * + * Usage: + * node scripts/build-from-upstream-linux.js --platform linux-x64 + * node scripts/build-from-upstream-linux.js --platform linux-arm64 + */ +const fs = require("fs"); +const path = require("path"); +const { execFileSync } = require("child_process"); + +const PROJECT_ROOT = path.resolve(__dirname, ".."); +const SRC_DIR = path.join(PROJECT_ROOT, "src"); +const OUT_DIR = path.join(PROJECT_ROOT, "out"); +const RESOURCES_DIR = path.join(PROJECT_ROOT, "resources"); + +const ASAR_UNPACK_GLOB = "{**/*.node,**/node-pty/build/Release/spawn-helper,**/node-pty/prebuilds/*/spawn-helper}"; + +const MACOS_ONLY_FILES = new Set([ + "node", "node_repl", + "electron.icns", "Assets.car", + "codexTemplate.png", "codexTemplate@2x.png", + "app.asar", "codex-notification.wav", +]); +const MACOS_ONLY_DIRS = new Set(["native", "app.asar.unpacked"]); + +function readElectronVersion() { + const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf-8")); + const raw = pkg.devDependencies?.electron; + if (!raw) throw new Error("devDependencies.electron not set in package.json"); + // strip leading ^ or ~ or = etc. + return raw.replace(/^[^\d]*/, ""); +} + +function copyLinuxResources(srcDir, destDir) { + if (!fs.existsSync(srcDir)) { + throw new Error(`Source platform dir not found: ${srcDir}`); + } + const skip = new Set(["_asar"]); + for (const f of MACOS_ONLY_FILES) skip.add(f); + for (const d of MACOS_ONLY_DIRS) skip.add(d); + + let copied = 0; + const copyDir = (s, d) => { + fs.mkdirSync(d, { recursive: true }); + for (const e of fs.readdirSync(s, { withFileTypes: true })) { + const sp = path.join(s, e.name), dp = path.join(d, e.name); + if (e.isDirectory()) copyDir(sp, dp); + else if (!e.isSymbolicLink()) { + fs.copyFileSync(sp, dp); + try { fs.chmodSync(dp, 0o755); } catch {} + copied++; + } + } + }; + for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { + if (skip.has(entry.name)) continue; + if (entry.name.endsWith(".lproj")) continue; + const srcPath = path.join(srcDir, entry.name); + const destPath = path.join(destDir, entry.name); + if (entry.isDirectory()) { + copyDir(srcPath, destPath); + } else if (!entry.isSymbolicLink()) { + fs.copyFileSync(srcPath, destPath); + try { fs.chmodSync(destPath, 0o755); } catch {} + copied++; + } + } + return copied; +} + +async function runMaker(kind, factory, packageDir, makeDir, targetArch, packageJSON, results) { + console.log(`\n-- maker:${kind}`); + try { + const maker = factory(); + if (typeof maker.isSupportedOnCurrentPlatform === "function" && !maker.isSupportedOnCurrentPlatform()) { + throw new Error(`maker-${kind} reports unsupported on current platform (missing peer installer pkg?)`); + } + if (typeof maker.ensureExternalBinariesExist === "function") { + maker.ensureExternalBinariesExist(); + } + if (typeof maker.prepareConfig === "function") { + await maker.prepareConfig(targetArch); + } + const artifacts = await maker.make({ + dir: packageDir, + makeDir, + appName: "Codex", + targetPlatform: "linux", + targetArch, + forgeConfig: { packagerConfig: {}, rebuildConfig: {}, makers: [], publishers: [], plugins: [], pluginInterface: {} }, + packageJSON, + }); + console.log(` [ok] ${kind}: ${artifacts.length} artifact(s)`); + for (const a of artifacts) console.log(` ${a}`); + results.push({ kind, ok: true, artifacts }); + } catch (err) { + console.error(` [x] ${kind} failed: ${err.stack || err.message || err}`); + results.push({ kind, ok: false, error: err.message || String(err) }); + } +} + +async function main() { + const args = process.argv.slice(2); + const platIdx = args.indexOf("--platform"); + const platform = platIdx !== -1 ? args[platIdx + 1] : null; + if (!platform || !["linux-x64", "linux-arm64"].includes(platform)) { + console.error("Usage: build-from-upstream-linux.js --platform linux-x64|linux-arm64"); + process.exit(1); + } + const arch = platform.split("-")[1]; + const sourcePlatformDir = path.join(SRC_DIR, `mac-${arch}`); + + if (!fs.existsSync(path.join(SRC_DIR, ".build-mode"))) { + console.error(`[x] src/ not prepared. Run: node scripts/prepare-src.js --platform ${platform}`); + process.exit(1); + } + if (!fs.existsSync(sourcePlatformDir)) { + console.error(`[x] ${sourcePlatformDir} not found.`); + process.exit(1); + } + + const electronVersion = readElectronVersion(); + const packageDir = path.join(OUT_DIR, `Codex-linux-${arch}`); + const resourcesPath = path.join(packageDir, "resources"); + const appAsarPath = path.join(resourcesPath, "app.asar"); + + console.log(`\n== build-linux: ${platform} ==`); + console.log(` electron: v${electronVersion}`); + console.log(` package: ${packageDir}`); + console.log(` source: ${path.relative(PROJECT_ROOT, sourcePlatformDir)}`); + + // ─── 1. Download Electron Linux template zip ─── + console.log(`\n-- downloading electron-v${electronVersion}-linux-${arch}.zip`); + const { downloadArtifact, initializeProxy } = require("@electron/get"); + initializeProxy(); + const zipPath = await downloadArtifact({ + version: electronVersion, + platform: "linux", + arch, + artifactName: "electron", + }); + const zipSize = fs.statSync(zipPath).size; + console.log(` [ok] ${zipPath} (${(zipSize / 1024 / 1024).toFixed(1)} MB)`); + + // ─── 2. Extract via system unzip (bypasses extract-zip silent-exit) ─── + console.log(`\n-- extracting via system unzip -> ${packageDir}`); + if (fs.existsSync(packageDir)) fs.rmSync(packageDir, { recursive: true, force: true }); + fs.mkdirSync(packageDir, { recursive: true }); + execFileSync("unzip", ["-qq", "-o", zipPath, "-d", packageDir], { stdio: ["ignore", "inherit", "inherit"] }); + const extractedCount = fs.readdirSync(packageDir).length; + console.log(` [ok] ${extractedCount} top-level entries extracted`); + if (extractedCount === 0) throw new Error("unzip produced no files"); + + // ─── 3. Rename `electron` binary -> `Codex` ─── + const electronBin = path.join(packageDir, "electron"); + const codexBin = path.join(packageDir, "Codex"); + if (!fs.existsSync(electronBin)) throw new Error(`Electron binary not found at ${electronBin}`); + fs.renameSync(electronBin, codexBin); + fs.chmodSync(codexBin, 0o755); + console.log(` [ok] electron -> Codex`); + + // ─── 4. Remove default_app + chrome-sandbox SUID (forge does same) ─── + for (const name of ["default_app.asar", "default_app"]) { + const p = path.join(resourcesPath, name); + if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true }); + } + + // ─── 5. Pack src/ -> app.asar (excludes src/mac-* via root filter) ─── + console.log(`\n-- packing app.asar from src/`); + const asar = require("@electron/asar"); + await asar.createPackageWithOptions(SRC_DIR, appAsarPath, { + unpack: ASAR_UNPACK_GLOB, + dot: true, + // Exclude src/mac-x64/, src/mac-arm64/, src/win/ subdirs — these are + // upstream platform trees retained alongside src/ for the afterCopy + // step, not part of the runtime app. + globOptions: { ignore: ["mac-x64/**", "mac-arm64/**", "win/**"] }, + }); + const asarSize = fs.statSync(appAsarPath).size; + console.log(` [ok] app.asar: ${(asarSize / 1024 / 1024).toFixed(1)} MB`); + + // ─── 6. Copy Linux-specific resources from src/mac-{arch}/ ─── + console.log(`\n-- copying Linux resources from ${path.relative(PROJECT_ROOT, sourcePlatformDir)}`); + const copied = copyLinuxResources(sourcePlatformDir, resourcesPath); + console.log(` [ok] ${copied} files copied to ${path.relative(PROJECT_ROOT, resourcesPath)}`); + + // ─── 6b. Strip cross-arch helper binaries ─── + // Upstream Codex bundles BOTH sky_linux_x64 and sky_linux_arm64. rpmbuild's + // brp-strip pass invokes the host's /usr/bin/strip on every ELF in the + // payload — on an x86_64 runner that strip can't parse arm64 ELF and the + // whole rpm fails. The runtime selector only ever calls the matching arch + // binary, so the wrong-arch copy is dead weight; remove it before any + // maker runs (also slims deb/zip a bit). + const otherArch = arch === "x64" ? "arm64" : "x64"; + const crossArchBins = [ + path.join(resourcesPath, "cua_node", "lib", "node_modules", "@oai", "sky", "bin", "linux", `sky_linux_${otherArch}`), + ]; + for (const f of crossArchBins) { + if (fs.existsSync(f)) { + fs.rmSync(f); + console.log(` [strip-safe] removed cross-arch ${path.relative(packageDir, f)}`); + } + } + + // ─── 7. Run makers ─── + const packageJSON = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf-8")); + const makeDir = path.join(OUT_DIR, "make"); + fs.mkdirSync(makeDir, { recursive: true }); + + console.log(`\n== makers: deb, rpm, zip (arch=${arch}) ==`); + const results = []; + + await runMaker("deb", () => { + const { default: MakerDeb } = require("@electron-forge/maker-deb"); + return new MakerDeb({ + options: { + name: "codex", + productName: "Codex", + genericName: "AI Coding Assistant", + categories: ["Development", "Utility"], + bin: "Codex", + maintainer: "Cometix Space", + homepage: "https://github.com/Haleclipse/CodexDesktop-Rebuild", + icon: path.join(RESOURCES_DIR, "electron.png"), + }, + }); + }, packageDir, makeDir, arch, packageJSON, results); + + await runMaker("rpm", () => { + const { default: MakerRpm } = require("@electron-forge/maker-rpm"); + return new MakerRpm({ + options: { + name: "codex", + productName: "Codex", + genericName: "AI Coding Assistant", + categories: ["Development", "Utility"], + bin: "Codex", + license: "Apache-2.0", + homepage: "https://github.com/Haleclipse/CodexDesktop-Rebuild", + icon: path.join(RESOURCES_DIR, "electron.png"), + }, + }); + }, packageDir, makeDir, arch, packageJSON, results); + + await runMaker("zip", () => { + const { default: MakerZip } = require("@electron-forge/maker-zip"); + return new MakerZip(); + }, packageDir, makeDir, arch, packageJSON, results); + + console.log(`\n== summary ==`); + for (const r of results) { + if (r.ok) console.log(` [ok] ${r.kind}: ${r.artifacts.join(", ")}`); + else console.log(` [FAIL] ${r.kind}: ${r.error}`); + } + const failed = results.filter(r => !r.ok); + if (failed.length === results.length) { + console.error(`\n[x] all makers failed`); + process.exit(1); + } + if (failed.length > 0) { + console.error(`\n[!] ${failed.length}/${results.length} makers failed`); + process.exit(1); + } + console.log(`\n[ok] all ${results.length} makers succeeded`); +} + +main().catch(err => { + console.error("\n[x] Fatal:", err.stack || err.message || err); + process.exit(1); +}); diff --git a/scripts/patch-copyright.js b/scripts/patch-copyright.js index 9efc793c..dda5761f 100644 --- a/scripts/patch-copyright.js +++ b/scripts/patch-copyright.js @@ -103,8 +103,8 @@ function main() { }); if (bundles.length === 0) { - console.error("[x] No main bundle found"); - process.exit(1); + console.log("[skip] No main bundle found (sync-upstream did not populate src/)"); + return; } for (const bundle of bundles) { diff --git a/scripts/patch-devtools.js b/scripts/patch-devtools.js index 90873af0..6d167d6e 100644 --- a/scripts/patch-devtools.js +++ b/scripts/patch-devtools.js @@ -92,8 +92,8 @@ function main() { }); if (bundles.length === 0) { - console.error("[x] No main bundle found"); - process.exit(1); + console.log("[skip] No main bundle found (sync-upstream did not populate src/)"); + return; } for (const bundle of bundles) { diff --git a/scripts/prepare-src.js b/scripts/prepare-src.js index 4bd50cb1..dc003ab0 100644 --- a/scripts/prepare-src.js +++ b/scripts/prepare-src.js @@ -178,6 +178,9 @@ function main() { fs.copyFileSync(vendorCodex, dest); try { fs.chmodSync(dest, 0o755); } catch {} console.log(` [codex] replaced with @cometix/codex`); + } else if (isLinux) { + console.error(` [x] @cometix/codex vendor not found for ${platform}; refusing to ship upstream macOS Mach-O binary inside a Linux package.`); + process.exit(1); } else { console.log(` [!] @cometix/codex vendor not found for ${platform}, keeping upstream`); } @@ -191,7 +194,8 @@ function main() { try { fs.chmodSync(dest, 0o755); } catch {} console.log(` [rg] replaced with Linux rg from @cometix/codex`); } else { - console.log(` [!] Linux rg not found in vendor, keeping upstream (will fail on Linux)`); + console.error(` [x] Linux rg not found in vendor for ${platform}; refusing to ship upstream macOS Mach-O rg inside a Linux package.`); + process.exit(1); } } diff --git a/scripts/sync-upstream.js b/scripts/sync-upstream.js index f0eaca36..10f2b238 100644 --- a/scripts/sync-upstream.js +++ b/scripts/sync-upstream.js @@ -207,10 +207,42 @@ async function syncWin(destDir) { throw new Error(`Windows: resources dir not found${alt ? `, app.asar at ${alt}` : ""}`); } + // 7z/7zz on Windows leaves URL-encoded names in the MSIX (notably %40 for @ + // in scoped npm package paths like %40worklouder/...). The asar header + // references the originals, so extraction fails when it tries to resolve + // unpacked .node binaries. Decode percent-encoded entries in-place. + decodePercentNames(path.join(resourcesDir, "app.asar.unpacked")); + assembleOutput(resourcesDir, destDir, "Windows"); return info; } +function decodePercentNames(root) { + if (!fs.existsSync(root)) return; + let renamed = 0; + const walk = (dir) => { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const cur = path.join(dir, e.name); + let next = cur; + if (/%[0-9A-Fa-f]{2}/.test(e.name)) { + try { + const decoded = decodeURIComponent(e.name); + if (decoded !== e.name) { + next = path.join(dir, decoded); + fs.renameSync(cur, next); + renamed++; + } + } catch { + /* keep as-is on malformed sequences */ + } + } + if (e.isDirectory()) walk(next); + } + }; + walk(root); + if (renamed) console.log(` [decode] renamed ${renamed} percent-encoded path(s)`); +} + // ─── Assemble output ──────────────────────────────────────────── function assembleOutput(resourcesDir, destDir, label) { @@ -286,11 +318,21 @@ async function main() { } if (!SKIP_WIN) { - try { - const winInfo = await getWindowsVersion(); - console.log(` win: ${winInfo.version}`); - results.win = winInfo; - } catch (e) { console.error(` [x] win check: ${e.message}`); } + const MAX_TRIES = 4; + for (let attempt = 1; attempt <= MAX_TRIES; attempt++) { + try { + const winInfo = await getWindowsVersion(); + console.log(` win: ${winInfo.version}`); + results.win = winInfo; + break; + } catch (e) { + const last = attempt === MAX_TRIES; + console.error( + ` [x] win check (attempt ${attempt}/${MAX_TRIES}): ${e.message}${last ? "" : " — retrying"}`, + ); + if (!last) await new Promise((r) => setTimeout(r, attempt * 2000)); + } + } } if (CHECK_ONLY) {