import fs from 'node:fs'; import path from 'node:path'; import type { CliArgs, CliCommandSource } from '../../cli/args'; import type { LifetimeRebuildSummary, VocabularyCleanupSummary, } from '../../core/services/immersion-tracker/types'; type StatsCliConfig = { immersionTracking?: { enabled?: boolean; }; stats: { serverPort: number; autoOpenBrowser?: boolean; }; }; export type StatsCliCommandResponse = { ok: boolean; url?: string; error?: string; }; export function writeStatsCliCommandResponse( responsePath: string, payload: StatsCliCommandResponse, ): void { fs.mkdirSync(path.dirname(responsePath), { recursive: true }); fs.writeFileSync(responsePath, JSON.stringify(payload, null, 2), 'utf8'); } export function createRunStatsCliCommandHandler(deps: { getResolvedConfig: () => StatsCliConfig; ensureImmersionTrackerStarted: () => void; ensureVocabularyCleanupTokenizerReady?: () => Promise | void; getImmersionTracker: () => { cleanupVocabularyStats?: () => Promise; rebuildLifetimeSummaries?: () => Promise; } | null; ensureStatsServerStarted: () => string; openExternal: (url: string) => Promise; writeResponse: (responsePath: string, payload: StatsCliCommandResponse) => void; exitAppWithCode: (code: number) => void; logInfo: (message: string) => void; logWarn: (message: string, error: unknown) => void; logError: (message: string, error: unknown) => void; }) { const writeResponseSafe = ( responsePath: string | undefined, payload: StatsCliCommandResponse, ): void => { if (!responsePath) return; try { deps.writeResponse(responsePath, payload); } catch (error) { deps.logWarn(`Failed to write stats response: ${responsePath}`, error); } }; return async ( args: Pick< CliArgs, 'statsResponsePath' | 'statsCleanup' | 'statsCleanupVocab' | 'statsCleanupLifetime' >, source: CliCommandSource, ): Promise => { try { const config = deps.getResolvedConfig(); if (config.immersionTracking?.enabled === false) { throw new Error('Immersion tracking is disabled in config.'); } deps.ensureImmersionTrackerStarted(); const tracker = deps.getImmersionTracker(); if (!tracker) { throw new Error('Immersion tracker failed to initialize.'); } if (args.statsCleanup) { const cleanupModes = [ args.statsCleanupVocab ? 'vocab' : null, args.statsCleanupLifetime ? 'lifetime' : null, ].filter(Boolean); if (cleanupModes.length !== 1) { throw new Error('Choose exactly one stats cleanup mode.'); } if (args.statsCleanupVocab) { await deps.ensureVocabularyCleanupTokenizerReady?.(); } if (args.statsCleanupVocab && tracker.cleanupVocabularyStats) { const result = await tracker.cleanupVocabularyStats(); deps.logInfo( `Stats vocabulary cleanup complete: scanned=${result.scanned} kept=${result.kept} deleted=${result.deleted} repaired=${result.repaired}`, ); writeResponseSafe(args.statsResponsePath, { ok: true }); return; } if (!args.statsCleanupLifetime || !tracker.rebuildLifetimeSummaries) { throw new Error('Stats cleanup mode is not available.'); } const result = await tracker.rebuildLifetimeSummaries(); deps.logInfo( `Stats lifetime rebuild complete: appliedSessions=${result.appliedSessions} rebuiltAtMs=${result.rebuiltAtMs}`, ); writeResponseSafe(args.statsResponsePath, { ok: true }); return; } const url = deps.ensureStatsServerStarted(); if (config.stats.autoOpenBrowser !== false) { await deps.openExternal(url); } deps.logInfo(`Stats dashboard available at ${url}`); writeResponseSafe(args.statsResponsePath, { ok: true, url }); } catch (error) { deps.logError('Stats command failed', error); const message = error instanceof Error ? error.message : String(error); writeResponseSafe(args.statsResponsePath, { ok: false, error: message }); if (source === 'initial') { deps.exitAppWithCode(1); } } }; }