import { existsSync, readFileSync } from "node:fs"; export type PersistedDevServerStatus = { dirty: boolean; lastChangedAt: string | null; changedPathCount: number; changedPathsSample: string[]; pendingMigrations: string[]; lastRestartAt: string | null; }; export type DevServerHealthStatus = { enabled: true; restartRequired: boolean; reason: "backend_changes" | "pending_migrations" | "backend_changes_and_pending_migrations" | null; lastChangedAt: string | null; changedPathCount: number; changedPathsSample: string[]; pendingMigrations: string[]; autoRestartEnabled: boolean; activeRunCount: number; waitingForIdle: boolean; lastRestartAt: string | null; }; function normalizeStringArray(value: unknown): string[] { if (!Array.isArray(value)) return []; return value .filter((entry): entry is string => typeof entry === "string") .map((entry) => entry.trim()) .filter((entry) => entry.length > 0); } function normalizeTimestamp(value: unknown): string | null { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } export function readPersistedDevServerStatus( env: NodeJS.ProcessEnv = process.env, ): PersistedDevServerStatus | null { const filePath = env.PAPERCLIP_DEV_SERVER_STATUS_FILE?.trim(); if (!filePath || !existsSync(filePath)) return null; try { const raw = JSON.parse(readFileSync(filePath, "utf8")) as Record; const changedPathsSample = normalizeStringArray(raw.changedPathsSample).slice(0, 5); const pendingMigrations = normalizeStringArray(raw.pendingMigrations); const changedPathCountRaw = raw.changedPathCount; const changedPathCount = typeof changedPathCountRaw === "number" && Number.isFinite(changedPathCountRaw) ? Math.max(0, Math.trunc(changedPathCountRaw)) : changedPathsSample.length; const dirtyRaw = raw.dirty; const dirty = typeof dirtyRaw === "boolean" ? dirtyRaw : changedPathCount > 0 || pendingMigrations.length > 0; return { dirty, lastChangedAt: normalizeTimestamp(raw.lastChangedAt), changedPathCount, changedPathsSample, pendingMigrations, lastRestartAt: normalizeTimestamp(raw.lastRestartAt), }; } catch { return null; } } export function toDevServerHealthStatus( persisted: PersistedDevServerStatus, opts: { autoRestartEnabled: boolean; activeRunCount: number }, ): DevServerHealthStatus { const hasPathChanges = persisted.changedPathCount > 0; const hasPendingMigrations = persisted.pendingMigrations.length > 0; const reason = hasPathChanges && hasPendingMigrations ? "backend_changes_and_pending_migrations" : hasPendingMigrations ? "pending_migrations" : hasPathChanges ? "backend_changes" : null; const restartRequired = persisted.dirty || reason !== null; return { enabled: true, restartRequired, reason, lastChangedAt: persisted.lastChangedAt, changedPathCount: persisted.changedPathCount, changedPathsSample: persisted.changedPathsSample, pendingMigrations: persisted.pendingMigrations, autoRestartEnabled: opts.autoRestartEnabled, activeRunCount: opts.activeRunCount, waitingForIdle: restartRequired && opts.autoRestartEnabled && opts.activeRunCount > 0, lastRestartAt: persisted.lastRestartAt, }; }