← Files Device ItARCHIVED FILE
scripts/setup.mjs
8.45 KB · Nov 3, 2026 · 00:14 UTC
#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
import { spawnSync } from 'node:child_process';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const installArgs = ['ci', '--ignore-scripts', '--no-audit', '--no-fund', '--include=dev', '--include=optional'];
export function runCommand(command, args, cwd, install = false) {
// Windows npm is a .cmd launcher. Only fixed npm arguments pass through cmd.exe.
if (process.platform === 'win32' && command === 'npm') {
args = ['/d', '/s', '/c', `npm ${args.join(' ')}`];
command = 'cmd.exe';
}
return spawnSync(command, args, { cwd, encoding: 'utf8', stdio: install ? ['ignore', 2, 2] : 'pipe', maxBuffer: 1024 * 1024, env: install ? { ...process.env, PLAYWRIGHT_SKIP_BROWSER_GC: '1' } : process.env });
}
export async function dependencyState(directory) {
const pkg = JSON.parse(await fs.readFile(path.join(directory, 'package.json'), 'utf8'));
const lock = JSON.parse(await fs.readFile(path.join(directory, 'package-lock.json'), 'utf8'));
for (const group of ['dependencies', 'devDependencies']) {
for (const [name, version] of Object.entries(pkg[group] || {})) {
if (!/^\d+\.\d+\.\d+$/.test(version) || lock.packages?.['']?.[group]?.[name] !== version || lock.packages?.[`node_modules/${name}`]?.version !== version) {
throw new Error(`Package and lock disagree for ${name}; restore the supplied package files before setup`);
}
}
}
try {
if ((await fs.lstat(path.join(directory, 'node_modules'))).isSymbolicLink()) throw new Error('node_modules is a symlink; use a separate package extraction before setup');
} catch (error) { if (error.code !== 'ENOENT') throw error; }
const issues = [];
let checkedPackages = 0;
for (const [relative, entry] of Object.entries(lock.packages || {})) {
if (!relative) continue;
if (!relative.startsWith('node_modules/') || relative.split('/').includes('..') || !entry.version || entry.link) throw new Error('Unsupported dependency lock entry; restore the supplied lockfile');
checkedPackages++;
try {
const installed = JSON.parse(await fs.readFile(path.join(directory, relative, 'package.json'), 'utf8'));
if (installed.version !== entry.version) issues.push(`${relative}: expected ${entry.version}, found ${installed.version}`);
} catch (error) {
if (entry.optional && error.code === 'ENOENT') continue;
issues.push(`${relative}: missing or unreadable package`);
}
}
if (!checkedPackages) throw new Error('Empty dependency lock; restore the supplied lockfile');
return { ok: issues.length === 0, checkedPackages, issues };
}
async function browserProbe(directory, run) {
const result = run(process.execPath, [path.join(directory, 'scripts/setup.mjs'), '--probe-browser'], directory);
if (result.status !== 0) return { ok: false, phase: 'browser', missing: false, error: `Browser probe process failed (${result.error?.code || result.status}); inspect local runtime access` };
try { return JSON.parse(result.stdout); }
catch { return { ok: false, phase: 'browser', missing: false, error: 'Browser probe did not return a readiness result' }; }
}
export async function setup({ install = false, directory = root, nodeVersion = process.versions.node, run = runCommand, probe = browserProbe } = {}) {
const result = { ok: false, mode: install ? 'setup' : 'preflight', root: directory, runtime: { node: nodeVersion, platform: process.platform, arch: process.arch }, actions: [], nextSteps: [] };
if (!/^\d+\.\d+\.\d+$/.test(nodeVersion) || Number(nodeVersion.split('.')[0]) < 22) {
result.nextSteps.push(`Install Node.js 22 or newer with npm on this local host, then rerun node scripts/run.mjs ${result.mode}. No runtime was installed.`);
return result;
}
const npm = run('npm', ['--version'], directory);
if (npm.status !== 0 || !/^\d+\.\d+\.\d+\s*$/.test(npm.stdout || '')) {
result.runtime.npm = 'unavailable';
result.nextSteps.push('Make npm available on PATH with your local Node.js installation, then rerun this command. No dependencies were installed.');
return result;
}
result.runtime.npm = npm.stdout.trim();
let dependenciesInstalled = false;
async function installDependencies() {
const installed = run('npm', installArgs, directory, true);
result.actions.push({ command: `npm ${installArgs.join(' ')}`, exitCode: installed.status, ok: installed.status === 0 });
if (installed.status !== 0) throw new Error('Locked dependency installation failed. Check the command output and npm registry/network access, then rerun setup. No fallback installer ran.');
dependenciesInstalled = true;
result.dependencies = await dependencyState(directory);
if (!result.dependencies.ok) throw new Error('Locked dependency installation completed but required package versions are still missing. Check npm output before rerunning setup.');
}
try {
result.dependencies = await dependencyState(directory);
if (!result.dependencies.ok) {
if (!install) { result.nextSteps.push('Run node scripts/run.mjs setup when dependency/browser downloads are authorized.'); return result; }
await installDependencies();
}
result.browser = await probe(directory, run);
if (result.browser.phase === 'dependencies' && install && !dependenciesInstalled) {
await installDependencies();
result.browser = await probe(directory, run);
}
if (!result.browser.ok && result.browser.missing && install) {
const args = [path.join(directory, 'node_modules/playwright/cli.js'), 'install', 'chromium'];
const installed = run(process.execPath, args, directory, true);
result.actions.push({ command: 'node node_modules/playwright/cli.js install chromium', exitCode: installed.status, ok: installed.status === 0 });
if (installed.status !== 0) throw new Error('Pinned Chromium download failed. Check the command output and network/cache permissions, then rerun setup. No system packages were installed.');
result.browser = await probe(directory, run);
}
if (!result.browser.ok) {
result.nextSteps.push(result.browser.phase === 'dependencies' ? 'Run node scripts/run.mjs setup to repair incomplete package dependencies when downloads are authorized.' : result.browser.missing ? 'Run node scripts/run.mjs setup when the pinned Chromium download is authorized.' : 'Read browser.error below. Resolve the local browser/runtime or OS dependency problem, then rerun preflight. Setup never installs OS packages.');
return result;
}
result.ok = true;
result.nextSteps.push('Ready. Follow FIRST-RUN.md: select approved public assets, prepare a fresh output, preview it, then verify the known shell text.');
} catch (error) { result.error = error.message; result.nextSteps.push('Resolve the reported error before preparing an app.'); }
return result;
}
async function probeLocalBrowser() {
let browser, result;
try {
const require = createRequire(path.join(root, 'package.json'));
let chromium;
try {
({ chromium } = require('playwright'));
for (const name of ['qrcode', 'pngjs', 'jsqr']) require(name);
} catch (error) { return { ok: false, phase: 'dependencies', missing: false, error: error.message }; }
try {
browser = await chromium.launch({ headless: true, timeout: 15000 });
result = { ok: true, phase: 'browser', version: browser.version(), executable: chromium.executablePath(), browserClosed: false };
} catch (error) {
result = { ok: false, phase: 'browser', missing: /Executable doesn't exist at /.test(error.message), error: error.message.slice(0, 2500) };
}
} finally {
if (browser) {
try { await browser.close(); result.browserClosed = true; }
catch (error) { result.ok = false; result.error = `Browser cleanup failed: ${error.message}`; }
}
}
return result;
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
if (process.argv[2] === '--probe-browser' && process.argv.length === 3) {
console.log(JSON.stringify(await probeLocalBrowser()));
} else if (process.argv.length === 2 || (process.argv.length === 3 && process.argv[2] === '--check')) {
const result = await setup({ install: process.argv[2] !== '--check' });
console.log(JSON.stringify(result, null, 2));
if (!result.ok) process.exitCode = 1;
} else { console.error('Usage: node scripts/setup.mjs [--check]'); process.exitCode = 1; }
}
SHA-256: 821907fcbd875ad088d7c54e303f1d79bab090dee85b40e5ea4f331637165665