import { app, utilityProcess } from 'electron'; import { existsSync } from 'node:fs'; import path from 'node:path'; import { getOpenClawDir, getOpenClawEntryPath } from './paths'; import { logger } from './logger'; import { getUvMirrorEnv } from './uv-env'; const OPENCLAW_DOCTOR_TIMEOUT_MS = 60_000; const OPENCLAW_DOCTOR_ARGS = ['doctor', '--json']; const OPENCLAW_DOCTOR_FIX_ARGS = ['doctor', '--fix', '--yes', '--non-interactive']; export type OpenClawDoctorMode = 'diagnose' | 'fix'; export interface OpenClawDoctorResult { mode: OpenClawDoctorMode; success: boolean; exitCode: number | null; stdout: string; stderr: string; command: string; cwd: string; durationMs: number; timedOut?: boolean; error?: string; } function getBundledBinPath(): string { const target = `${process.platform}-${process.arch}`; return app.isPackaged ? path.join(process.resourcesPath, 'bin') : path.join(process.cwd(), 'resources', 'bin', target); } async function runDoctorCommand(mode: OpenClawDoctorMode): Promise { const openclawDir = getOpenClawDir(); const entryScript = getOpenClawEntryPath(); const args = mode === 'fix' ? OPENCLAW_DOCTOR_FIX_ARGS : OPENCLAW_DOCTOR_ARGS; const command = `openclaw ${args.join(' ')}`; const startedAt = Date.now(); if (!existsSync(entryScript)) { const error = `OpenClaw entry script not found at ${entryScript}`; logger.error(`Cannot run OpenClaw doctor: ${error}`); return { mode, success: false, exitCode: null, stdout: '', stderr: '', command, cwd: openclawDir, durationMs: Date.now() - startedAt, error, }; } const binPath = getBundledBinPath(); const binPathExists = existsSync(binPath); const finalPath = binPathExists ? `${binPath}${path.delimiter}${process.env.PATH || ''}` : process.env.PATH || ''; const uvEnv = await getUvMirrorEnv(); logger.info( `Running OpenClaw doctor (mode=${mode}, entry="${entryScript}", args="${args.join(' ')}", cwd="${openclawDir}", bundledBin=${binPathExists ? 'yes' : 'no'})`, ); return await new Promise((resolve) => { const child = utilityProcess.fork(entryScript, args, { cwd: openclawDir, stdio: 'pipe', env: { ...process.env, ...uvEnv, PATH: finalPath, OPENCLAW_NO_RESPAWN: '1', } as NodeJS.ProcessEnv, }); let stdout = ''; let stderr = ''; let settled = false; const finish = (result: Omit) => { if (settled) return; settled = true; resolve({ ...result, durationMs: Date.now() - startedAt, }); }; const timeout = setTimeout(() => { logger.error(`OpenClaw doctor timed out after ${OPENCLAW_DOCTOR_TIMEOUT_MS}ms`); try { child.kill(); } catch { // ignore } finish({ mode, success: false, exitCode: null, stdout, stderr, command, cwd: openclawDir, timedOut: true, error: `Timed out after ${OPENCLAW_DOCTOR_TIMEOUT_MS}ms`, }); }, OPENCLAW_DOCTOR_TIMEOUT_MS); child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('error', (error) => { clearTimeout(timeout); logger.error('Failed to spawn OpenClaw doctor process:', error); finish({ mode, success: false, exitCode: null, stdout, stderr, command, cwd: openclawDir, error: error instanceof Error ? error.message : String(error), }); }); child.on('exit', (code) => { clearTimeout(timeout); logger.info(`OpenClaw doctor exited with code ${code ?? 'null'}`); finish({ mode, success: code === 0, exitCode: code, stdout, stderr, command, cwd: openclawDir, }); }); }); } export async function runOpenClawDoctor(): Promise { return await runDoctorCommand('diagnose'); } export async function runOpenClawDoctorFix(): Promise { return await runDoctorCommand('fix'); }