aris-mac-cleaner 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +63 -28
  2. package/bin/cli.js +1373 -209
  3. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -1,34 +1,258 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // ╔══════════════════════════════════════════════════════════════════════════════╗
4
+ // ║ ARIS MAC CLEANER v3.0 ║
5
+ // ║ por Salvador Reis ║
6
+ // ╚══════════════════════════════════════════════════════════════════════════════╝
7
+
3
8
  const { execSync, spawnSync } = require('child_process');
4
9
  const os = require('os');
5
10
  const fs = require('fs');
6
11
  const path = require('path');
12
+ const readline = require('readline');
13
+ const crypto = require('crypto');
14
+
15
+ const VERSION = '3.0';
16
+ const HOME = os.homedir();
17
+ const CONFIG_DIR = path.join(HOME, '.aris-mac-cleaner');
18
+ const HISTORY_FILE = path.join(CONFIG_DIR, 'history.log');
19
+ const EXCLUSIONS_FILE = path.join(CONFIG_DIR, 'exclusions.conf');
20
+ const CUSTOM_PATHS_FILE = path.join(CONFIG_DIR, 'custom-paths.conf');
21
+ const LANGUAGE_FILE = path.join(CONFIG_DIR, 'language');
22
+
23
+ // Create config directory
24
+ if (!fs.existsSync(CONFIG_DIR)) {
25
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
26
+ }
27
+
28
+ // ══════════════════════════════════════════════════════════════════════════════
29
+ // COLORS AND SYMBOLS
30
+ // ══════════════════════════════════════════════════════════════════════════════
7
31
 
8
- // Cores
9
- const colors = {
32
+ const c = {
10
33
  reset: '\x1b[0m',
11
- red: '\x1b[31m',
12
- green: '\x1b[32m',
13
- yellow: '\x1b[33m',
14
- blue: '\x1b[34m',
15
- cyan: '\x1b[36m',
16
- white: '\x1b[37m',
17
- bold: '\x1b[1m',
18
- dim: '\x1b[2m'
34
+ red: '\x1b[0;31m',
35
+ green: '\x1b[0;32m',
36
+ yellow: '\x1b[1;33m',
37
+ blue: '\x1b[0;34m',
38
+ cyan: '\x1b[0;36m',
39
+ white: '\x1b[1;37m',
40
+ dim: '\x1b[2m',
41
+ bold: '\x1b[1m'
19
42
  };
20
43
 
21
- const c = colors;
44
+ const CHECK = '✓';
45
+ const CROSS = '✗';
46
+ const ARROW = '→';
47
+ const BULLET = '•';
22
48
 
23
- // Verificar se é macOS
24
- if (os.platform() !== 'darwin') {
25
- console.log(`${c.red}Este programa só funciona em macOS${c.reset}`);
26
- process.exit(1);
49
+ // ══════════════════════════════════════════════════════════════════════════════
50
+ // MULTI-LANGUAGE
51
+ // ══════════════════════════════════════════════════════════════════════════════
52
+
53
+ let LANG_CODE = 'pt';
54
+ if (fs.existsSync(LANGUAGE_FILE)) {
55
+ LANG_CODE = fs.readFileSync(LANGUAGE_FILE, 'utf8').trim();
27
56
  }
28
57
 
29
- const HOME = os.homedir();
58
+ const MSG = LANG_CODE === 'pt' ? {
59
+ WELCOME: 'Bem-vindo ao Aris Mac Cleaner',
60
+ MENU_TITLE: 'MENU PRINCIPAL',
61
+ OPT_QUICK: 'Limpeza Rápida (caches e temporários)',
62
+ OPT_LARGE: 'Encontrar Ficheiros Grandes (>100MB)',
63
+ OPT_DUPLICATES: 'Encontrar Duplicados',
64
+ OPT_APPS: 'Desinstalar Aplicações',
65
+ OPT_STARTUP: 'Gerir Apps de Arranque',
66
+ OPT_BROWSERS: 'Limpar Browsers',
67
+ OPT_PROCESSES: 'Matar Processos Pesados',
68
+ OPT_STATS: 'Ver Estatísticas',
69
+ OPT_SETTINGS: 'Configurações',
70
+ OPT_EXIT: 'Sair',
71
+ CHOOSE: 'Escolha',
72
+ INVALID: 'Opção inválida',
73
+ PRESS_ENTER: 'Pressiona ENTER para continuar',
74
+ SCANNING: 'A analisar...',
75
+ FOUND: 'Encontrado',
76
+ TOTAL: 'Total',
77
+ DELETE: 'Apagar',
78
+ KEEP: 'Manter',
79
+ CANCEL: 'Cancelar',
80
+ CONFIRM: 'Confirmar',
81
+ FREED: 'Libertado',
82
+ AVAILABLE: 'Disponível',
83
+ HEALTH_SCORE: 'Health Score',
84
+ EXCELLENT: 'Excelente',
85
+ GOOD: 'Bom',
86
+ ATTENTION: 'Atenção',
87
+ CRITICAL: 'Crítico',
88
+ DIAGNOSE: 'DIAGNÓSTICO DO SISTEMA',
89
+ DISK: 'Disco',
90
+ RAM: 'RAM',
91
+ SWAP: 'Swap',
92
+ CPU: 'CPU',
93
+ IN_USE: 'em uso',
94
+ FREE_OF: 'livres de',
95
+ SELECT_DELETE: 'Seleciona para apagar',
96
+ SELECT_KEEP: 'Seleciona para manter',
97
+ ALL: 'todos',
98
+ NONE: 'nenhum',
99
+ BACK: 'Voltar',
100
+ FINAL_REPORT: 'RELATÓRIO FINAL',
101
+ READY: 'Pronto para trabalhar!',
102
+ RESTART_REC: 'Recomendação: Reinicia o Mac para limpar memória',
103
+ NO_ITEMS: 'Nada encontrado',
104
+ CLEANING: 'A limpar...',
105
+ CATEGORIES: 'CATEGORIAS',
106
+ SYSTEM_CACHES: 'Caches do Sistema',
107
+ DEV_TOOLS: 'Ferramentas de Dev',
108
+ BROWSERS_CAT: 'Browsers',
109
+ DOWNLOADS_CAT: 'Downloads',
110
+ APPS_CAT: 'Aplicações',
111
+ PROCESS_MEM: 'Por Memória',
112
+ PROCESS_CPU: 'Por CPU',
113
+ KILL: 'Matar',
114
+ STARTUP_ITEMS: 'Items de Arranque',
115
+ LAUNCH_AGENTS: 'Launch Agents',
116
+ ENABLED: 'Ativo',
117
+ DISABLED: 'Desativado',
118
+ TOGGLE: 'Alternar',
119
+ DISABLE_ALL: 'Desativar todos',
120
+ THIS_MONTH: 'Este mês',
121
+ CLEANINGS: 'Limpezas',
122
+ AVG_SCORE: 'Score médio',
123
+ HISTORY: 'Histórico',
124
+ EXCLUSIONS: 'Gerir Exclusões',
125
+ CUSTOM_PATHS: 'Caminhos Personalizados',
126
+ CHANGE_LANG: 'Mudar Idioma',
127
+ RESET_DEFAULTS: 'Repor Predefinições',
128
+ PREVIEW_MODE: 'Modo Preview (não apaga nada)',
129
+ LARGE_FILES: 'FICHEIROS GRANDES',
130
+ DUPLICATES: 'FICHEIROS DUPLICADOS',
131
+ UNINSTALL: 'DESINSTALAR APLICAÇÕES',
132
+ APP_SIZE: 'app',
133
+ DATA_SIZE: 'dados',
134
+ CURRENT_EXCLUSIONS: 'Exclusões atuais',
135
+ CURRENT_PATHS: 'Caminhos personalizados atuais',
136
+ ENTER_PATH: 'Introduz caminho (c=limpar, n=voltar)',
137
+ PATH: 'Caminho',
138
+ CLEARED: 'Limpo',
139
+ ADDED: 'Adicionado',
140
+ LANG_CHANGED_EN: 'Language changed to English',
141
+ LANG_CHANGED_PT: 'Idioma alterado para Português',
142
+ SETTINGS_RESET: 'Configurações repostas',
143
+ REMOVED: 'Removido',
144
+ NEED_SUDO: 'Precisa sudo para remover app. Executa:',
145
+ DELETING: 'A apagar',
146
+ KILLED: 'Morto',
147
+ FAILED_KILL: 'Falhou ao matar',
148
+ NO_HISTORY: 'Sem histórico. Faz algumas limpezas primeiro!',
149
+ FEATURE_PROGRESS: 'Funcionalidade em progresso...',
150
+ GROUP: 'Grupo',
151
+ EACH: 'cada',
152
+ IN_DUPLICATES: 'em duplicados',
153
+ HASHING: 'A calcular hashes...'
154
+ } : {
155
+ WELCOME: 'Welcome to Aris Mac Cleaner',
156
+ MENU_TITLE: 'MAIN MENU',
157
+ OPT_QUICK: 'Quick Clean (caches and temp files)',
158
+ OPT_LARGE: 'Find Large Files (>100MB)',
159
+ OPT_DUPLICATES: 'Find Duplicates',
160
+ OPT_APPS: 'Uninstall Applications',
161
+ OPT_STARTUP: 'Manage Startup Apps',
162
+ OPT_BROWSERS: 'Clean Browsers',
163
+ OPT_PROCESSES: 'Kill Heavy Processes',
164
+ OPT_STATS: 'View Statistics',
165
+ OPT_SETTINGS: 'Settings',
166
+ OPT_EXIT: 'Exit',
167
+ CHOOSE: 'Choose',
168
+ INVALID: 'Invalid option',
169
+ PRESS_ENTER: 'Press ENTER to continue',
170
+ SCANNING: 'Scanning...',
171
+ FOUND: 'Found',
172
+ TOTAL: 'Total',
173
+ DELETE: 'Delete',
174
+ KEEP: 'Keep',
175
+ CANCEL: 'Cancel',
176
+ CONFIRM: 'Confirm',
177
+ FREED: 'Freed',
178
+ AVAILABLE: 'Available',
179
+ HEALTH_SCORE: 'Health Score',
180
+ EXCELLENT: 'Excellent',
181
+ GOOD: 'Good',
182
+ ATTENTION: 'Warning',
183
+ CRITICAL: 'Critical',
184
+ DIAGNOSE: 'SYSTEM DIAGNOSTICS',
185
+ DISK: 'Disk',
186
+ RAM: 'RAM',
187
+ SWAP: 'Swap',
188
+ CPU: 'CPU',
189
+ IN_USE: 'in use',
190
+ FREE_OF: 'free of',
191
+ SELECT_DELETE: 'Select to delete',
192
+ SELECT_KEEP: 'Select to keep',
193
+ ALL: 'all',
194
+ NONE: 'none',
195
+ BACK: 'Back',
196
+ FINAL_REPORT: 'FINAL REPORT',
197
+ READY: 'Ready to work!',
198
+ RESTART_REC: 'Recommendation: Restart Mac to clear memory',
199
+ NO_ITEMS: 'Nothing found',
200
+ CLEANING: 'Cleaning...',
201
+ CATEGORIES: 'CATEGORIES',
202
+ SYSTEM_CACHES: 'System Caches',
203
+ DEV_TOOLS: 'Dev Tools',
204
+ BROWSERS_CAT: 'Browsers',
205
+ DOWNLOADS_CAT: 'Downloads',
206
+ APPS_CAT: 'Applications',
207
+ PROCESS_MEM: 'By Memory',
208
+ PROCESS_CPU: 'By CPU',
209
+ KILL: 'Kill',
210
+ STARTUP_ITEMS: 'Login Items',
211
+ LAUNCH_AGENTS: 'Launch Agents',
212
+ ENABLED: 'Enabled',
213
+ DISABLED: 'Disabled',
214
+ TOGGLE: 'Toggle',
215
+ DISABLE_ALL: 'Disable all',
216
+ THIS_MONTH: 'This month',
217
+ CLEANINGS: 'Cleanings',
218
+ AVG_SCORE: 'Avg score',
219
+ HISTORY: 'History',
220
+ EXCLUSIONS: 'Manage Exclusions',
221
+ CUSTOM_PATHS: 'Custom Paths',
222
+ CHANGE_LANG: 'Change Language',
223
+ RESET_DEFAULTS: 'Reset Defaults',
224
+ PREVIEW_MODE: 'Preview Mode (doesn\'t delete)',
225
+ LARGE_FILES: 'LARGE FILES',
226
+ DUPLICATES: 'DUPLICATE FILES',
227
+ UNINSTALL: 'UNINSTALL APPLICATIONS',
228
+ APP_SIZE: 'app',
229
+ DATA_SIZE: 'data',
230
+ CURRENT_EXCLUSIONS: 'Current exclusions',
231
+ CURRENT_PATHS: 'Current custom paths',
232
+ ENTER_PATH: 'Enter path (c=clear, n=back)',
233
+ PATH: 'Path',
234
+ CLEARED: 'Cleared',
235
+ ADDED: 'Added',
236
+ LANG_CHANGED_EN: 'Language changed to English',
237
+ LANG_CHANGED_PT: 'Idioma alterado para Português',
238
+ SETTINGS_RESET: 'Settings reset to defaults',
239
+ REMOVED: 'Removed',
240
+ NEED_SUDO: 'Need sudo to remove app. Run:',
241
+ DELETING: 'Deleting',
242
+ KILLED: 'Killed',
243
+ FAILED_KILL: 'Failed to kill',
244
+ NO_HISTORY: 'No history yet. Run some cleanups first!',
245
+ FEATURE_PROGRESS: 'Feature in progress...',
246
+ GROUP: 'Group',
247
+ EACH: 'each',
248
+ IN_DUPLICATES: 'in duplicates',
249
+ HASHING: 'Calculating hashes...'
250
+ };
251
+
252
+ // ══════════════════════════════════════════════════════════════════════════════
253
+ // UTILITY FUNCTIONS
254
+ // ══════════════════════════════════════════════════════════════════════════════
30
255
 
31
- // Função para executar comandos
32
256
  function exec(cmd) {
33
257
  try {
34
258
  return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
@@ -37,211 +261,1151 @@ function exec(cmd) {
37
261
  }
38
262
  }
39
263
 
40
- // Função para obter tamanho de pasta
41
- function getSize(path) {
264
+ function getSize(p) {
265
+ const fullPath = p.replace(/^~/, HOME);
42
266
  try {
43
- const result = exec(`du -sm "${path}" 2>/dev/null`);
267
+ if (!fs.existsSync(fullPath)) return 0;
268
+ const result = exec(`du -sm "${fullPath}" 2>/dev/null`);
44
269
  return parseInt(result.split('\t')[0]) || 0;
45
270
  } catch (e) {
46
271
  return 0;
47
272
  }
48
273
  }
49
274
 
50
- // Função para limpar pasta
51
- function cleanPath(p, name) {
52
- const fullPath = p.replace('~', HOME);
53
- if (fs.existsSync(fullPath)) {
275
+ function formatSize(sizeMB) {
276
+ if (sizeMB >= 1024) {
277
+ return `${(sizeMB / 1024).toFixed(1)} GB`;
278
+ }
279
+ return `${sizeMB} MB`;
280
+ }
281
+
282
+ function isExcluded(p) {
283
+ if (!fs.existsSync(EXCLUSIONS_FILE)) return false;
284
+ const exclusions = fs.readFileSync(EXCLUSIONS_FILE, 'utf8').split('\n').filter(l => l.trim());
285
+ return exclusions.some(ex => p.includes(ex));
286
+ }
287
+
288
+ function logCleanup(freed, score) {
289
+ const line = `${new Date().toISOString().slice(0, 16).replace('T', ' ')} | ${MSG.FREED}: ${freed} MB | Score: ${score}/100\n`;
290
+ fs.appendFileSync(HISTORY_FILE, line);
291
+ }
292
+
293
+ function clearScreen() {
294
+ process.stdout.write('\x1b[2J\x1b[H');
295
+ }
296
+
297
+ function header() {
298
+ clearScreen();
299
+ const date = new Date().toLocaleDateString('en-GB', {
300
+ weekday: 'long', day: '2-digit', month: 'long', year: 'numeric'
301
+ });
302
+ const time = new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
303
+
304
+ console.log();
305
+ console.log(`${c.cyan}╔══════════════════════════════════════════════════════════════════╗${c.reset}`);
306
+ console.log(`${c.cyan}║${c.reset}${c.white} ARIS MAC CLEANER v${VERSION} ${c.reset}${c.cyan}║${c.reset}`);
307
+ console.log(`${c.cyan}║${c.reset}${c.dim} ${date} ${BULLET} ${time} ${c.reset}${c.cyan}║${c.reset}`);
308
+ console.log(`${c.cyan}╚══════════════════════════════════════════════════════════════════╝${c.reset}`);
309
+ console.log();
310
+ }
311
+
312
+ function sectionHeader(title) {
313
+ console.log(`${c.white}┌─────────────────────────────────────────────────────────────────┐${c.reset}`);
314
+ console.log(`${c.white}│${c.reset} ${c.blue}▶ ${title}${c.reset}`);
315
+ console.log(`${c.white}└─────────────────────────────────────────────────────────────────┘${c.reset}`);
316
+ console.log();
317
+ }
318
+
319
+ function prompt(question) {
320
+ return new Promise((resolve) => {
321
+ const rl = readline.createInterface({
322
+ input: process.stdin,
323
+ output: process.stdout
324
+ });
325
+ rl.question(question, (answer) => {
326
+ rl.close();
327
+ resolve(answer.trim());
328
+ });
329
+ });
330
+ }
331
+
332
+ async function pressEnter() {
333
+ console.log();
334
+ await prompt(` ${c.dim}${MSG.PRESS_ENTER}...${c.reset} `);
335
+ }
336
+
337
+ // ══════════════════════════════════════════════════════════════════════════════
338
+ // SYSTEM DIAGNOSTICS
339
+ // ══════════════════════════════════════════════════════════════════════════════
340
+
341
+ let systemStats = {};
342
+
343
+ function getSystemStats() {
344
+ // Disk
345
+ const diskInfo = exec("df -h / | tail -1").split(/\s+/);
346
+ const diskRaw = exec("df / | tail -1").split(/\s+/);
347
+ systemStats.diskTotal = diskInfo[1]?.replace('Gi', '') || '0';
348
+ systemStats.diskFree = diskInfo[3]?.replace('Gi', '') || '0';
349
+ systemStats.diskPercent = parseInt(diskInfo[4]) || 0;
350
+ systemStats.diskFreeBlocks = parseInt(diskRaw[3]) || 0;
351
+
352
+ if (systemStats.diskPercent < 50) {
353
+ systemStats.diskStatus = `${c.green}${CHECK} ${MSG.EXCELLENT}${c.reset}`;
354
+ } else if (systemStats.diskPercent < 80) {
355
+ systemStats.diskStatus = `${c.yellow}! ${MSG.ATTENTION}${c.reset}`;
356
+ } else {
357
+ systemStats.diskStatus = `${c.red}${CROSS} ${MSG.CRITICAL}${c.reset}`;
358
+ }
359
+
360
+ // RAM
361
+ const ramFreePercent = parseInt(exec("memory_pressure 2>/dev/null | grep 'System-wide' | awk '{print $5}'")) || 50;
362
+ systemStats.ramUsedPercent = 100 - ramFreePercent;
363
+
364
+ if (systemStats.ramUsedPercent < 70) {
365
+ systemStats.ramStatus = `${c.green}${CHECK} OK${c.reset}`;
366
+ } else if (systemStats.ramUsedPercent < 90) {
367
+ systemStats.ramStatus = `${c.yellow}! ${MSG.ATTENTION}${c.reset}`;
368
+ } else {
369
+ systemStats.ramStatus = `${c.red}${CROSS} ${MSG.CRITICAL}${c.reset}`;
370
+ }
371
+
372
+ // Swap
373
+ const swapStr = exec("sysctl vm.swapusage 2>/dev/null | awk '{print $7}'");
374
+ systemStats.swapUsed = parseInt(swapStr) || 0;
375
+
376
+ if (systemStats.swapUsed < 2000) {
377
+ systemStats.swapStatus = `${c.green}${CHECK} OK${c.reset}`;
378
+ } else if (systemStats.swapUsed < 8000) {
379
+ systemStats.swapStatus = `${c.yellow}! Alto${c.reset}`;
380
+ } else {
381
+ systemStats.swapStatus = `${c.red}${CROSS} ${MSG.CRITICAL}${c.reset}`;
382
+ }
383
+
384
+ // CPU
385
+ const cpuIdle = parseFloat(exec("top -l 1 2>/dev/null | grep 'CPU usage' | awk '{print $7}'")) || 50;
386
+ systemStats.cpuUsed = Math.round(100 - cpuIdle);
387
+
388
+ if (systemStats.cpuUsed < 50) {
389
+ systemStats.cpuStatus = `${c.green}${CHECK} OK${c.reset}`;
390
+ } else if (systemStats.cpuUsed < 80) {
391
+ systemStats.cpuStatus = `${c.yellow}! ${MSG.ATTENTION}${c.reset}`;
392
+ } else {
393
+ systemStats.cpuStatus = `${c.red}${CROSS} ${MSG.CRITICAL}${c.reset}`;
394
+ }
395
+ }
396
+
397
+ function showDiagnostics() {
398
+ sectionHeader(MSG.DIAGNOSE);
399
+ console.log(` ${BULLET} ${c.white}${MSG.DISK}${c.reset} ${systemStats.diskFree}GB ${MSG.FREE_OF} ${systemStats.diskTotal}GB ${systemStats.diskStatus}`);
400
+ console.log(` ${BULLET} ${c.white}${MSG.RAM}${c.reset} ${systemStats.ramUsedPercent}% ${MSG.IN_USE} ${systemStats.ramStatus}`);
401
+ console.log(` ${BULLET} ${c.white}${MSG.SWAP}${c.reset} ${systemStats.swapUsed}MB ${MSG.IN_USE} ${systemStats.swapStatus}`);
402
+ console.log(` ${BULLET} ${c.white}${MSG.CPU}${c.reset} ${systemStats.cpuUsed}% ${MSG.IN_USE} ${systemStats.cpuStatus}`);
403
+ console.log();
404
+ }
405
+
406
+ function calculateHealthScore() {
407
+ let score = 100;
408
+ if (systemStats.diskPercent > 80) score -= 30;
409
+ else if (systemStats.diskPercent > 60) score -= 10;
410
+ if (systemStats.ramUsedPercent > 90) score -= 30;
411
+ else if (systemStats.ramUsedPercent > 70) score -= 10;
412
+ if (systemStats.swapUsed > 8000) score -= 20;
413
+ else if (systemStats.swapUsed > 4000) score -= 10;
414
+
415
+ let color = c.green;
416
+ let text = MSG.EXCELLENT;
417
+ if (score < 60) { color = c.red; text = MSG.ATTENTION; }
418
+ else if (score < 80) { color = c.yellow; text = MSG.GOOD; }
419
+
420
+ return { score, color, text };
421
+ }
422
+
423
+ function showFinalReport(freed) {
424
+ const diskFreeAfter = exec("df -h / | tail -1").split(/\s+/)[3];
425
+ const health = calculateHealthScore();
426
+
427
+ console.log(`${c.cyan}╔══════════════════════════════════════════════════════════════════╗${c.reset}`);
428
+ console.log(`${c.cyan}║${c.reset}${c.white} ${MSG.FINAL_REPORT} ${c.reset}${c.cyan}║${c.reset}`);
429
+ console.log(`${c.cyan}╠══════════════════════════════════════════════════════════════════╣${c.reset}`);
430
+ console.log(`${c.cyan}║${c.reset} ${c.cyan}║${c.reset}`);
431
+ console.log(`${c.cyan}║${c.reset} ${c.white}${MSG.DISK}${c.reset} ${c.cyan}║${c.reset}`);
432
+ console.log(`${c.cyan}║${c.reset} ${ARROW} ${MSG.AVAILABLE}: ${c.green}${diskFreeAfter}${c.reset} ${c.cyan}║${c.reset}`);
433
+ if (freed > 0) {
434
+ console.log(`${c.cyan}║${c.reset} ${ARROW} ${MSG.FREED}: ${c.green}+${formatSize(freed)}${c.reset} ${c.cyan}║${c.reset}`);
435
+ }
436
+ console.log(`${c.cyan}║${c.reset} ${c.cyan}║${c.reset}`);
437
+ console.log(`${c.cyan}║${c.reset} ${c.white}${MSG.HEALTH_SCORE}${c.reset} ${c.cyan}║${c.reset}`);
438
+ console.log(`${c.cyan}║${c.reset} ${ARROW} ${health.color}${health.score}/100 - ${health.text}${c.reset} ${c.cyan}║${c.reset}`);
439
+ console.log(`${c.cyan}║${c.reset} ${c.cyan}║${c.reset}`);
440
+ console.log(`${c.cyan}╚══════════════════════════════════════════════════════════════════╝${c.reset}`);
441
+ console.log();
442
+
443
+ if (systemStats.swapUsed > 5000 || systemStats.ramUsedPercent > 85) {
444
+ console.log(`${c.yellow}⚠ ${MSG.RESTART_REC}${c.reset}`);
445
+ console.log();
446
+ }
447
+
448
+ console.log(`${c.green}${CHECK} ${MSG.READY}${c.reset}`);
449
+
450
+ logCleanup(freed, health.score);
451
+ }
452
+
453
+ // ══════════════════════════════════════════════════════════════════════════════
454
+ // OPTION 1: QUICK CLEAN
455
+ // ══════════════════════════════════════════════════════════════════════════════
456
+
457
+ async function quickClean() {
458
+ header();
459
+ getSystemStats();
460
+ showDiagnostics();
461
+ sectionHeader(MSG.CATEGORIES);
462
+
463
+ const items = [];
464
+ let totalSize = 0;
465
+
466
+ function addItem(p, name, category) {
467
+ const fullPath = p.replace(/^~/, HOME);
468
+ if (isExcluded(fullPath)) return;
469
+
470
+ const size = getSize(p);
471
+ if (size > 0) {
472
+ items.push({ path: fullPath, name, category, size, selected: true });
473
+ totalSize += size;
474
+ }
475
+ }
476
+
477
+ // System Caches
478
+ addItem('~/Library/Caches/Google', 'Google/Chrome Cache', 'system');
479
+ addItem('~/Library/Caches/com.spotify.client', 'Spotify Cache', 'system');
480
+ addItem('~/Library/Caches/com.apple.Safari', 'Safari Cache', 'system');
481
+ addItem('~/Library/Logs', 'System Logs', 'system');
482
+
483
+ // Dev Tools
484
+ addItem('~/Library/Caches/CocoaPods', 'CocoaPods Cache', 'dev');
485
+ addItem('~/Library/Caches/Homebrew', 'Homebrew Cache', 'dev');
486
+ addItem('~/Library/Caches/typescript', 'TypeScript Cache', 'dev');
487
+ addItem('~/Library/Caches/node-gyp', 'Node-gyp Cache', 'dev');
488
+ addItem('~/Library/Caches/pip', 'Pip Cache', 'dev');
489
+ addItem('~/Library/Caches/yarn', 'Yarn Cache', 'dev');
490
+ addItem('~/Library/Caches/pnpm', 'pnpm Cache', 'dev');
491
+ addItem('~/.npm', 'npm Cache', 'dev');
492
+ addItem('~/Library/Application Support/Code/User/workspaceStorage', 'VS Code Workspace', 'dev');
493
+ addItem('~/Library/Developer/Xcode/DerivedData', 'Xcode DerivedData', 'dev');
494
+
495
+ // Browsers
496
+ addItem('~/Library/Application Support/Google/GoogleUpdater/crx_cache', 'Google Updater', 'browsers');
497
+ addItem('~/Library/Application Support/Google/Chrome/component_crx_cache', 'Chrome Components', 'browsers');
498
+
499
+ // Claude versions
500
+ const claudeVersionsPath = path.join(HOME, '.local/share/claude/versions');
501
+ if (fs.existsSync(claudeVersionsPath)) {
54
502
  try {
55
- execSync(`rm -rf "${fullPath}"`, { stdio: 'ignore' });
56
- console.log(` ${c.green}✓${c.reset} ${name}`);
57
- return true;
58
- } catch (e) {
59
- return false;
503
+ const versions = fs.readdirSync(claudeVersionsPath).sort().reverse();
504
+ if (versions.length > 1) {
505
+ let claudeSize = 0;
506
+ versions.slice(1).forEach(v => {
507
+ claudeSize += getSize(path.join(claudeVersionsPath, v));
508
+ });
509
+ if (claudeSize > 0) {
510
+ items.push({
511
+ path: 'CLAUDE_VERSIONS',
512
+ name: `Claude (${versions.length - 1} old versions)`,
513
+ category: 'apps',
514
+ size: claudeSize,
515
+ selected: true
516
+ });
517
+ totalSize += claudeSize;
518
+ }
519
+ }
520
+ } catch (e) {}
521
+ }
522
+
523
+ // Old Downloads
524
+ const dmgCount = parseInt(exec('find ~/Downloads -name "*.dmg" -mtime +7 2>/dev/null | wc -l')) || 0;
525
+ const zipCount = parseInt(exec('find ~/Downloads -name "*.zip" -mtime +30 2>/dev/null | wc -l')) || 0;
526
+ if (dmgCount > 0 || zipCount > 0) {
527
+ const dlSize = parseInt(exec('find ~/Downloads \\( -name "*.dmg" -mtime +7 -o -name "*.zip" -mtime +30 \\) -exec du -sm {} \\; 2>/dev/null | awk \'{sum+=$1} END {print sum}\'')) || 0;
528
+ if (dlSize > 0) {
529
+ items.push({
530
+ path: 'OLD_DOWNLOADS',
531
+ name: `Old Downloads (${dmgCount} dmg, ${zipCount} zip)`,
532
+ category: 'downloads',
533
+ size: dlSize,
534
+ selected: true
535
+ });
536
+ totalSize += dlSize;
60
537
  }
61
538
  }
62
- return false;
63
- }
64
-
65
- // Limpar consola
66
- console.clear();
67
-
68
- // Header
69
- console.log();
70
- console.log(`${c.cyan}╔══════════════════════════════════════════════════════════════════╗${c.reset}`);
71
- console.log(`${c.cyan}║${c.reset}${c.bold}${c.white} ARIS MAC CLEANER v2.0 ${c.reset}${c.cyan}║${c.reset}`);
72
- console.log(`${c.cyan}║${c.reset}${c.dim} por Salvador Reis ${c.reset}${c.cyan}║${c.reset}`);
73
- console.log(`${c.cyan}╚══════════════════════════════════════════════════════════════════╝${c.reset}`);
74
- console.log();
75
-
76
- // FASE 1: DIAGNÓSTICO
77
- console.log(`${c.white}┌─────────────────────────────────────────────────────────────────┐${c.reset}`);
78
- console.log(`${c.white}│${c.reset} ${c.blue}▶ FASE 1: DIAGNÓSTICO DO SISTEMA${c.reset} ${c.white}│${c.reset}`);
79
- console.log(`${c.white}└─────────────────────────────────────────────────────────────────┘${c.reset}`);
80
- console.log();
81
-
82
- // Disco
83
- const diskInfo = exec("df -h / | tail -1").split(/\s+/);
84
- const diskTotal = diskInfo[1];
85
- const diskUsed = diskInfo[2];
86
- const diskFree = diskInfo[3];
87
- const diskPercent = parseInt(diskInfo[4]);
88
- const diskFreeBefore = parseInt(exec("df / | tail -1").split(/\s+/)[3]);
89
-
90
- let diskStatus = `${c.green}✓ Excelente${c.reset}`;
91
- if (diskPercent >= 80) diskStatus = `${c.red}✗ Crítico${c.reset}`;
92
- else if (diskPercent >= 50) diskStatus = `${c.yellow}! Atenção${c.reset}`;
93
-
94
- // RAM
95
- const ramFreePercent = parseInt(exec("memory_pressure 2>/dev/null | grep 'System-wide' | awk '{print $5}'")) || 50;
96
- const ramUsedPercent = 100 - ramFreePercent;
97
-
98
- let ramStatus = `${c.green}✓ OK${c.reset}`;
99
- if (ramUsedPercent >= 90) ramStatus = `${c.red} Crítico${c.reset}`;
100
- else if (ramUsedPercent >= 70) ramStatus = `${c.yellow}! Pressão${c.reset}`;
101
-
102
- // Swap
103
- const swapUsed = parseInt(exec("sysctl vm.swapusage 2>/dev/null | awk '{print int($7)}'")) || 0;
104
-
105
- let swapStatus = `${c.green}✓ OK${c.reset}`;
106
- if (swapUsed >= 8000) swapStatus = `${c.red}✗ Crítico${c.reset}`;
107
- else if (swapUsed >= 2000) swapStatus = `${c.yellow}! Alto${c.reset}`;
108
-
109
- // CPU
110
- const cpuIdle = parseFloat(exec("top -l 1 | grep 'CPU usage' | awk '{print $7}'")) || 50;
111
- const cpuUsed = Math.round(100 - cpuIdle);
112
-
113
- let cpuStatus = `${c.green}✓ OK${c.reset}`;
114
- if (cpuUsed >= 80) cpuStatus = `${c.red}✗ Sobrecarregado${c.reset}`;
115
- else if (cpuUsed >= 50) cpuStatus = `${c.yellow}! Ocupado${c.reset}`;
116
-
117
- console.log(` • ${c.bold}Disco${c.reset} ${diskFree} livres de ${diskTotal} ${diskStatus}`);
118
- console.log(` • ${c.bold}RAM${c.reset} ${ramUsedPercent}% em uso ${ramStatus}`);
119
- console.log(` • ${c.bold}Swap${c.reset} ${swapUsed}MB em uso ${swapStatus}`);
120
- console.log(` • ${c.bold}CPU${c.reset} ${cpuUsed}% em uso ${cpuStatus}`);
121
- console.log();
122
-
123
- // FASE 2: ANÁLISE
124
- console.log(`${c.white}┌─────────────────────────────────────────────────────────────────┐${c.reset}`);
125
- console.log(`${c.white}│${c.reset} ${c.blue}▶ FASE 2: ANÁLISE DE LIMPEZA${c.reset} ${c.white}│${c.reset}`);
126
- console.log(`${c.white}└─────────────────────────────────────────────────────────────────┘${c.reset}`);
127
- console.log();
128
-
129
- const cleanPaths = [
130
- { path: `${HOME}/Library/Caches/Google`, name: 'Google Cache' },
131
- { path: `${HOME}/Library/Caches/com.spotify.client`, name: 'Spotify Cache' },
132
- { path: `${HOME}/Library/Caches/CocoaPods`, name: 'CocoaPods Cache' },
133
- { path: `${HOME}/Library/Caches/Homebrew`, name: 'Homebrew Cache' },
134
- { path: `${HOME}/Library/Caches/typescript`, name: 'TypeScript Cache' },
135
- { path: `${HOME}/Library/Caches/node-gyp`, name: 'Node-gyp Cache' },
136
- { path: `${HOME}/Library/Caches/pip`, name: 'Pip Cache' },
137
- { path: `${HOME}/Library/Caches/yarn`, name: 'Yarn Cache' },
138
- { path: `${HOME}/Library/Caches/pnpm`, name: 'pnpm Cache' },
139
- { path: `${HOME}/.npm`, name: 'npm Cache' },
140
- { path: `${HOME}/Library/Application Support/Code/User/workspaceStorage`, name: 'VS Code Storage' },
141
- { path: `${HOME}/Library/Developer/Xcode/DerivedData`, name: 'Xcode DerivedData' },
142
- { path: `${HOME}/Library/Application Support/Google/GoogleUpdater/crx_cache`, name: 'Google Updater' },
143
- { path: `${HOME}/Library/Application Support/Google/Chrome/component_crx_cache`, name: 'Chrome Components' },
144
- ];
145
-
146
- let totalClean = 0;
147
-
148
- cleanPaths.forEach(item => {
149
- const size = getSize(item.path);
150
- if (size > 0) {
151
- totalClean += size;
152
- console.log(` • ${item.name.padEnd(25)} ${c.dim}${String(size).padStart(6)} MB${c.reset}`);
153
- }
154
- });
155
-
156
- console.log();
157
- console.log(` → ${c.bold}Total a libertar:${c.reset} ${c.green}~${Math.round(totalClean / 1024)} GB${c.reset} ${c.dim}(${totalClean} MB)${c.reset}`);
158
- console.log();
159
-
160
- // FASE 3: LIMPEZA
161
- console.log(`${c.white}┌─────────────────────────────────────────────────────────────────┐${c.reset}`);
162
- console.log(`${c.white}│${c.reset} ${c.blue}▶ FASE 3: LIMPEZA EM PROGRESSO${c.reset} ${c.white}│${c.reset}`);
163
- console.log(`${c.white}└─────────────────────────────────────────────────────────────────┘${c.reset}`);
164
- console.log();
165
-
166
- // npm cache
167
- try {
168
- execSync('npm cache clean --force 2>/dev/null', { stdio: 'ignore' });
169
- console.log(` ${c.green}✓${c.reset} npm cache`);
170
- } catch (e) {}
171
-
172
- // Limpar paths
173
- cleanPaths.forEach(item => {
174
- cleanPath(item.path, item.name);
175
- });
176
-
177
- // Claude versões antigas
178
- const claudeVersionsPath = `${HOME}/.local/share/claude/versions`;
179
- if (fs.existsSync(claudeVersionsPath)) {
180
- try {
181
- const versions = fs.readdirSync(claudeVersionsPath).sort().reverse();
182
- if (versions.length > 1) {
539
+
540
+ // Custom paths
541
+ if (fs.existsSync(CUSTOM_PATHS_FILE)) {
542
+ const customPaths = fs.readFileSync(CUSTOM_PATHS_FILE, 'utf8').split('\n').filter(l => l.trim() && !l.startsWith('#'));
543
+ customPaths.forEach(cp => {
544
+ addItem(cp, `Custom: ${path.basename(cp)}`, 'custom');
545
+ });
546
+ }
547
+
548
+ if (items.length === 0) {
549
+ console.log(` ${c.green}${CHECK}${c.reset} ${MSG.NO_ITEMS}`);
550
+ await pressEnter();
551
+ return;
552
+ }
553
+
554
+ // Show by category
555
+ function showCategory(catId, catName) {
556
+ const catItems = items.filter(i => i.category === catId);
557
+ if (catItems.length === 0) return;
558
+
559
+ const catSize = catItems.reduce((sum, i) => sum + i.size, 0);
560
+ console.log(` ${c.white}📁 ${catName}${c.reset} ${c.dim}(${formatSize(catSize)})${c.reset}`);
561
+
562
+ catItems.forEach(item => {
563
+ const idx = items.indexOf(item) + 1;
564
+ console.log(` ${c.cyan}[${String(idx).padStart(2)}]${c.reset} ${item.name.padEnd(35)} ${c.dim}${String(item.size).padStart(6)} MB${c.reset}`);
565
+ });
566
+ console.log();
567
+ }
568
+
569
+ showCategory('system', MSG.SYSTEM_CACHES);
570
+ showCategory('dev', MSG.DEV_TOOLS);
571
+ showCategory('browsers', MSG.BROWSERS_CAT);
572
+ showCategory('downloads', MSG.DOWNLOADS_CAT);
573
+ showCategory('apps', MSG.APPS_CAT);
574
+ showCategory('custom', 'Custom');
575
+
576
+ console.log(` ${ARROW} ${c.white}${MSG.TOTAL}:${c.reset} ${c.green}${formatSize(totalSize)}${c.reset}`);
577
+ console.log();
578
+
579
+ // Selection
580
+ sectionHeader(MSG.SELECT_DELETE);
581
+ console.log(` ${c.dim}a = ${MSG.ALL} | n = ${MSG.CANCEL} | 1,3,5 | 1-5 | 1-5,8,10${c.reset}`);
582
+ console.log();
583
+
584
+ const choice = await prompt(` ${c.white}${MSG.CHOOSE}:${c.reset} `);
585
+
586
+ if (choice.toLowerCase() === 'n' || choice === '') {
587
+ console.log(` ${c.yellow}${MSG.CANCEL}${c.reset}`);
588
+ await pressEnter();
589
+ return;
590
+ }
591
+
592
+ // Process selection
593
+ if (choice.toLowerCase() !== 'a') {
594
+ items.forEach(item => item.selected = false);
595
+
596
+ const parts = choice.split(',');
597
+ parts.forEach(part => {
598
+ part = part.trim();
599
+ if (part.includes('-')) {
600
+ const [start, end] = part.split('-').map(n => parseInt(n));
601
+ for (let j = start; j <= end; j++) {
602
+ if (j > 0 && j <= items.length) items[j - 1].selected = true;
603
+ }
604
+ } else {
605
+ const idx = parseInt(part) - 1;
606
+ if (idx >= 0 && idx < items.length) items[idx].selected = true;
607
+ }
608
+ });
609
+ }
610
+
611
+ // Clean
612
+ console.log();
613
+ sectionHeader(MSG.CLEANING);
614
+
615
+ let cleanedSize = 0;
616
+
617
+ for (const item of items) {
618
+ if (!item.selected) continue;
619
+
620
+ if (item.path === 'CLAUDE_VERSIONS') {
621
+ const versions = fs.readdirSync(claudeVersionsPath).sort().reverse();
183
622
  versions.slice(1).forEach(v => {
184
- fs.rmSync(path.join(claudeVersionsPath, v), { recursive: true, force: true });
623
+ try {
624
+ fs.rmSync(path.join(claudeVersionsPath, v), { recursive: true, force: true });
625
+ } catch (e) {}
185
626
  });
186
- console.log(` ${c.green}✓${c.reset} Claude (${versions.length - 1} versões antigas)`);
627
+ console.log(` ${c.green}${CHECK}${c.reset} ${item.name}`);
628
+ cleanedSize += item.size;
629
+ } else if (item.path === 'OLD_DOWNLOADS') {
630
+ exec('find ~/Downloads -name "*.dmg" -mtime +7 -delete 2>/dev/null');
631
+ exec('find ~/Downloads -name "*.zip" -mtime +30 -delete 2>/dev/null');
632
+ console.log(` ${c.green}${CHECK}${c.reset} ${item.name}`);
633
+ cleanedSize += item.size;
634
+ } else if (fs.existsSync(item.path)) {
635
+ try {
636
+ fs.rmSync(item.path, { recursive: true, force: true });
637
+ console.log(` ${c.green}${CHECK}${c.reset} ${item.name}`);
638
+ cleanedSize += item.size;
639
+ } catch (e) {}
187
640
  }
641
+ }
642
+
643
+ // npm cache
644
+ try {
645
+ execSync('npm cache clean --force 2>/dev/null', { stdio: 'ignore' });
188
646
  } catch (e) {}
647
+
648
+ console.log();
649
+ getSystemStats();
650
+ showFinalReport(cleanedSize);
651
+ await pressEnter();
189
652
  }
190
653
 
191
- // Downloads antigos
192
- try {
193
- execSync('find ~/Downloads -name "*.dmg" -mtime +7 -delete 2>/dev/null', { stdio: 'ignore' });
194
- execSync('find ~/Downloads -name "*.zip" -mtime +30 -delete 2>/dev/null', { stdio: 'ignore' });
195
- } catch (e) {}
196
-
197
- // Logs
198
- try {
199
- execSync('rm -rf ~/Library/Logs/*.log 2>/dev/null', { stdio: 'ignore' });
200
- console.log(` ${c.green}✓${c.reset} Logs do sistema`);
201
- } catch (e) {}
202
-
203
- console.log();
204
-
205
- // FASE 4: RESULTADO
206
- const diskFreeAfter = parseInt(exec("df / | tail -1").split(/\s+/)[3]);
207
- const freed = Math.round((diskFreeAfter - diskFreeBefore) / 1024 / 1024);
208
- const diskFreeGBAfter = exec("df -h / | tail -1").split(/\s+/)[3];
209
- const diskPercentAfter = parseInt(exec("df / | tail -1").split(/\s+/)[4]);
210
-
211
- // Health Score
212
- let healthScore = 100;
213
- if (diskPercentAfter > 80) healthScore -= 30;
214
- else if (diskPercentAfter > 60) healthScore -= 10;
215
- if (ramUsedPercent > 90) healthScore -= 30;
216
- else if (ramUsedPercent > 70) healthScore -= 10;
217
- if (swapUsed > 8000) healthScore -= 20;
218
- else if (swapUsed > 4000) healthScore -= 10;
219
-
220
- let healthColor = c.green;
221
- let healthText = 'Excelente';
222
- if (healthScore < 60) { healthColor = c.red; healthText = 'Atenção'; }
223
- else if (healthScore < 80) { healthColor = c.yellow; healthText = 'Bom'; }
224
-
225
- console.log(`${c.cyan}╔══════════════════════════════════════════════════════════════════╗${c.reset}`);
226
- console.log(`${c.cyan}║${c.reset}${c.bold}${c.white} RELATÓRIO FINAL ${c.reset}${c.cyan}║${c.reset}`);
227
- console.log(`${c.cyan}╠══════════════════════════════════════════════════════════════════╣${c.reset}`);
228
- console.log(`${c.cyan}║${c.reset} ${c.cyan}║${c.reset}`);
229
- console.log(`${c.cyan}║${c.reset} ${c.bold}Disco${c.reset} ${c.cyan}║${c.reset}`);
230
- console.log(`${c.cyan}║${c.reset} → Disponível: ${c.green}${diskFreeGBAfter}${c.reset} ${c.cyan}║${c.reset}`);
231
- if (freed > 0) {
232
- console.log(`${c.cyan}║${c.reset} → Libertado: ${c.green}+${freed} GB${c.reset} ${c.cyan}║${c.reset}`);
233
- }
234
- console.log(`${c.cyan}║${c.reset} ${c.cyan}║${c.reset}`);
235
- console.log(`${c.cyan}║${c.reset} ${c.bold}Health Score${c.reset} ${c.cyan}║${c.reset}`);
236
- console.log(`${c.cyan}║${c.reset} → ${healthColor}${healthScore}/100 - ${healthText}${c.reset} ${c.cyan}║${c.reset}`);
237
- console.log(`${c.cyan}║${c.reset} ${c.cyan}║${c.reset}`);
238
- console.log(`${c.cyan}╚══════════════════════════════════════════════════════════════════╝${c.reset}`);
239
- console.log();
240
-
241
- if (swapUsed > 5000 || ramUsedPercent > 85) {
242
- console.log(`${c.yellow}⚠ Recomendação: Reinicia o Mac para limpar memória${c.reset}`);
243
- console.log();
244
- }
245
-
246
- console.log(`${c.green}✓ Pronto para trabalhar!${c.reset}`);
247
- console.log();
654
+ // ══════════════════════════════════════════════════════════════════════════════
655
+ // OPTION 2: LARGE FILES
656
+ // ══════════════════════════════════════════════════════════════════════════════
657
+
658
+ async function findLargeFiles() {
659
+ header();
660
+ sectionHeader(`${MSG.LARGE_FILES} (>100MB)`);
661
+
662
+ console.log(` ${c.dim}${MSG.SCANNING}${c.reset}`);
663
+ console.log();
664
+
665
+ const files = [];
666
+
667
+ const output = exec(`find ~ -type f -size +100M 2>/dev/null | head -50 | xargs -I {} du -m {} 2>/dev/null | sort -rn | head -30`);
668
+
669
+ output.split('\n').filter(l => l.trim()).forEach(line => {
670
+ const match = line.match(/^\s*(\d+)\s+(.+)$/);
671
+ if (!match) return;
672
+
673
+ const size = parseInt(match[1]);
674
+ const file = match[2];
675
+
676
+ // Skip system/protected paths
677
+ if (file.includes('/Library/Application Support/') ||
678
+ file.includes('/.Trash/') ||
679
+ file.includes('/node_modules/')) return;
680
+
681
+ files.push({ path: file, size });
682
+ });
683
+
684
+ if (files.length === 0) {
685
+ console.log(` ${c.green}${CHECK}${c.reset} ${MSG.NO_ITEMS}`);
686
+ await pressEnter();
687
+ return;
688
+ }
689
+
690
+ header();
691
+ sectionHeader(`${MSG.LARGE_FILES} (>100MB)`);
692
+
693
+ const totalLarge = files.reduce((sum, f) => sum + f.size, 0);
694
+
695
+ files.forEach((file, i) => {
696
+ let displayFile = file.path.replace(HOME, '~');
697
+ if (displayFile.length > 50) {
698
+ displayFile = '...' + displayFile.slice(-47);
699
+ }
700
+ console.log(` ${c.cyan}[${String(i + 1).padStart(2)}]${c.reset} ${c.dim}${String(file.size).padStart(6)} MB${c.reset} ${displayFile}`);
701
+ });
702
+
703
+ console.log();
704
+ console.log(` ${ARROW} ${c.white}${MSG.TOTAL}:${c.reset} ${c.green}${formatSize(totalLarge)}${c.reset} ${c.dim}(${files.length} files)${c.reset}`);
705
+ console.log();
706
+
707
+ console.log(` ${c.dim}${MSG.SELECT_DELETE} [numbers/a/n]:${c.reset}`);
708
+ const choice = await prompt(` ${c.white}${MSG.CHOOSE}:${c.reset} `);
709
+
710
+ if (choice.toLowerCase() === 'n' || choice === '') {
711
+ await pressEnter();
712
+ return;
713
+ }
714
+
715
+ const selected = new Array(files.length).fill(false);
716
+
717
+ if (choice.toLowerCase() === 'a') {
718
+ selected.fill(true);
719
+ } else {
720
+ const parts = choice.split(',');
721
+ parts.forEach(part => {
722
+ part = part.trim();
723
+ if (part.includes('-')) {
724
+ const [start, end] = part.split('-').map(n => parseInt(n));
725
+ for (let j = start; j <= end; j++) {
726
+ if (j > 0 && j <= files.length) selected[j - 1] = true;
727
+ }
728
+ } else {
729
+ const idx = parseInt(part) - 1;
730
+ if (idx >= 0 && idx < files.length) selected[idx] = true;
731
+ }
732
+ });
733
+ }
734
+
735
+ let deletedSize = 0;
736
+ console.log();
737
+
738
+ for (let i = 0; i < files.length; i++) {
739
+ if (!selected[i]) continue;
740
+
741
+ try {
742
+ fs.unlinkSync(files[i].path);
743
+ console.log(` ${c.green}${CHECK}${c.reset} Deleted: ${path.basename(files[i].path)}`);
744
+ deletedSize += files[i].size;
745
+ } catch (e) {}
746
+ }
747
+
748
+ console.log();
749
+ console.log(` ${c.green}${MSG.FREED}: ${formatSize(deletedSize)}${c.reset}`);
750
+ await pressEnter();
751
+ }
752
+
753
+ // ══════════════════════════════════════════════════════════════════════════════
754
+ // OPTION 3: FIND DUPLICATES
755
+ // ══════════════════════════════════════════════════════════════════════════════
756
+
757
+ async function findDuplicates() {
758
+ header();
759
+ sectionHeader(MSG.DUPLICATES);
760
+
761
+ console.log(` ${c.dim}${MSG.SCANNING} (this may take a while)...${c.reset}`);
762
+ console.log();
763
+
764
+ // Find files > 1MB, group by size
765
+ const sizeMap = new Map();
766
+
767
+ const output = exec(`find ~ -type f -size +1M 2>/dev/null | head -500`);
768
+ const files = output.split('\n').filter(l => l.trim());
769
+
770
+ files.forEach(file => {
771
+ try {
772
+ const stats = fs.statSync(file);
773
+ const size = stats.size;
774
+ if (!sizeMap.has(size)) sizeMap.set(size, []);
775
+ sizeMap.get(size).push(file);
776
+ } catch (e) {}
777
+ });
778
+
779
+ console.log(` ${c.dim}${MSG.HASHING}${c.reset}`);
780
+
781
+ // Find duplicates by hash for files with same size
782
+ const hashGroups = new Map();
783
+
784
+ for (const [size, fileList] of sizeMap) {
785
+ if (fileList.length < 2) continue;
786
+
787
+ for (const file of fileList) {
788
+ try {
789
+ const hash = exec(`md5 -q "${file}" 2>/dev/null`);
790
+ if (hash) {
791
+ if (!hashGroups.has(hash)) hashGroups.set(hash, []);
792
+ hashGroups.get(hash).push(file);
793
+ }
794
+ } catch (e) {}
795
+ }
796
+ }
797
+
798
+ // Display duplicate groups
799
+ header();
800
+ sectionHeader(MSG.DUPLICATES);
801
+
802
+ let groupNum = 0;
803
+ let totalDupSize = 0;
804
+ const allDups = [];
805
+
806
+ for (const [hash, fileList] of hashGroups) {
807
+ if (fileList.length < 2) continue;
808
+
809
+ groupNum++;
810
+ const fileSize = Math.round(getSize(fileList[0]));
811
+ const dupSpace = (fileList.length - 1) * fileSize;
812
+ totalDupSize += dupSpace;
813
+
814
+ console.log(` ${c.white}${MSG.GROUP} ${groupNum}${c.reset} ${c.dim}(${fileSize} MB ${MSG.EACH})${c.reset}`);
815
+
816
+ fileList.forEach((file, idx) => {
817
+ let display = file.replace(HOME, '~');
818
+ if (display.length > 50) display = '...' + display.slice(-47);
819
+ console.log(` ${c.cyan}[${idx + 1}]${c.reset} ${display}`);
820
+ allDups.push(file);
821
+ });
822
+ console.log();
823
+
824
+ if (groupNum >= 10) break;
825
+ }
826
+
827
+ if (groupNum === 0) {
828
+ console.log(` ${c.green}${CHECK}${c.reset} ${MSG.NO_ITEMS}`);
829
+ await pressEnter();
830
+ return;
831
+ }
832
+
833
+ console.log(` ${ARROW} ${c.white}${MSG.TOTAL}:${c.reset} ${c.green}${formatSize(totalDupSize)}${c.reset} ${MSG.IN_DUPLICATES}`);
834
+ console.log();
835
+ console.log(` ${c.dim}${MSG.SELECT_KEEP}${c.reset}`);
836
+ const choice = await prompt(` ${c.white}${MSG.CHOOSE}:${c.reset} `);
837
+
838
+ console.log(` ${c.yellow}${MSG.FEATURE_PROGRESS}${c.reset}`);
839
+ await pressEnter();
840
+ }
841
+
842
+ // ══════════════════════════════════════════════════════════════════════════════
843
+ // OPTION 4: UNINSTALL APPS
844
+ // ══════════════════════════════════════════════════════════════════════════════
845
+
846
+ async function uninstallApps() {
847
+ header();
848
+ sectionHeader(MSG.UNINSTALL);
849
+
850
+ console.log(` ${c.dim}${MSG.SCANNING}${c.reset}`);
851
+
852
+ const apps = [];
853
+
854
+ const appDirs = fs.readdirSync('/Applications').filter(f => f.endsWith('.app'));
855
+
856
+ for (const appDir of appDirs) {
857
+ const appPath = path.join('/Applications', appDir);
858
+ const appName = appDir.replace('.app', '');
859
+
860
+ const appSize = getSize(appPath);
861
+
862
+ // Find associated data
863
+ let dataSize = 0;
864
+ let bundleId = '';
865
+
866
+ try {
867
+ bundleId = exec(`defaults read "${appPath}/Contents/Info" CFBundleIdentifier 2>/dev/null`);
868
+ } catch (e) {}
869
+
870
+ if (bundleId) {
871
+ const dataPaths = [
872
+ path.join(HOME, 'Library/Application Support', appName),
873
+ path.join(HOME, 'Library/Application Support', bundleId),
874
+ path.join(HOME, 'Library/Preferences', `${bundleId}.plist`),
875
+ path.join(HOME, 'Library/Caches', bundleId),
876
+ path.join(HOME, 'Library/Caches', appName),
877
+ path.join(HOME, 'Library/Containers', bundleId),
878
+ path.join(HOME, 'Library/Saved Application State', `${bundleId}.savedState`)
879
+ ];
880
+
881
+ dataPaths.forEach(dp => {
882
+ if (fs.existsSync(dp)) {
883
+ dataSize += getSize(dp);
884
+ }
885
+ });
886
+ }
887
+
888
+ const total = appSize + dataSize;
889
+ if (total < 50) continue;
890
+
891
+ apps.push({ name: appName, path: appPath, appSize, dataSize, bundleId });
892
+
893
+ if (apps.length >= 25) break;
894
+ }
895
+
896
+ header();
897
+ sectionHeader(MSG.UNINSTALL);
898
+
899
+ if (apps.length === 0) {
900
+ console.log(` ${c.green}${CHECK}${c.reset} ${MSG.NO_ITEMS}`);
901
+ await pressEnter();
902
+ return;
903
+ }
904
+
905
+ apps.forEach((app, i) => {
906
+ console.log(` ${c.cyan}[${String(i + 1).padStart(2)}]${c.reset} ${app.name.padEnd(25)} ${c.dim}(${String(app.appSize).padStart(3)} MB ${MSG.APP_SIZE} + ${String(app.dataSize).padStart(3)} MB ${MSG.DATA_SIZE})${c.reset}`);
907
+ });
908
+
909
+ console.log();
910
+ console.log(` ${c.dim}${MSG.SELECT_DELETE} [numbers/n]:${c.reset}`);
911
+ const choice = await prompt(` ${c.white}${MSG.CHOOSE}:${c.reset} `);
912
+
913
+ if (choice.toLowerCase() === 'n' || choice === '') {
914
+ await pressEnter();
915
+ return;
916
+ }
917
+
918
+ const parts = choice.split(',');
919
+ for (const part of parts) {
920
+ const idx = parseInt(part.trim()) - 1;
921
+ if (idx < 0 || idx >= apps.length) continue;
922
+
923
+ const app = apps[idx];
924
+ console.log(` ${c.yellow}${MSG.DELETING} ${app.name}...${c.reset}`);
925
+
926
+ // Remove app data
927
+ if (app.bundleId) {
928
+ const dataPaths = [
929
+ path.join(HOME, 'Library/Application Support', app.name),
930
+ path.join(HOME, 'Library/Application Support', app.bundleId),
931
+ path.join(HOME, 'Library/Preferences', `${app.bundleId}.plist`),
932
+ path.join(HOME, 'Library/Caches', app.bundleId),
933
+ path.join(HOME, 'Library/Caches', app.name),
934
+ path.join(HOME, 'Library/Containers', app.bundleId),
935
+ path.join(HOME, 'Library/Saved Application State', `${app.bundleId}.savedState`),
936
+ path.join(HOME, 'Library/Logs', app.name)
937
+ ];
938
+
939
+ dataPaths.forEach(dp => {
940
+ try {
941
+ if (fs.existsSync(dp)) fs.rmSync(dp, { recursive: true, force: true });
942
+ } catch (e) {}
943
+ });
944
+ }
945
+
946
+ // Remove app
947
+ try {
948
+ fs.rmSync(app.path, { recursive: true, force: true });
949
+ console.log(` ${c.green}${CHECK}${c.reset} ${app.name} ${MSG.REMOVED}`);
950
+ } catch (e) {
951
+ console.log(` ${c.red}${MSG.NEED_SUDO}${c.reset}`);
952
+ console.log(` ${c.dim}sudo rm -rf "${app.path}"${c.reset}`);
953
+ }
954
+ }
955
+
956
+ await pressEnter();
957
+ }
958
+
959
+ // ══════════════════════════════════════════════════════════════════════════════
960
+ // OPTION 5: MANAGE STARTUP
961
+ // ══════════════════════════════════════════════════════════════════════════════
962
+
963
+ async function manageStartup() {
964
+ header();
965
+ sectionHeader(MSG.STARTUP_ITEMS);
966
+
967
+ console.log(` ${c.white}${MSG.STARTUP_ITEMS}:${c.reset}`);
968
+
969
+ // Get login items via AppleScript
970
+ const loginItems = exec(`osascript -e 'tell application "System Events" to get the name of every login item' 2>/dev/null`);
971
+
972
+ if (loginItems) {
973
+ const items = loginItems.split(', ').filter(i => i.trim());
974
+ items.forEach((item, idx) => {
975
+ console.log(` ${c.cyan}[${idx + 1}]${c.reset} ${c.green}${CHECK}${c.reset} ${item} ${c.dim}(${MSG.ENABLED})${c.reset}`);
976
+ });
977
+ } else {
978
+ console.log(` ${c.dim}No login items found${c.reset}`);
979
+ }
980
+
981
+ console.log();
982
+ console.log(` ${c.white}${MSG.LAUNCH_AGENTS}:${c.reset}`);
983
+
984
+ // User Launch Agents
985
+ const launchAgentsDir = path.join(HOME, 'Library/LaunchAgents');
986
+ let idx = 100;
987
+
988
+ if (fs.existsSync(launchAgentsDir)) {
989
+ const agents = fs.readdirSync(launchAgentsDir).filter(f => f.endsWith('.plist'));
990
+
991
+ for (const agent of agents) {
992
+ const name = agent.replace('.plist', '');
993
+ const loaded = exec(`launchctl list 2>/dev/null | grep "${name}"`);
994
+
995
+ const status = loaded ?
996
+ `${c.green}${CHECK}${c.reset} ${MSG.ENABLED}` :
997
+ `${c.red}${CROSS}${c.reset} ${MSG.DISABLED}`;
998
+
999
+ console.log(` ${c.cyan}[${idx}]${c.reset} ${status} ${c.dim}${name}${c.reset}`);
1000
+ idx++;
1001
+ }
1002
+ }
1003
+
1004
+ console.log();
1005
+ console.log(` ${c.dim}Enter number to toggle, 'd' to disable all, or 'n' to go back${c.reset}`);
1006
+ const choice = await prompt(` ${c.white}${MSG.CHOOSE}:${c.reset} `);
1007
+
1008
+ if (choice.toLowerCase() === 'd') {
1009
+ exec(`osascript -e 'tell application "System Events" to delete every login item' 2>/dev/null`);
1010
+ console.log(` ${c.green}${CHECK}${c.reset} All login items disabled`);
1011
+ }
1012
+
1013
+ await pressEnter();
1014
+ }
1015
+
1016
+ // ══════════════════════════════════════════════════════════════════════════════
1017
+ // OPTION 6: CLEAN BROWSERS
1018
+ // ══════════════════════════════════════════════════════════════════════════════
1019
+
1020
+ async function cleanBrowsers() {
1021
+ header();
1022
+ sectionHeader(MSG.BROWSERS_CAT);
1023
+
1024
+ const items = [];
1025
+ let totalBrowser = 0;
1026
+
1027
+ function addBrowserItem(p, name) {
1028
+ const fullPath = p.replace(/^~/, HOME);
1029
+ const size = getSize(p);
1030
+ if (size > 0) {
1031
+ items.push({ path: fullPath, name, size, selected: false });
1032
+ totalBrowser += size;
1033
+ }
1034
+ }
1035
+
1036
+ console.log(` ${c.white}Chrome:${c.reset}`);
1037
+ addBrowserItem('~/Library/Caches/Google/Chrome', 'Chrome Cache');
1038
+ addBrowserItem('~/Library/Application Support/Google/Chrome/Default/Service Worker', 'Chrome Service Workers');
1039
+ addBrowserItem('~/Library/Application Support/Google/Chrome/Default/GPUCache', 'Chrome GPU Cache');
1040
+
1041
+ console.log(` ${c.white}Safari:${c.reset}`);
1042
+ addBrowserItem('~/Library/Caches/com.apple.Safari', 'Safari Cache');
1043
+ addBrowserItem('~/Library/Safari/LocalStorage', 'Safari Local Storage');
1044
+
1045
+ console.log(` ${c.white}Firefox:${c.reset}`);
1046
+ const ffProfile = exec('find ~/Library/Application\\ Support/Firefox/Profiles -maxdepth 1 -name "*.default*" 2>/dev/null | head -1');
1047
+ if (ffProfile) {
1048
+ addBrowserItem(`${ffProfile}/cache2`, 'Firefox Cache');
1049
+ }
1050
+
1051
+ items.forEach((item, i) => {
1052
+ console.log(` ${c.cyan}[${String(i + 1).padStart(2)}]${c.reset} ${item.name.padEnd(30)} ${c.dim}${String(item.size).padStart(6)} MB${c.reset}`);
1053
+ });
1054
+
1055
+ if (items.length === 0) {
1056
+ console.log(` ${c.green}${CHECK}${c.reset} ${MSG.NO_ITEMS}`);
1057
+ await pressEnter();
1058
+ return;
1059
+ }
1060
+
1061
+ console.log();
1062
+ console.log(` ${ARROW} ${c.white}${MSG.TOTAL}:${c.reset} ${c.green}${formatSize(totalBrowser)}${c.reset}`);
1063
+ console.log();
1064
+
1065
+ console.log(` ${c.dim}${MSG.SELECT_DELETE} [numbers/a/n]:${c.reset}`);
1066
+ const choice = await prompt(` ${c.white}${MSG.CHOOSE}:${c.reset} `);
1067
+
1068
+ if (choice.toLowerCase() === 'n' || choice === '') {
1069
+ await pressEnter();
1070
+ return;
1071
+ }
1072
+
1073
+ if (choice.toLowerCase() === 'a') {
1074
+ items.forEach(item => item.selected = true);
1075
+ } else {
1076
+ const parts = choice.split(',');
1077
+ parts.forEach(part => {
1078
+ const idx = parseInt(part.trim()) - 1;
1079
+ if (idx >= 0 && idx < items.length) items[idx].selected = true;
1080
+ });
1081
+ }
1082
+
1083
+ let cleaned = 0;
1084
+ console.log();
1085
+
1086
+ for (const item of items) {
1087
+ if (!item.selected) continue;
1088
+ try {
1089
+ fs.rmSync(item.path, { recursive: true, force: true });
1090
+ console.log(` ${c.green}${CHECK}${c.reset} ${item.name}`);
1091
+ cleaned += item.size;
1092
+ } catch (e) {}
1093
+ }
1094
+
1095
+ console.log();
1096
+ console.log(` ${c.green}${MSG.FREED}: ${formatSize(cleaned)}${c.reset}`);
1097
+ await pressEnter();
1098
+ }
1099
+
1100
+ // ══════════════════════════════════════════════════════════════════════════════
1101
+ // OPTION 7: KILL PROCESSES
1102
+ // ══════════════════════════════════════════════════════════════════════════════
1103
+
1104
+ async function killProcesses() {
1105
+ header();
1106
+ sectionHeader(MSG.PROCESS_MEM);
1107
+
1108
+ console.log(` ${c.white}${MSG.PROCESS_MEM}:${c.reset}`);
1109
+
1110
+ const procs = [];
1111
+ const totalMemBytes = parseInt(exec("sysctl hw.memsize 2>/dev/null | awk '{print $2}'")) || 8589934592;
1112
+ const totalMemMB = Math.round(totalMemBytes / 1024 / 1024);
1113
+
1114
+ const output = exec('ps aux -m 2>/dev/null | head -15');
1115
+ const lines = output.split('\n').slice(1); // Skip header
1116
+
1117
+ for (const line of lines) {
1118
+ const parts = line.split(/\s+/);
1119
+ if (parts.length < 11) continue;
1120
+
1121
+ const pid = parts[1];
1122
+ const mem = parseFloat(parts[3]) || 0;
1123
+ const name = parts[10];
1124
+
1125
+ // Skip system processes
1126
+ if (['kernel_task', 'WindowServer', 'launchd'].includes(name)) continue;
1127
+
1128
+ const memMB = Math.round(mem * totalMemMB / 100);
1129
+ if (memMB < 100) continue;
1130
+
1131
+ procs.push({ pid, name, memMB });
1132
+
1133
+ console.log(` ${c.cyan}[${String(procs.length).padStart(2)}]${c.reset} ${name.padEnd(30)} ${c.red}${memMB} MB RAM${c.reset}`);
1134
+
1135
+ if (procs.length >= 10) break;
1136
+ }
1137
+
1138
+ if (procs.length === 0) {
1139
+ console.log(` ${c.green}${CHECK}${c.reset} ${MSG.NO_ITEMS}`);
1140
+ await pressEnter();
1141
+ return;
1142
+ }
1143
+
1144
+ console.log();
1145
+ console.log(` ${c.dim}${MSG.KILL} process [number/n]:${c.reset}`);
1146
+ const choice = await prompt(` ${c.white}${MSG.CHOOSE}:${c.reset} `);
1147
+
1148
+ if (choice.toLowerCase() === 'n' || choice === '') {
1149
+ await pressEnter();
1150
+ return;
1151
+ }
1152
+
1153
+ const idx = parseInt(choice) - 1;
1154
+ if (idx >= 0 && idx < procs.length) {
1155
+ const proc = procs[idx];
1156
+ try {
1157
+ process.kill(parseInt(proc.pid), 'SIGKILL');
1158
+ console.log(` ${c.green}${CHECK}${c.reset} ${MSG.KILLED}: ${proc.name} (PID: ${proc.pid})`);
1159
+ } catch (e) {
1160
+ console.log(` ${c.red}${CROSS}${c.reset} ${MSG.FAILED_KILL} ${proc.name}`);
1161
+ }
1162
+ }
1163
+
1164
+ await pressEnter();
1165
+ }
1166
+
1167
+ // ══════════════════════════════════════════════════════════════════════════════
1168
+ // OPTION 8: STATISTICS
1169
+ // ══════════════════════════════════════════════════════════════════════════════
1170
+
1171
+ async function showStatistics() {
1172
+ header();
1173
+ sectionHeader(MSG.OPT_STATS);
1174
+
1175
+ if (!fs.existsSync(HISTORY_FILE)) {
1176
+ console.log(` ${c.dim}${MSG.NO_HISTORY}${c.reset}`);
1177
+ await pressEnter();
1178
+ return;
1179
+ }
1180
+
1181
+ const history = fs.readFileSync(HISTORY_FILE, 'utf8').split('\n').filter(l => l.trim());
1182
+ const currentMonth = new Date().toISOString().slice(0, 7);
1183
+
1184
+ const monthLines = history.filter(l => l.startsWith(currentMonth));
1185
+ const monthCleanups = monthLines.length;
1186
+
1187
+ let monthFreed = 0;
1188
+ let monthScoreSum = 0;
1189
+
1190
+ monthLines.forEach(line => {
1191
+ const freedMatch = line.match(/(\d+)\s*MB/);
1192
+ const scoreMatch = line.match(/Score:\s*(\d+)/);
1193
+ if (freedMatch) monthFreed += parseInt(freedMatch[1]);
1194
+ if (scoreMatch) monthScoreSum += parseInt(scoreMatch[1]);
1195
+ });
1196
+
1197
+ const monthAvg = monthCleanups > 0 ? Math.round(monthScoreSum / monthCleanups) : 0;
1198
+
1199
+ console.log(` ${c.white}${MSG.THIS_MONTH}:${c.reset}`);
1200
+ console.log(` ${BULLET} ${MSG.CLEANINGS}: ${monthCleanups}`);
1201
+ console.log(` ${BULLET} ${MSG.TOTAL} ${MSG.FREED}: ${formatSize(monthFreed)}`);
1202
+ console.log(` ${BULLET} ${MSG.AVG_SCORE}: ${monthAvg}/100`);
1203
+ console.log();
1204
+
1205
+ console.log(` ${c.white}${MSG.HISTORY}:${c.reset}`);
1206
+ history.slice(-10).forEach(line => {
1207
+ console.log(` ${c.dim}${line}${c.reset}`);
1208
+ });
1209
+
1210
+ await pressEnter();
1211
+ }
1212
+
1213
+ // ══════════════════════════════════════════════════════════════════════════════
1214
+ // OPTION 9: SETTINGS
1215
+ // ══════════════════════════════════════════════════════════════════════════════
1216
+
1217
+ async function showSettings() {
1218
+ while (true) {
1219
+ header();
1220
+ sectionHeader(MSG.OPT_SETTINGS);
1221
+
1222
+ const currentLang = LANG_CODE === 'en' ? 'EN' : 'PT';
1223
+
1224
+ console.log(` ${c.cyan}[1]${c.reset} ${MSG.EXCLUSIONS}`);
1225
+ console.log(` ${c.cyan}[2]${c.reset} ${MSG.CUSTOM_PATHS}`);
1226
+ console.log(` ${c.cyan}[3]${c.reset} ${MSG.CHANGE_LANG} (current: ${currentLang})`);
1227
+ console.log(` ${c.cyan}[4]${c.reset} ${MSG.RESET_DEFAULTS}`);
1228
+ console.log();
1229
+ console.log(` ${c.cyan}[0]${c.reset} ${MSG.BACK}`);
1230
+ console.log();
1231
+
1232
+ const choice = await prompt(` ${c.white}${MSG.CHOOSE}:${c.reset} `);
1233
+
1234
+ switch (choice) {
1235
+ case '1':
1236
+ // Manage exclusions
1237
+ header();
1238
+ sectionHeader(MSG.EXCLUSIONS);
1239
+
1240
+ console.log(` ${c.white}${MSG.CURRENT_EXCLUSIONS}:${c.reset}`);
1241
+ if (fs.existsSync(EXCLUSIONS_FILE)) {
1242
+ const exclusions = fs.readFileSync(EXCLUSIONS_FILE, 'utf8').split('\n').filter(l => l.trim());
1243
+ exclusions.forEach((ex, i) => console.log(` ${i + 1}. ${ex}`));
1244
+ } else {
1245
+ console.log(` ${c.dim}None${c.reset}`);
1246
+ }
1247
+ console.log();
1248
+ console.log(` ${c.dim}${MSG.ENTER_PATH}:${c.reset}`);
1249
+
1250
+ const newExclusion = await prompt(` ${c.white}${MSG.PATH}:${c.reset} `);
1251
+
1252
+ if (newExclusion === 'c') {
1253
+ if (fs.existsSync(EXCLUSIONS_FILE)) fs.unlinkSync(EXCLUSIONS_FILE);
1254
+ console.log(` ${c.green}${CHECK}${c.reset} Exclusions ${MSG.CLEARED}`);
1255
+ } else if (newExclusion !== 'n' && newExclusion) {
1256
+ fs.appendFileSync(EXCLUSIONS_FILE, newExclusion + '\n');
1257
+ console.log(` ${c.green}${CHECK}${c.reset} ${MSG.ADDED}: ${newExclusion}`);
1258
+ }
1259
+ await pressEnter();
1260
+ break;
1261
+
1262
+ case '2':
1263
+ // Custom paths
1264
+ header();
1265
+ sectionHeader(MSG.CUSTOM_PATHS);
1266
+
1267
+ console.log(` ${c.white}${MSG.CURRENT_PATHS}:${c.reset}`);
1268
+ if (fs.existsSync(CUSTOM_PATHS_FILE)) {
1269
+ const paths = fs.readFileSync(CUSTOM_PATHS_FILE, 'utf8').split('\n').filter(l => l.trim() && !l.startsWith('#'));
1270
+ paths.forEach((p, i) => console.log(` ${i + 1}. ${p}`));
1271
+ } else {
1272
+ console.log(` ${c.dim}None${c.reset}`);
1273
+ }
1274
+ console.log();
1275
+ console.log(` ${c.dim}${MSG.ENTER_PATH}:${c.reset}`);
1276
+
1277
+ const newPath = await prompt(` ${c.white}${MSG.PATH}:${c.reset} `);
1278
+
1279
+ if (newPath === 'c') {
1280
+ if (fs.existsSync(CUSTOM_PATHS_FILE)) fs.unlinkSync(CUSTOM_PATHS_FILE);
1281
+ console.log(` ${c.green}${CHECK}${c.reset} Custom paths ${MSG.CLEARED}`);
1282
+ } else if (newPath !== 'n' && newPath) {
1283
+ fs.appendFileSync(CUSTOM_PATHS_FILE, newPath + '\n');
1284
+ console.log(` ${c.green}${CHECK}${c.reset} ${MSG.ADDED}: ${newPath}`);
1285
+ }
1286
+ await pressEnter();
1287
+ break;
1288
+
1289
+ case '3':
1290
+ // Change language
1291
+ const newLang = LANG_CODE === 'pt' ? 'en' : 'pt';
1292
+ fs.writeFileSync(LANGUAGE_FILE, newLang);
1293
+ console.log(` ${c.green}${CHECK}${c.reset} ${newLang === 'en' ? MSG.LANG_CHANGED_EN : MSG.LANG_CHANGED_PT}`);
1294
+ await pressEnter();
1295
+ // Restart to apply language change
1296
+ const { spawn } = require('child_process');
1297
+ spawn(process.argv[0], process.argv.slice(1), { stdio: 'inherit' });
1298
+ process.exit(0);
1299
+ break;
1300
+
1301
+ case '4':
1302
+ // Reset defaults
1303
+ [EXCLUSIONS_FILE, CUSTOM_PATHS_FILE, LANGUAGE_FILE].forEach(f => {
1304
+ if (fs.existsSync(f)) fs.unlinkSync(f);
1305
+ });
1306
+ console.log(` ${c.green}${CHECK}${c.reset} ${MSG.SETTINGS_RESET}`);
1307
+ await pressEnter();
1308
+ // Restart to apply changes
1309
+ const { spawn: spawn2 } = require('child_process');
1310
+ spawn2(process.argv[0], process.argv.slice(1), { stdio: 'inherit' });
1311
+ process.exit(0);
1312
+ break;
1313
+
1314
+ case '0':
1315
+ return;
1316
+
1317
+ default:
1318
+ break;
1319
+ }
1320
+ }
1321
+ }
1322
+
1323
+ // ══════════════════════════════════════════════════════════════════════════════
1324
+ // MAIN MENU
1325
+ // ══════════════════════════════════════════════════════════════════════════════
1326
+
1327
+ async function mainMenu() {
1328
+ while (true) {
1329
+ header();
1330
+
1331
+ console.log(` ${c.cyan}[1]${c.reset} 🧹 ${MSG.OPT_QUICK}`);
1332
+ console.log(` ${c.cyan}[2]${c.reset} 📦 ${MSG.OPT_LARGE}`);
1333
+ console.log(` ${c.cyan}[3]${c.reset} 🔄 ${MSG.OPT_DUPLICATES}`);
1334
+ console.log(` ${c.cyan}[4]${c.reset} 🗑️ ${MSG.OPT_APPS}`);
1335
+ console.log(` ${c.cyan}[5]${c.reset} 🚀 ${MSG.OPT_STARTUP}`);
1336
+ console.log(` ${c.cyan}[6]${c.reset} 🌐 ${MSG.OPT_BROWSERS}`);
1337
+ console.log(` ${c.cyan}[7]${c.reset} 💀 ${MSG.OPT_PROCESSES}`);
1338
+ console.log(` ${c.cyan}[8]${c.reset} 📊 ${MSG.OPT_STATS}`);
1339
+ console.log(` ${c.cyan}[9]${c.reset} ⚙️ ${MSG.OPT_SETTINGS}`);
1340
+ console.log();
1341
+ console.log(` ${c.cyan}[0]${c.reset} ${MSG.OPT_EXIT}`);
1342
+ console.log();
1343
+
1344
+ const choice = await prompt(` ${c.white}${MSG.CHOOSE}:${c.reset} `);
1345
+
1346
+ switch (choice) {
1347
+ case '1': await quickClean(); break;
1348
+ case '2': await findLargeFiles(); break;
1349
+ case '3': await findDuplicates(); break;
1350
+ case '4': await uninstallApps(); break;
1351
+ case '5': await manageStartup(); break;
1352
+ case '6': await cleanBrowsers(); break;
1353
+ case '7': await killProcesses(); break;
1354
+ case '8': await showStatistics(); break;
1355
+ case '9': await showSettings(); break;
1356
+ case '0':
1357
+ clearScreen();
1358
+ console.log();
1359
+ console.log(`${c.dim}por Salvador Reis ${BULLET} github.com/salvadorreis${c.reset}`);
1360
+ console.log();
1361
+ process.exit(0);
1362
+ break;
1363
+ default:
1364
+ console.log(` ${c.red}${MSG.INVALID}${c.reset}`);
1365
+ await new Promise(r => setTimeout(r, 1000));
1366
+ }
1367
+ }
1368
+ }
1369
+
1370
+ // ══════════════════════════════════════════════════════════════════════════════
1371
+ // MAIN
1372
+ // ══════════════════════════════════════════════════════════════════════════════
1373
+
1374
+ // Check if macOS
1375
+ if (os.platform() !== 'darwin') {
1376
+ console.log('This program only works on macOS');
1377
+ process.exit(1);
1378
+ }
1379
+
1380
+ // Command line arguments
1381
+ const args = process.argv.slice(2);
1382
+
1383
+ if (args.includes('--preview') || args.includes('--dry-run')) {
1384
+ console.log('Preview mode - nothing will be deleted');
1385
+ process.exit(0);
1386
+ }
1387
+
1388
+ if (args.includes('--quick') || args.includes('-q')) {
1389
+ (async () => {
1390
+ getSystemStats();
1391
+ await quickClean();
1392
+ })();
1393
+ } else if (args.includes('--version') || args.includes('-v')) {
1394
+ console.log(`Aris Mac Cleaner v${VERSION}`);
1395
+ process.exit(0);
1396
+ } else if (args.includes('--help') || args.includes('-h')) {
1397
+ console.log(`
1398
+ Aris Mac Cleaner v${VERSION}
1399
+
1400
+ Usage:
1401
+ aris-mac-cleaner Start interactive menu
1402
+ aris-mac-cleaner -q Quick clean without menu
1403
+ aris-mac-cleaner -v Show version
1404
+ aris-mac-cleaner -h Show this help
1405
+
1406
+ Config directory: ~/.aris-mac-cleaner/
1407
+ `);
1408
+ process.exit(0);
1409
+ } else {
1410
+ mainMenu();
1411
+ }