#!/usr/bin/env node // The console-window class (5 October 2026, PC 1): a child the app starts on Windows without CREATE_NO_WINDOW, or an // elevated child started without SW_HIDE, gets a console of its own, and on Windows 11 with Windows Terminal as the // default terminal that console is a visible Terminal window on the user's desk. Rule: every process the app starts // on Windows runs with a hidden console. This check fails CI when // - a `Command::new(` in app/igneum-app/src is not quieted within 20 lines: crate::platform::quiet, run_timeout, // run_capture, run_streamed, spawn_detached, elevated_command, or creation_flags(0x0800_0000) (CREATE_NO_WINDOW); // programs that only exist off Windows (nohup, osascript, pkexec, hdiutil, ...) are allowed, and a // `// console: ` comment on the line or the line above allows a builder the caller quiets; // the check looks 2 lines back as well, for `run_timeout(\n Command::new(...)`; // - a `creation_flags(` carries anything but 0x0800_0000 (DETACHED_PROCESS made powershell exit at start-up, // 0.3.0 to 0.3.4, docs/bugs.md); // - a PowerShell `Start-Process` written by the Rust code lacks `-WindowStyle Hidden` or `-NoNewWindow` (a GUI // program, which never gets a console, takes a `# console: ` comment on the line or the line above); // - app/windows/host.cpp calls CreateProcessW without CREATE_NO_WINDOW or sets up a ShellExecuteExW without // nShow = SW_HIDE (ShellExecuteW "open" of a URL is the browser, allowed). // node tools/ci/windows-spawn-check.mjs the tree // node tools/ci/windows-spawn-check.mjs --self-test the rules on known-good and known-bad samples import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, resolve, dirname, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..'); const QUIET = /\bquiet\(|\brun_timeout\(|\brun_capture\(|\brun_streamed\(|\bspawn_detached\(|\belevated_command\(|creation_flags\(0x0800_0000\)/; const OFF_WINDOWS = /Command::new\((?:crate::platform::)?tool\("(?:nohup|osascript|pkexec|hdiutil|ditto|open|xattr|system_profiler|sysctl|sntp|scutil|caffeinate)"\)|Command::new\("(?:pkexec|xdg-open|\/bin\/bash|\/usr\/bin\/[a-z]+)"\)|Command::new\(staged\.join\("Contents\/MacOS/; const WINDOW = 20; export function checkRust(text, file) { const lines = text.split('\n'); const out = []; for (let i = 0; i < lines.length; i++) { const l = lines[i]; if (/Command::new\(/.test(l) && !/^\s*\/\//.test(l)) { const allowed = OFF_WINDOWS.test(l) || /\/\/ console:/.test(l) || (i > 0 && /\/\/ console:/.test(lines[i - 1])); if (!allowed) { const span = lines.slice(Math.max(0, i - 2), i + WINDOW).join('\n'); // 2 lines back: run_timeout(\n Command::new(...) if (!QUIET.test(span)) out.push(`${file}:${i + 1}: Command::new without a hidden console within ${WINDOW} lines (quiet, run_timeout, run_capture, run_streamed, spawn_detached, elevated_command or creation_flags(0x0800_0000)); add one, or a '// console: ' comment`); } } const cf = /creation_flags\(([^)]*)\)/.exec(l); if (cf && cf[1].trim() !== '0x0800_0000') out.push(`${file}:${i + 1}: creation_flags(${cf[1]}) is not CREATE_NO_WINDOW alone (0x0800_0000)`); if (/Start-Process\b/.test(l) && !/^\s*\/\//.test(l) && !/-WindowStyle Hidden|-NoNewWindow/.test(l) && !/(\/\/|#) console:/.test(l) && !(i > 0 && /(\/\/|#) console:/.test(lines[i - 1]))) out.push(`${file}:${i + 1}: Start-Process without -WindowStyle Hidden or -NoNewWindow (a GUI program takes a '# console: ' comment)`); } return out; } export function checkHost(text, file) { const lines = text.split('\n'); const out = []; for (let i = 0; i < lines.length; i++) { const l = lines[i]; if (/CreateProcessW?\s*\(/.test(l) && !/CREATE_NO_WINDOW/.test(lines.slice(i, i + 3).join('\n'))) out.push(`${file}:${i + 1}: CreateProcess without CREATE_NO_WINDOW`); if (/ShellExecuteExW?\s*\(/.test(l) && !/nShow\s*=\s*SW_HIDE/.test(lines.slice(Math.max(0, i - 12), i + 1).join('\n'))) out.push(`${file}:${i + 1}: ShellExecuteEx without nShow = SW_HIDE in the 12 lines before it`); if (/ShellExecuteW?\s*\(/.test(l) && !/ShellExecuteExW?/.test(l) && !/L"open"/.test(l)) out.push(`${file}:${i + 1}: ShellExecute that is not the browser "open" of a URL`); } return out; } function walk(dir, ext, acc = []) { for (const e of readdirSync(dir)) { const p = join(dir, e); if (statSync(p).isDirectory()) { if (e !== 'target') walk(p, ext, acc); } else if (p.endsWith(ext)) acc.push(p); } return acc; } function selfTest() { const good = `fn a() {\n let mut c = Command::new(crate::platform::tool("powershell"));\n c.args(["-NoProfile"]);\n crate::platform::quiet(&mut c);\n c.spawn();\n}\n`; const bad = `fn a() {\n let mut c = Command::new(crate::platform::tool("powershell"));\n c.args(["-NoProfile"]);\n c.spawn();\n}\n`; const badFlag = `c.creation_flags(0x0000_0008);\n`; const badPs = `let ps = format!("Start-Process -FilePath '{}' -Wait", exe);\n`; const okPs = `let ps = format!("Start-Process -FilePath '{}' -Wait -WindowStyle Hidden", exe);\n`; const offWin = `let out = Command::new(tool("osascript")).args(["-e", "x"]).output();\n`; const allowed = `// console: the caller quiets it\nlet mut c = Command::new(wsl_exe);\n`; const hostGood = `sei.nShow = SW_HIDE;\nif (!ShellExecuteExW(&sei)) {}\nCreateProcessW(exe, buf, nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, dir, &si, &pi);\nShellExecuteW(nullptr, L"open", url, nullptr, nullptr, SW_SHOWNORMAL);\n`; const hostBad = `sei.nShow = SW_SHOW;\nif (!ShellExecuteExW(&sei)) {}\nCreateProcessW(exe, buf, nullptr, nullptr, TRUE, 0, nullptr, dir, &si, &pi);\n`; const cases = [ ['quieted Command', checkRust(good, 't.rs').length === 0], ['bare Command fails', checkRust(bad, 't.rs').length === 1], ['DETACHED_PROCESS fails', checkRust(badFlag, 't.rs').length === 1], ['Start-Process without Hidden fails', checkRust(badPs, 't.rs').length === 1], ['Start-Process with Hidden passes', checkRust(okPs, 't.rs').length === 0], ['off-Windows program passes', checkRust(offWin, 't.rs').length === 0], ['console: comment passes', checkRust(allowed, 't.rs').length === 0], ['host.cpp good passes', checkHost(hostGood, 'h.cpp').length === 0], ['host.cpp bad fails twice', checkHost(hostBad, 'h.cpp').length === 2], ]; let fail = 0; for (const [name, ok] of cases) { console.log(`${ok ? 'ok ' : 'FAIL'} ${name}`); if (!ok) fail++; } return fail; } if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { if (process.argv.includes('--self-test')) { const f = selfTest(); console.log(f ? `windows-spawn: self-test FAILED (${f})` : 'windows-spawn: self-test ok'); process.exit(f ? 1 : 0); } const problems = []; for (const f of walk(join(ROOT, 'app', 'igneum-app', 'src'), '.rs')) problems.push(...checkRust(readFileSync(f, 'utf8'), relative(ROOT, f))); const host = join(ROOT, 'app', 'windows', 'host.cpp'); try { problems.push(...checkHost(readFileSync(host, 'utf8'), relative(ROOT, host))); } catch {} for (const p of problems) console.log(p); console.log(problems.length ? `windows-spawn: ${problems.length} spawn(s) without a hidden console` : 'windows-spawn: every Windows spawn in app/igneum-app/src and app/windows/host.cpp runs with a hidden console'); process.exit(problems.length ? 1 : 0); }