// core.ts — Scan orchestrator, report generator, wipe executor. import { getAllScanners, getScanner, registerDiscoveredLocations, type ScanResult, type SearchResult, type WipeResult } from './locations.js'; // ── Types ── export interface WipeAction { location: string; action: 'delete' | 'move' | 'skip'; itemIds: string[]; } export interface WipeReport { results: WipeResult[]; totalDeleted: number; totalFailed: number; totalSkipped: number; } // ── Time Parsing ── export function parseTimeRange(since: string): Date { // ISO timestamp if (since.includes('T') || since.includes('-')) { const d = new Date(since); if (!isNaN(d.getTime())) return d; } // Relative: "30m", "2h", "1d" const match = since.match(/^(\d+)(m|h|d)$/); if (match) { const value = parseInt(match[1], 10); const unit = match[2]; const now = Date.now(); switch (unit) { case 'm': return new Date(now - value * 60 * 1000); case 'h': return new Date(now - value * 60 * 60 * 1000); case 'd': return new Date(now - value * 24 * 60 * 60 * 1000); } } throw new Error(`Invalid time range: "${since}". Use "30m", "2h", "1d", or an ISO timestamp.`); } // ── Scan ── export function scanAll(since: Date, locationFilter?: string[], discover = true): ScanResult[] { // Auto-discover additional storage locations on the system if (discover) { registerDiscoveredLocations(); } const scanners = getAllScanners(); const filtered = locationFilter ? scanners.filter(s => locationFilter.includes(s.id)) : scanners; return filtered.map(scanner => { try { return scanner.scan(since); } catch (err: any) { return { location: scanner.id, itemCount: 0, items: [], canAutoDelete: false, note: `Scan error: ${err.message}`, }; } }); } // ── Search ── export function searchAll(keywords: string[], locationFilter?: string[], discover = true): SearchResult[] { if (discover) registerDiscoveredLocations(); const scanners = getAllScanners(); const filtered = locationFilter ? scanners.filter(s => locationFilter.includes(s.id)) : scanners; return filtered.map(scanner => { try { return scanner.search(keywords); } catch (err: any) { return { location: scanner.id, matchCount: 0, matches: [], note: `Search error: ${err.message}` }; } }); } export function generateSearchReport(results: SearchResult[], keywords: string[]): string { const lines: string[] = [`# Search Report\n`]; lines.push(`**Keywords:** ${keywords.map(k => `"${k}"`).join(', ')}\n`); const totalMatches = results.reduce((sum, r) => sum + r.matchCount, 0); const locationsWithHits = results.filter(r => r.matchCount > 0).length; lines.push(`**${totalMatches} matches** across ${locationsWithHits} of ${results.length} locations.\n`); for (const result of results) { const scanner = getScanner(result.location); const displayName = scanner?.name || result.location; lines.push(`## ${displayName} (${result.matchCount} matches)`); if (result.note) lines.push(`> ${result.note}\n`); if (result.matchCount === 0) { lines.push('No matches.\n'); continue; } for (const match of result.matches) { const ts = match.timestamp ? match.timestamp.replace('T', ' ').slice(0, 19) + ' ' : ''; lines.push(`- \`[${match.id.slice(0, 12)}]\` ${ts}**${match.matchedTerm}**: ${match.context}`); } lines.push(''); } return lines.join('\n'); } // ── Report ── export function generateReport(results: ScanResult[], since: Date): string { const sinceStr = since.toISOString().replace('T', ' ').slice(0, 19); const lines: string[] = [`# Wipe Report: Since ${sinceStr}\n`]; const totalItems = results.reduce((sum, r) => sum + r.itemCount, 0); lines.push(`**${totalItems} items found** across ${results.length} locations.\n`); for (const result of results) { // Use scanner name if available, fall back to location ID const scanner = getScanner(result.location); const displayName = scanner?.name || result.location; lines.push(`## ${displayName} (${result.itemCount} items)`); if (result.note) { lines.push(`> ${result.note}\n`); } if (result.itemCount === 0) { lines.push('Nothing found.\n'); continue; } if (!result.canAutoDelete) { lines.push('**Cannot auto-delete.**\n'); } for (const item of result.items) { const ts = item.timestamp.replace('T', ' ').slice(0, 19); const size = item.size ? ` (${(item.size / 1024).toFixed(1)}KB)` : ''; lines.push(`- \`[${item.id.slice(0, 12)}]\` ${ts}${size} ${item.preview}`); } lines.push(''); } return lines.join('\n'); } // ── Execute ── export async function executeWipe(actions: WipeAction[]): Promise { const results: WipeResult[] = []; let totalDeleted = 0; let totalFailed = 0; let totalSkipped = 0; for (const action of actions) { if (action.action === 'skip') { totalSkipped += action.itemIds.length; results.push({ location: action.location, deleted: 0, failed: 0, skipped: action.itemIds.length, errors: [], }); continue; } const scanner = getScanner(action.location); if (!scanner) { results.push({ location: action.location, deleted: 0, failed: action.itemIds.length, skipped: 0, errors: [`Unknown location: ${action.location}`], }); totalFailed += action.itemIds.length; continue; } try { const result = await scanner.wipe(action.itemIds); results.push(result); totalDeleted += result.deleted; totalFailed += result.failed; totalSkipped += result.skipped; } catch (err: any) { results.push({ location: action.location, deleted: 0, failed: action.itemIds.length, skipped: 0, errors: [err.message], }); totalFailed += action.itemIds.length; } } return { results, totalDeleted, totalFailed, totalSkipped }; } // ── Generate Wipe Execution Summary ── export function generateWipeReport(report: WipeReport): string { const lines: string[] = ['# Wipe Execution Report\n']; lines.push(`**Deleted:** ${report.totalDeleted} | **Failed:** ${report.totalFailed} | **Skipped:** ${report.totalSkipped}\n`); for (const result of report.results) { lines.push(`## ${result.location}`); lines.push(` Deleted: ${result.deleted}, Failed: ${result.failed}, Skipped: ${result.skipped}`); if (result.errors.length > 0) { for (const err of result.errors) { lines.push(` Error: ${err}`); } } lines.push(''); } return lines.join('\n'); }