import { execFile } from 'node:child_process'; import { access, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { promisify } from 'node:util'; import toolInvocation from '../src/shared/tool-invocation.cjs'; const exec = promisify(execFile); const { npmProbeCandidates } = toolInvocation; const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')); const checks = []; const jsonMode = process.argv.includes('--json'); function add(id, name, ok, detail, help = '') { checks.push({ id, name, status: ok ? 'pass' : 'fail', ok, detail, help }); } const major = Number(process.versions.node.split('.')[0]); add('node', 'Node.js', major >= 22, process.version, 'Install Node.js 22 or newer.'); try { const failures = []; let version = ''; let source = ''; for (const candidate of npmProbeCandidates()) { try { const { stdout } = await exec(candidate.file, candidate.args, { windowsHide: true }); version = stdout.trim(); source = candidate.source; if (version) break; } catch (error) { failures.push(`${candidate.source}: ${error.message}`); } } if (!version) throw new Error(failures.join(' | ') || 'No npm invocation candidate succeeded.'); add('npm', 'npm', true, `${version} (${source})`); } catch (error) { add('npm', 'npm', false, error.message, 'Install npm together with Node.js and ensure npm.cmd is available on PATH.'); } try { const { stdout } = await exec('git', ['--version']); add('git', 'Git', true, stdout.trim()); const [name, email] = await Promise.all([ exec('git', ['config', '--global', '--get', 'user.name']).then((result) => result.stdout.trim()).catch(() => ''), exec('git', ['config', '--global', '--get', 'user.email']).then((result) => result.stdout.trim()).catch(() => '') ]); add('git-identity', 'Git identity', Boolean(name && email), name && email ? `${name} <${email}>` : 'user.name or user.email is missing', 'Configure git config --global user.name and user.email.'); } catch (error) { add('git', 'Git', false, error.message, 'Install Git and ensure git is on PATH.'); } try { await access(new URL('../node_modules/electron/package.json', import.meta.url)); add('electron', 'Electron dependency', true, 'installed'); } catch { add('electron', 'Electron dependency', false, 'not installed', 'Run npm install.'); } let markerDirectory = null; try { markerDirectory = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-doctor-')); await writeFile(path.join(markerDirectory, 'write-test'), 'ok'); add('temp-storage', 'Local diagnostic storage', true, markerDirectory.replace(os.homedir(), '')); } catch (error) { add('temp-storage', 'Local diagnostic storage', false, error.message, 'Check local disk permissions and free space.'); } finally { if (markerDirectory) await rm(markerDirectory, { recursive: true, force: true }).catch(() => {}); } const report = { product: 'ForgeFlow', version: packageJson.version, generatedAt: new Date().toISOString(), platform: process.platform, arch: process.arch, ready: checks.every((check) => check.ok), checks }; if (jsonMode) console.log(JSON.stringify(report, null, 2)); else { console.log('ForgeFlow doctor\n'); for (const check of checks) console.log(`${check.ok ? 'PASS' : 'FAIL'} ${check.name.padEnd(26)} ${check.detail}`); console.log(`\n${report.ready ? 'Environment is ready.' : 'Resolve failed checks before starting ForgeFlow.'}`); } if (!report.ready) process.exitCode = 1;