import { app } from 'electron'; import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, } from 'node:fs'; import { dirname, join } from 'node:path'; const CACHE_SCHEMA_VERSION = 1; const CACHE_FILE_NAME = 'gateway-prelaunch-maintenance-cache.json'; export type PrelaunchMaintenanceTaskName = | 'plugin-maintenance' | 'runtime-deps-cleanup' | 'skills-symlink-cleanup'; export interface PrelaunchMaintenanceRunResult { executed: boolean; reason: 'cache-hit' | 'cache-miss' | 'cache-unavailable' | 'task-failed'; } type CacheKeyInput = string | (() => string); type MaintenanceTask = () => void | boolean; interface CacheEntry { key: string; updatedAt: string; } interface CacheFile { schemaVersion: number; tasks: Partial>; } function getDefaultCachePath(): string { return join(app.getPath('userData'), CACHE_FILE_NAME); } function emptyCache(): CacheFile { return { schemaVersion: CACHE_SCHEMA_VERSION, tasks: {}, }; } function readCache(cachePath: string): CacheFile | null { try { if (!existsSync(cachePath)) return emptyCache(); const parsed = JSON.parse(readFileSync(cachePath, 'utf-8')) as CacheFile; if (parsed.schemaVersion !== CACHE_SCHEMA_VERSION || !parsed.tasks) { return emptyCache(); } return parsed; } catch { return null; } } function writeCache(cachePath: string, cache: CacheFile): boolean { try { mkdirSync(dirname(cachePath), { recursive: true }); writeFileSync(cachePath, `${JSON.stringify(cache, null, 2)}\n`, 'utf-8'); return true; } catch { return false; } } export function stableJson(value: unknown): string { if (value == null || typeof value !== 'object') return JSON.stringify(value); if (Array.isArray(value)) { return `[${value.map((item) => stableJson(item)).join(',')}]`; } const entries = Object.entries(value as Record) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`); return `{${entries.join(',')}}`; } export function pathSignature(path: string): string { try { const stat = statSync(path); return `${stat.isDirectory() ? 'dir' : 'file'}:${Math.round(stat.mtimeMs)}:${stat.size}`; } catch { return 'missing'; } } export function directoryChildrenSignature(path: string, maxEntries = 200): string { try { const entries = readdirSync(path, { withFileTypes: true, encoding: 'utf8' }) .sort((left, right) => left.name.localeCompare(right.name)) .slice(0, maxEntries) .map((entry) => { const childPath = join(path, entry.name); return [ entry.name, entry.isDirectory() ? 'dir' : entry.isSymbolicLink() ? 'symlink' : 'file', pathSignature(childPath), ].join(':'); }); return stableJson(entries); } catch { return 'missing'; } } export function buildPrelaunchMaintenanceCacheKey(parts: Record): string { return stableJson({ schemaVersion: CACHE_SCHEMA_VERSION, ...parts, }); } export function runCachedPrelaunchMaintenanceTask( taskName: PrelaunchMaintenanceTaskName, cacheKey: CacheKeyInput, task: MaintenanceTask, options: { cachePath?: string } = {}, ): PrelaunchMaintenanceRunResult { const readCacheKey = (): string => (typeof cacheKey === 'function' ? cacheKey() : cacheKey); const cachePath = options.cachePath ?? getDefaultCachePath(); const cache = readCache(cachePath); if (!cache) { task(); return { executed: true, reason: 'cache-unavailable' }; } let initialCacheKey: string; try { initialCacheKey = readCacheKey(); } catch { task(); return { executed: true, reason: 'cache-unavailable' }; } if (cache.tasks[taskName]?.key === initialCacheKey) { return { executed: false, reason: 'cache-hit' }; } const taskResult = task(); if (taskResult === false) { return { executed: true, reason: 'task-failed' }; } let finalCacheKey: string; try { finalCacheKey = readCacheKey(); } catch { return { executed: true, reason: 'cache-unavailable' }; } cache.tasks[taskName] = { key: finalCacheKey, updatedAt: new Date().toISOString(), }; writeCache(cachePath, cache); return { executed: true, reason: 'cache-miss' }; }