/** * KHAELOR * File: src/cli/lifecycle.ts * Description: Startup/shutdown discipline — crash logging, signal handling, quit-with-running-processes confirmation. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { defaultLogDir, writeCrashFile } from "./logger.js"; import type { FileLogger } from "./logger.js"; /** * Install crash handlers that persist the error to ~/.khaelor/logs/ before * the terminal guard (src/tui/renderer/terminal.ts) restores the shell and * exits. Registered FIRST so the crash file exists by the time the guard's * own uncaughtException handler runs. Never writes to the live screen. */ export function installCrashHandlers(logger: FileLogger): void { const record = (origin: string) => { return (error: unknown): void => { const file = writeCrashFile(defaultLogDir(), error); logger.error(`${origin}`, { error: error instanceof Error ? error.message : String(error), ...(file !== null ? { crashFile: file } : {}), }); }; }; process.on("uncaughtException", record("uncaughtException")); process.on("unhandledRejection", record("unhandledRejection")); } /** * Quit confirmation policy: quitting with running background processes * requires a second quit within the window (they are killed on exit — * ProcessExited{cause:"khaelor-shutdown"} is recorded for each). */ export class QuitGuard { readonly #windowMs: number; readonly #now: () => number; #armedAt = 0; constructor(options: { windowMs?: number; now?: () => number } = {}) { this.#windowMs = options.windowMs ?? 5_000; this.#now = options.now ?? Date.now; } /** * Returns "quit" when quitting may proceed, or "confirm" when the caller * must warn the user (running processes) and wait for a second quit. */ request(runningProcesses: number): "quit" | "confirm" { if (runningProcesses === 0) return "quit"; const now = this.#now(); if (now - this.#armedAt <= this.#windowMs) return "quit"; this.#armedAt = now; return "confirm"; } } /** Message shown when quit needs confirmation. */ export function quitConfirmationLines(runningProcesses: number): string[] { const plural = runningProcesses === 1 ? "process is" : "processes are"; return [ "", ` ${runningProcesses} background ${plural} still running`, " quitting stops them (recorded as khaelor-shutdown) — quit again to confirm", ]; } /** * Wire SIGINT/SIGTERM to an async, exactly-once shutdown path. The terminal * guard has its own SIGTERM restore; this handler runs first (registration * order) and owns the orderly engine shutdown. */ export function installSignalShutdown(shutdown: (signal: string) => Promise): void { let invoked = false; const handler = (signal: string): void => { if (invoked) return; invoked = true; void shutdown(signal).finally(() => { process.exit(signal === "SIGINT" ? 130 : 143); }); }; process.on("SIGINT", () => handler("SIGINT")); process.on("SIGTERM", () => handler("SIGTERM")); }