import fs from 'fs/promises'; import path from 'path'; import os from 'os'; interface ScanResult { path: string; size: number; lastAccessed: Date; type: 'node_modules' | 'vendor' | 'venv'; } export async function scanDirectory(rootDir: string, maxDepth: number = 5): Promise { const results: ScanResult[] = []; async function traverse(currentPath: string, depth: number) { if (depth > maxDepth) return; try { const entries = await fs.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentPath, entry.name); if (entry.isDirectory()) { if (entry.name === 'node_modules' || entry.name === 'vendor' || entry.name === '.venv') { // Found a target try { const stats = await fs.stat(fullPath); results.push({ path: fullPath, size: 0, // Calculating size is expensive, might do lazily or separate task lastAccessed: stats.atime, type: entry.name as any }); // Don't traverse inside node_modules continue; } catch (e) { console.error(`Error stat-ing ${fullPath}`, e); } } else if (!entry.name.startsWith('.')) { // Recurse normal directories await traverse(fullPath, depth + 1); } } } } catch (error) { console.error(`Error scanning ${currentPath}`, error); } } await traverse(rootDir, 0); return results; } export async function getFolderSize(folderPath: string): Promise { let total = 0; try { const stats = await fs.stat(folderPath); if (stats.isFile()) return stats.size; const files = await fs.readdir(folderPath, { withFileTypes: true }); for (const file of files) { total += await getFolderSize(path.join(folderPath, file.name)); } } catch (e) { // ignore errors } return total; } export interface DeepScanResult { path: string; size: number; isDirectory: boolean; } export async function findLargeFiles(rootDir: string, threshold: number = 100 * 1024 * 1024): Promise { const results: DeepScanResult[] = []; async function traverse(currentPath: string) { try { const stats = await fs.stat(currentPath); if (stats.size > threshold && !stats.isDirectory()) { results.push({ path: currentPath, size: stats.size, isDirectory: false }); return; } if (stats.isDirectory()) { // SKIP node_modules to prevent self-deletion of the running app! if (path.basename(currentPath) === 'node_modules') return; const entries = await fs.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith('.') && entry.name !== '.Trash') continue; await traverse(path.join(currentPath, entry.name)); } } } catch (e) { /* skip */ } } await traverse(rootDir); return results.sort((a, b) => b.size - a.size); } export async function getDeepDiveSummary() { const home = os.homedir(); const targets = [ path.join(home, 'Downloads'), path.join(home, 'Documents'), path.join(home, 'Desktop'), path.join(home, 'Library/Application Support'), ]; const results: DeepScanResult[] = []; for (const t of targets) { console.log(`Scanning ${t}...`); const large = await findLargeFiles(t, 50 * 1024 * 1024); // 50MB+ console.log(`Found ${large.length} large files in ${t}`); results.push(...large); } return results.slice(0, 20); // Top 20 } import { exec } from 'child_process'; import util from 'util'; const execPromise = util.promisify(exec); export async function getDiskUsage() { try { // macOS/Linux: df -k / gives 1K-blocks const { stdout } = await execPromise('df -k /'); const lines = stdout.trim().split('\n'); // Filesystem 1024-blocks Used Available Capacity iused ifree %iused Mounted on // /dev/disk3s1s1 488245288 15266888 308805360 5% 350280 1957260560 0% / if (lines.length < 2) return null; const parts = lines[1].split(/\s+/); // parts[1] is total in 1K blocks // parts[2] is used // parts[3] is available const total = parseInt(parts[1]) * 1024; const used = parseInt(parts[2]) * 1024; const available = parseInt(parts[3]) * 1024; return { totalGB: (total / 1024 / 1024 / 1024).toFixed(2), usedGB: (used / 1024 / 1024 / 1024).toFixed(2), freeGB: (available / 1024 / 1024 / 1024).toFixed(2) }; } catch (e) { console.error("Error getting disk usage:", e); return null; } } export async function findHeavyFolders(rootDir: string): Promise { try { console.log(`Deepest scan on: ${rootDir}`); // du -k -d 2: report size in KB, max depth 2 // sort -nr: numeric reverse sort // head -n 50: top 50 const { stdout } = await execPromise(`du -k -d 2 "${rootDir}" | sort -nr | head -n 50`); const lines = stdout.trim().split('\n'); const results = lines.map(line => { // Trim leading whitespace const trimmed = line.trim(); // Split by first whitespace only const firstSpace = trimmed.indexOf('\t'); // du output is usually tab separated or space // Actually du output on mac is "sizepath" // Robust splitting for size and path const match = trimmed.match(/^(\d+)\s+(.+)$/); if (!match) return null; const sizeK = parseInt(match[1]); const fullPath = match[2]; return { path: fullPath, size: sizeK * 1024, // Convert KB to Bytes isDirectory: true }; }).filter(item => item !== null && item.path !== rootDir) as DeepScanResult[]; return results; } catch (e) { console.error("Deepest scan failed:", e); return []; } }