#!/usr/bin/env node /** * Verify a harness tree is self-contained and actually boots from the bundle. * * Default: check the assembled `build/harness`. With `--app `, locate the * `resources/harness` inside a packed/unpacked app instead — this additionally * proves electron-builder's `extraResources` copy preserved the relative * in-tree links. `--boot` also spawns the web profile straight from the bundle * (temp DSH_HOME, telemetry off) and waits for its readiness line, proving the * relinked node_modules resolves without the repository present. * * Usage: * node scripts/verify-harness.mjs # build/harness * node scripts/verify-harness.mjs --boot # + web boot smoke * node scripts/verify-harness.mjs --app dist # a packed app's harness * node scripts/verify-harness.mjs --app dist --boot */ import { spawn, spawnSync } from 'node:child_process' import { existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { basename, dirname, isAbsolute, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { assertSelfContained } from './harness-relink.mjs' const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../..') const desktopDir = join(root, 'apps/desktop') const defaultHarness = join(desktopDir, 'build/harness') const args = process.argv.slice(2) const boot = args.includes('--boot') const appIndex = args.indexOf('--app') const appValue = args.find((a) => a.startsWith('--app='))?.slice('--app='.length) ?? (appIndex !== -1 ? args[appIndex + 1] : undefined) const appRoot = appValue === undefined ? undefined : isAbsolute(appValue) ? resolve(appValue) : resolve(desktopDir, appValue) const timeoutMs = 60_000 const READY_RE = /http:\/\/127\.0\.0\.1:(\d+)/ function fail(message) { console.error(`verify-harness: ${message}`) process.exit(1) } /** Find every `resources/harness` under an unpacked electron-builder app. */ function findHarnessUnder(appDir) { const found = [] // electron-builder unpacked layout: // win: /win-unpacked/resources/harness // mac: /mac(-arch)/.app/Contents/Resources/harness const visit = (dir, depth) => { if (depth > 5) return let entries try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return } for (const entry of entries) { const p = join(dir, entry.name) if (basename(p) === 'harness' && basename(dirname(p)) === 'resources') found.push(p) if (entry.isDirectory()) visit(p, depth + 1) } } visit(appDir, 0) return found } function nodeBinary(harness) { return join(harness, 'bin', process.platform === 'win32' ? 'node.exe' : 'node') } function cliEntry(harness) { return join(harness, 'apps/cli/lib/bin.js') } /** Run the bundled CLI's `--version` straight from the bundle. */ function checkVersion(harness) { const result = spawnSync(nodeBinary(harness), [cliEntry(harness), '--version'], { cwd: harness, encoding: 'utf8', timeout: timeoutMs, }) if (result.error !== undefined || result.status !== 0) { fail(`the bundled CLI failed to run: ${result.error?.message ?? result.stderr?.trim() ?? `exit ${String(result.status)}`}`) } console.log(` dsh --version: ${result.stdout?.trim()}`) } /** * Spawn the web profile from the bundle and resolve once it prints its * readiness URL. Hermetic: a temp DSH_HOME and telemetry off, so the check * neither touches the real profile nor the repository. */ function bootWeb(harness) { const home = mkdtempSync(join(tmpdir(), 'dsh-verify-home-')) const child = spawn( nodeBinary(harness), [cliEntry(harness), '--profile', 'web', '--host', '127.0.0.1', '--port', '0'], { cwd: harness, stdio: ['ignore', 'pipe', 'inherit'], env: { ...process.env, DSH_HOME: home, DSH_TELEMETRY_DISABLED: '1', DSH_ALLOW_PLUGIN_INSTALL: '1' }, }, ) return new Promise((resolvePromise, reject) => { let settled = false let buffered = '' const cleanup = () => { try { rmSync(home, { recursive: true, force: true }) } catch { // Best effort; a locked file on Windows is not fatal for a verify tool. } } const timer = setTimeout(() => { if (settled) return settled = true child.kill() cleanup() reject(new Error(`web boot timed out after ${timeoutMs}ms`)) }, timeoutMs) child.stdout?.setEncoding('utf8') child.stdout?.on('data', (chunk) => { if (settled) return buffered += chunk if (READY_RE.test(buffered)) { settled = true clearTimeout(timer) child.kill() cleanup() resolvePromise() } }) child.on('error', (error) => { if (settled) return settled = true clearTimeout(timer) cleanup() reject(error) }) child.on('exit', (code) => { if (settled) return settled = true clearTimeout(timer) cleanup() reject(new Error(`harness exited before serving (code ${String(code)})`)) }) }) } async function main() { const harness = (() => { if (appRoot !== undefined) { const found = findHarnessUnder(appRoot) if (found.length === 0) fail(`no resources/harness found under ${appRoot}`) if (found.length > 1) { console.warn(`verify-harness: ${found.length} harness dirs found; checking the first:\n ${found.join('\n ')}`) } return found[0] } if (!existsSync(defaultHarness)) fail(`build/harness not found at ${defaultHarness} — run build:harness first`) return defaultHarness })() console.log(`verify-harness: ${harness}`) if (!existsSync(nodeBinary(harness))) fail(`bundled node not found at ${nodeBinary(harness)}`) try { assertSelfContained(harness) } catch (error) { fail(error instanceof Error ? error.message : String(error)) } console.log(' links: self-contained (no link escapes the bundle)') checkVersion(harness) if (boot) { try { await bootWeb(harness) } catch (error) { fail(error instanceof Error ? error.message : String(error)) } console.log(' web boot: ok') } console.log('verify-harness: ok') } main().catch((error) => fail(error instanceof Error ? error.message : String(error)))