import { spawn } from 'node:child_process'; const run = (file, args, timeoutMs = 6000) => new Promise((resolve, reject) => { const child = spawn(file, args, { stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = '', stderr = '', settled = false; const finish = (error) => { if (settled) return; settled = true; clearTimeout(timer); error ? reject(error) : resolve(stdout.trim()); }; const timer = setTimeout(() => { child.kill('SIGTERM'); finish(new Error(`${file} timed out`)); }, timeoutMs); child.stdout?.on('data', chunk => { stdout = (stdout + chunk).slice(-64000); }); child.stderr?.on('data', chunk => { stderr = (stderr + chunk).slice(-2000); }); child.once('error', finish); child.once('close', code => finish(code === 0 ? null : new Error(stderr.trim() || `${file} exited ${code}`))); }); const portal = iface => run('gdbus', ['call', '--session', '--dest', 'org.freedesktop.portal.Desktop', '--object-path', '/org/freedesktop/portal/desktop', '--method', 'org.freedesktop.DBus.Properties.Get', `org.freedesktop.portal.${iface}`, 'version']); const checks = [ ['session', false, async () => { if (process.env.XDG_SESSION_TYPE !== 'wayland') throw new Error('a GNOME Wayland session is required for the primary backend'); return 'Wayland'; }], ['remote-desktop', false, () => portal('RemoteDesktop')], ['screencast', false, () => portal('ScreenCast')], ['pipewire', false, async () => { await run('pw-cli', ['info', '0']); return 'live server responds'; }], ['frame-pipeline', false, async () => { await run('python3', ['-c', 'import gi; gi.require_version("Gst", "1.0"); from gi.repository import Gst; from PIL import Image; Gst.init(None); assert all(Gst.ElementFactory.find(n) for n in ["pipewiresrc", "videoconvert", "appsink"]), "missing GStreamer capture elements"; assert Image.registered_extensions().get(".webp") == "WEBP", "Pillow WebP missing"']); return 'GStreamer capture elements and Pillow WebP available'; }], ['at-spi', false, async () => { await run('python3', ['-c', 'import gi; gi.require_version("Atspi", "2.0"); from gi.repository import Atspi; Atspi.init(); assert Atspi.get_desktop(0) is not None, "accessibility desktop unavailable"']); return 'live accessibility desktop responds'; }], ['libei', false, async () => { await run('python3', ['-c', 'import ctypes; lib=ctypes.CDLL("libei.so.1"); assert lib.ei_new_sender and lib.ei_setup_backend_fd']); return 'sender library loads'; }], ['shell-helper', false, async () => { await run('gdbus', ['call', '--session', '--dest', 'io.qvac.Jarvis.Shell', '--object-path', '/io/qvac/Jarvis/Shell', '--method', 'io.qvac.Jarvis.Shell.ListWindows']); return 'GNOME extension window service responds'; }], ['daemon', true, async () => { await run('gdbus', ['call', '--session', '--dest', 'io.qvac.Jarvis', '--object-path', '/io/qvac/Jarvis', '--method', 'io.qvac.Jarvis.Session.ComputerStatus']); return 'computer-use status responds'; }], ]; let failed = 0; for (const [name, optional, check] of checks) { try { console.log(`ok ${name}: ${await check()}${optional ? ' (optional)' : ''}`); } catch (error) { if (!optional) failed++; console.log(`FAIL ${name}: ${error.message}${optional ? ' (optional)' : ''}`); } } console.log(failed ? `cu-doctor: ${failed} required checks failed` : 'cu-doctor: live prerequisites passed; input and capture still require a consented cu-smoke run'); process.exitCode = failed ? 1 : 0;