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.
- package/README.md +63 -28
- package/bin/cli.js +1373 -209
- 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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
44
|
+
const CHECK = '✓';
|
|
45
|
+
const CROSS = '✗';
|
|
46
|
+
const ARROW = '→';
|
|
47
|
+
const BULLET = '•';
|
|
22
48
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
264
|
+
function getSize(p) {
|
|
265
|
+
const fullPath = p.replace(/^~/, HOME);
|
|
42
266
|
try {
|
|
43
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
console.log(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
623
|
+
try {
|
|
624
|
+
fs.rmSync(path.join(claudeVersionsPath, v), { recursive: true, force: true });
|
|
625
|
+
} catch (e) {}
|
|
185
626
|
});
|
|
186
|
-
console.log(` ${c.green}
|
|
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
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
console.log(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
console.log(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
console.log(
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
}
|