import fs from "node:fs/promises"; import path from "node:path"; import { CLEANUP_INTERVAL_MS, DEFAULT_IDLE_HOURS, PROJECTS_ROOT } from "./config"; import { isBuildActive } from "./build"; import type { ProjectMeta } from "./types"; let schedulerStarted = false; export function isProjectIdle(lastAccessAt: string, idleHours: number, now = Date.now()) { const timestamp = Date.parse(lastAccessAt); return Number.isFinite(timestamp) && timestamp <= now - idleHours * 60 * 60 * 1000; } export async function cleanupIdleNodeModules(idleHours = DEFAULT_IDLE_HOURS) { await fs.mkdir(PROJECTS_ROOT, { recursive: true }); const cutoff = Date.now() - idleHours * 60 * 60 * 1000; const cleaned: string[] = []; for (const entry of await fs.readdir(PROJECTS_ROOT, { withFileTypes: true })) { if (!entry.isDirectory() || isBuildActive(entry.name)) continue; const root = path.join(PROJECTS_ROOT, entry.name); try { const metaPath = path.join(root, "project.json"); const meta: ProjectMeta = JSON.parse(await fs.readFile(metaPath, "utf8")); if (!isProjectIdle(meta.lastAccessAt, idleHours, Date.now())) continue; const modules = path.join(root, "workspace", "node_modules"); const stat = await fs.lstat(modules).catch(() => null); if (!stat?.isDirectory() || stat.isSymbolicLink()) continue; await fs.rm(modules, { recursive: true, force: true }); meta.nodeModulesCleanedAt = new Date().toISOString(); await fs.writeFile(metaPath, JSON.stringify(meta, null, 2)); cleaned.push(entry.name); } catch { /* one broken project must not stop maintenance */ } } return { cleaned, idleHours, checkedAt: new Date().toISOString() }; } export function startCleanupScheduler() { if (schedulerStarted || process.env.NODE_ENV === "test") return; schedulerStarted = true; const timer = setInterval(() => { void cleanupIdleNodeModules(); }, CLEANUP_INTERVAL_MS); timer.unref(); void cleanupIdleNodeModules(); }