@tukuyomil032/broom 1.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/LICENSE +21 -0
- package/README.md +554 -0
- package/dist/commands/analyze.js +371 -0
- package/dist/commands/backup.js +257 -0
- package/dist/commands/clean.js +255 -0
- package/dist/commands/completion.js +714 -0
- package/dist/commands/config.js +474 -0
- package/dist/commands/doctor.js +280 -0
- package/dist/commands/duplicates.js +325 -0
- package/dist/commands/help.js +34 -0
- package/dist/commands/index.js +22 -0
- package/dist/commands/installer.js +266 -0
- package/dist/commands/optimize.js +270 -0
- package/dist/commands/purge.js +271 -0
- package/dist/commands/remove.js +184 -0
- package/dist/commands/reports.js +173 -0
- package/dist/commands/schedule.js +249 -0
- package/dist/commands/status.js +468 -0
- package/dist/commands/touchid.js +230 -0
- package/dist/commands/uninstall.js +336 -0
- package/dist/commands/update.js +182 -0
- package/dist/commands/watch.js +258 -0
- package/dist/index.js +131 -0
- package/dist/scanners/base.js +21 -0
- package/dist/scanners/browser-cache.js +111 -0
- package/dist/scanners/dev-cache.js +64 -0
- package/dist/scanners/docker.js +96 -0
- package/dist/scanners/downloads.js +66 -0
- package/dist/scanners/homebrew.js +82 -0
- package/dist/scanners/index.js +126 -0
- package/dist/scanners/installer.js +87 -0
- package/dist/scanners/ios-backups.js +82 -0
- package/dist/scanners/node-modules.js +75 -0
- package/dist/scanners/temp-files.js +65 -0
- package/dist/scanners/trash.js +90 -0
- package/dist/scanners/user-cache.js +62 -0
- package/dist/scanners/user-logs.js +53 -0
- package/dist/scanners/xcode.js +124 -0
- package/dist/types/index.js +23 -0
- package/dist/ui/index.js +5 -0
- package/dist/ui/monitors.js +345 -0
- package/dist/ui/output.js +304 -0
- package/dist/ui/prompts.js +270 -0
- package/dist/utils/config.js +133 -0
- package/dist/utils/debug.js +119 -0
- package/dist/utils/fs.js +283 -0
- package/dist/utils/help.js +265 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/paths.js +142 -0
- package/dist/utils/report.js +404 -0
- package/package.json +87 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch command - Monitor directory sizes
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { enhanceCommandHelp } from '../utils/help.js';
|
|
7
|
+
import { expandPath, formatSize, exists } from '../utils/fs.js';
|
|
8
|
+
import { printHeader, separator, success, error, warning } from '../ui/output.js';
|
|
9
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
10
|
+
import { exec } from 'child_process';
|
|
11
|
+
import { promisify } from 'util';
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
const WATCH_CONFIG_FILE = expandPath('~/.config/broom/watch.json');
|
|
14
|
+
/**
|
|
15
|
+
* Parse size threshold
|
|
16
|
+
*/
|
|
17
|
+
function parseThreshold(thresholdStr) {
|
|
18
|
+
const match = thresholdStr.match(/^(\d+(?:\.\d+)?)(KB|MB|GB|TB)?$/i);
|
|
19
|
+
if (!match) {
|
|
20
|
+
throw new Error(`Invalid threshold format: ${thresholdStr}`);
|
|
21
|
+
}
|
|
22
|
+
const value = parseFloat(match[1]);
|
|
23
|
+
const unit = (match[2] || 'MB').toUpperCase();
|
|
24
|
+
switch (unit) {
|
|
25
|
+
case 'KB':
|
|
26
|
+
return value * 1024;
|
|
27
|
+
case 'MB':
|
|
28
|
+
return value * 1024 * 1024;
|
|
29
|
+
case 'GB':
|
|
30
|
+
return value * 1024 * 1024 * 1024;
|
|
31
|
+
case 'TB':
|
|
32
|
+
return value * 1024 * 1024 * 1024 * 1024;
|
|
33
|
+
default:
|
|
34
|
+
return value * 1024 * 1024; // Default to MB
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Load watch configuration
|
|
39
|
+
*/
|
|
40
|
+
async function loadWatchConfig() {
|
|
41
|
+
try {
|
|
42
|
+
if (exists(WATCH_CONFIG_FILE)) {
|
|
43
|
+
const content = await readFile(WATCH_CONFIG_FILE, 'utf-8');
|
|
44
|
+
const data = JSON.parse(content);
|
|
45
|
+
// Parse dates
|
|
46
|
+
return data.map((w) => ({
|
|
47
|
+
...w,
|
|
48
|
+
lastNotified: w.lastNotified ? new Date(w.lastNotified) : undefined,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Return empty array if cannot read
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Save watch configuration
|
|
59
|
+
*/
|
|
60
|
+
async function saveWatchConfig(watches) {
|
|
61
|
+
await mkdir(expandPath('~/.config/broom'), { recursive: true });
|
|
62
|
+
await writeFile(WATCH_CONFIG_FILE, JSON.stringify(watches, null, 2), 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get directory size
|
|
66
|
+
*/
|
|
67
|
+
async function getDirectorySize(path) {
|
|
68
|
+
try {
|
|
69
|
+
const { stdout } = await execAsync(`du -sk "${path}"`);
|
|
70
|
+
const size = parseInt(stdout.split('\t')[0]) * 1024;
|
|
71
|
+
return size;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Send macOS notification
|
|
79
|
+
*/
|
|
80
|
+
async function sendNotification(title, message) {
|
|
81
|
+
try {
|
|
82
|
+
const script = `display notification "${message}" with title "${title}" sound name "default"`;
|
|
83
|
+
await execAsync(`osascript -e '${script}'`);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Notification failed
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Add watch
|
|
91
|
+
*/
|
|
92
|
+
async function addWatch(options) {
|
|
93
|
+
if (!options.path) {
|
|
94
|
+
error('--path is required');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!options.threshold) {
|
|
98
|
+
error('--threshold is required (e.g., 5GB)');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const path = expandPath(options.path);
|
|
102
|
+
if (!exists(path)) {
|
|
103
|
+
error(`Path does not exist: ${path}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const threshold = parseThreshold(options.threshold);
|
|
107
|
+
const watches = await loadWatchConfig();
|
|
108
|
+
// Check if already watching
|
|
109
|
+
if (watches.some((w) => w.path === path)) {
|
|
110
|
+
warning(`Already watching: ${path}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
watches.push({
|
|
114
|
+
path,
|
|
115
|
+
threshold,
|
|
116
|
+
notify: options.notify ?? true,
|
|
117
|
+
autoClean: options.autoClean ?? false,
|
|
118
|
+
});
|
|
119
|
+
await saveWatchConfig(watches);
|
|
120
|
+
success(`Added watch: ${path}`);
|
|
121
|
+
console.log(` Threshold: ${formatSize(threshold)}`);
|
|
122
|
+
console.log(` Notify: ${options.notify ?? true}`);
|
|
123
|
+
console.log(` Auto-clean: ${options.autoClean ?? false}`);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Remove watch
|
|
127
|
+
*/
|
|
128
|
+
async function removeWatch(pathToRemove) {
|
|
129
|
+
const expandedPath = expandPath(pathToRemove);
|
|
130
|
+
const watches = await loadWatchConfig();
|
|
131
|
+
const index = watches.findIndex((w) => w.path === expandedPath);
|
|
132
|
+
if (index === -1) {
|
|
133
|
+
error(`Not watching: ${expandedPath}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
watches.splice(index, 1);
|
|
137
|
+
await saveWatchConfig(watches);
|
|
138
|
+
success(`Removed watch: ${expandedPath}`);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* List watches
|
|
142
|
+
*/
|
|
143
|
+
async function listWatches() {
|
|
144
|
+
printHeader('👀 Directory Watches');
|
|
145
|
+
const watches = await loadWatchConfig();
|
|
146
|
+
if (watches.length === 0) {
|
|
147
|
+
warning('No watches configured');
|
|
148
|
+
console.log();
|
|
149
|
+
console.log(chalk.dim('Use "broom watch --add --path <path> --threshold 5GB" to add a watch'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
console.log();
|
|
153
|
+
for (const watch of watches) {
|
|
154
|
+
console.log(chalk.bold(`📁 ${watch.path}`));
|
|
155
|
+
console.log(` Threshold: ${chalk.cyan(formatSize(watch.threshold))}`);
|
|
156
|
+
console.log(` Notify: ${watch.notify ? chalk.green('Yes') : chalk.red('No')}`);
|
|
157
|
+
console.log(` Auto-clean: ${watch.autoClean ? chalk.green('Yes') : chalk.red('No')}`);
|
|
158
|
+
if (watch.lastNotified) {
|
|
159
|
+
console.log(` Last notified: ${chalk.dim(watch.lastNotified.toLocaleString())}`);
|
|
160
|
+
}
|
|
161
|
+
console.log();
|
|
162
|
+
}
|
|
163
|
+
separator();
|
|
164
|
+
console.log();
|
|
165
|
+
console.log(chalk.dim('Use "broom watch --check" to check all watches'));
|
|
166
|
+
console.log(chalk.dim('Use "broom watch --remove <path>" to remove a watch'));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Check watches
|
|
170
|
+
*/
|
|
171
|
+
async function checkWatches() {
|
|
172
|
+
printHeader('👀 Checking Watches');
|
|
173
|
+
const watches = await loadWatchConfig();
|
|
174
|
+
if (watches.length === 0) {
|
|
175
|
+
warning('No watches configured');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
console.log();
|
|
179
|
+
let needsSave = false;
|
|
180
|
+
for (const watch of watches) {
|
|
181
|
+
const size = await getDirectorySize(watch.path);
|
|
182
|
+
const percentage = (size / watch.threshold) * 100;
|
|
183
|
+
const exceeded = size > watch.threshold;
|
|
184
|
+
console.log(chalk.bold(`📁 ${watch.path}`));
|
|
185
|
+
console.log(` Current size: ${chalk.cyan(formatSize(size))}`);
|
|
186
|
+
console.log(` Threshold: ${formatSize(watch.threshold)}`);
|
|
187
|
+
if (exceeded) {
|
|
188
|
+
console.log(chalk.red(` ⚠️ EXCEEDED (${percentage.toFixed(1)}%)`));
|
|
189
|
+
// Send notification if enabled
|
|
190
|
+
if (watch.notify) {
|
|
191
|
+
const now = new Date();
|
|
192
|
+
const hoursSinceLastNotif = watch.lastNotified
|
|
193
|
+
? (now.getTime() - watch.lastNotified.getTime()) / (1000 * 60 * 60)
|
|
194
|
+
: 24;
|
|
195
|
+
// Only notify once per 6 hours
|
|
196
|
+
if (hoursSinceLastNotif >= 6) {
|
|
197
|
+
await sendNotification('Broom Alert', `${watch.path} exceeded ${formatSize(watch.threshold)}`);
|
|
198
|
+
watch.lastNotified = now;
|
|
199
|
+
needsSave = true;
|
|
200
|
+
console.log(chalk.dim(' 📬 Notification sent'));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Auto-clean if enabled
|
|
204
|
+
if (watch.autoClean) {
|
|
205
|
+
console.log(chalk.dim(' 🧹 Auto-clean triggered'));
|
|
206
|
+
// TODO: Trigger cleanup for this path
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
console.log(chalk.green(` ✓ OK (${percentage.toFixed(1)}%)`));
|
|
211
|
+
}
|
|
212
|
+
console.log();
|
|
213
|
+
}
|
|
214
|
+
if (needsSave) {
|
|
215
|
+
await saveWatchConfig(watches);
|
|
216
|
+
}
|
|
217
|
+
separator();
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Execute watch command
|
|
221
|
+
*/
|
|
222
|
+
export async function watchCommand(options) {
|
|
223
|
+
if (options.add) {
|
|
224
|
+
await addWatch(options);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (options.remove) {
|
|
228
|
+
await removeWatch(options.remove);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (options.check) {
|
|
232
|
+
await checkWatches();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (options.list || Object.keys(options).length === 0) {
|
|
236
|
+
await listWatches();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Create watch command
|
|
242
|
+
*/
|
|
243
|
+
export function createWatchCommand() {
|
|
244
|
+
const cmd = new Command('watch')
|
|
245
|
+
.description('Monitor directory sizes')
|
|
246
|
+
.option('-a, --add', 'Add a new watch')
|
|
247
|
+
.option('-r, --remove <path>', 'Remove a watch')
|
|
248
|
+
.option('-l, --list', 'List all watches')
|
|
249
|
+
.option('-c, --check', 'Check all watches now')
|
|
250
|
+
.option('-p, --path <path>', 'Path to watch')
|
|
251
|
+
.option('-t, --threshold <size>', 'Size threshold (e.g., 5GB)')
|
|
252
|
+
.option('-n, --notify', 'Enable notifications (default: true)')
|
|
253
|
+
.option('--auto-clean', 'Automatically clean when threshold exceeded')
|
|
254
|
+
.action(async (options) => {
|
|
255
|
+
await watchCommand(options);
|
|
256
|
+
});
|
|
257
|
+
return enhanceCommandHelp(cmd);
|
|
258
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* broom - macOS Disk Cleanup CLI
|
|
4
|
+
*
|
|
5
|
+
* A TypeScript rewrite of mole (https://github.com/tw93/Mole)
|
|
6
|
+
* with modern features and interactive interface.
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { createCleanCommand, createUninstallCommand, createOptimizeCommand, createAnalyzeCommand, createStatusCommand, createPurgeCommand, createInstallerCommand, createTouchIdCommand, createCompletionCommand, createUpdateCommand, createRemoveCommand, createConfigCommand, createDoctorCommand, createBackupCommand, createRestoreCommand, createDuplicatesCommand, createScheduleCommand, createWatchCommand, createReportsCommand, createHelpCommand, setCommandsList, } from './commands/index.js';
|
|
11
|
+
import { enableDebug, debug } from './utils/debug.js';
|
|
12
|
+
import { getGlobalOptionsTable } from './utils/help.js';
|
|
13
|
+
const VERSION = '1.0.0';
|
|
14
|
+
// ASCII art logo
|
|
15
|
+
const logo = chalk.cyan(`
|
|
16
|
+
██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗
|
|
17
|
+
██╔══██╗██╔══██╗██╔═══██╗██╔═══██╗████╗ ████║
|
|
18
|
+
██████╔╝██████╔╝██║ ██║██║ ██║██╔████╔██║
|
|
19
|
+
██╔══██╗██╔══██╗██║ ██║██║ ██║██║╚██╔╝██║
|
|
20
|
+
██████╔╝██║ ██║╚██████╔╝╚██████╔╝██║ ╚═╝ ██║
|
|
21
|
+
╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
|
|
22
|
+
`);
|
|
23
|
+
const description = `
|
|
24
|
+
${logo}
|
|
25
|
+
${chalk.bold('🧹 macOS Disk Cleanup CLI')}
|
|
26
|
+
|
|
27
|
+
Clean up your Mac with ease. Remove caches, logs, trash,
|
|
28
|
+
browser data, dev artifacts, and more.
|
|
29
|
+
|
|
30
|
+
${chalk.bold('Commands:')}
|
|
31
|
+
clean Scan and clean up disk space
|
|
32
|
+
uninstall Remove apps and their leftovers
|
|
33
|
+
optimize System maintenance and optimization
|
|
34
|
+
analyze Analyze disk space usage
|
|
35
|
+
status Show system status and resource usage
|
|
36
|
+
purge Clean project-specific build artifacts
|
|
37
|
+
installer Find and remove installer files
|
|
38
|
+
touchid Configure Touch ID for sudo
|
|
39
|
+
completion Generate shell completion scripts
|
|
40
|
+
update Self-update broom to the latest version
|
|
41
|
+
remove Uninstall broom from the system
|
|
42
|
+
config Manage broom configuration
|
|
43
|
+
doctor Run system health diagnostics
|
|
44
|
+
backup Manage file backups
|
|
45
|
+
restore Restore files from backup
|
|
46
|
+
duplicates Find and remove duplicate files
|
|
47
|
+
schedule Schedule automated cleanups
|
|
48
|
+
watch Monitor directory sizes
|
|
49
|
+
reports Manage cleanup reports
|
|
50
|
+
|
|
51
|
+
${chalk.bold('Examples:')}
|
|
52
|
+
${chalk.dim('$')} broom clean Interactive cleanup
|
|
53
|
+
${chalk.dim('$')} broom clean --dry-run Preview what would be cleaned
|
|
54
|
+
${chalk.dim('$')} broom clean --all Clean all categories
|
|
55
|
+
${chalk.dim('$')} broom uninstall Remove an app completely
|
|
56
|
+
${chalk.dim('$')} broom optimize Run system optimization tasks
|
|
57
|
+
${chalk.dim('$')} broom analyze See what's using disk space
|
|
58
|
+
${chalk.dim('$')} broom status --watch Live system monitoring
|
|
59
|
+
${chalk.dim('$')} broom purge Clean project artifacts
|
|
60
|
+
|
|
61
|
+
${getGlobalOptionsTable()}
|
|
62
|
+
`;
|
|
63
|
+
// Create program
|
|
64
|
+
const program = new Command();
|
|
65
|
+
program
|
|
66
|
+
.name('broom')
|
|
67
|
+
.version(VERSION, '-v, --version', 'Output the current version')
|
|
68
|
+
.description(description)
|
|
69
|
+
.option('--debug', 'Enable debug mode with detailed logs')
|
|
70
|
+
.helpOption('-h, --help', 'Display help for command')
|
|
71
|
+
.hook('preAction', (thisCommand) => {
|
|
72
|
+
const opts = thisCommand.opts();
|
|
73
|
+
if (opts.debug) {
|
|
74
|
+
enableDebug();
|
|
75
|
+
debug('Debug mode enabled');
|
|
76
|
+
debug(`Version: ${VERSION}`);
|
|
77
|
+
debug(`Node: ${process.version}`);
|
|
78
|
+
debug(`Platform: ${process.platform} ${process.arch}`);
|
|
79
|
+
debug(`Args: ${process.argv.slice(2).join(' ')}`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// Register commands
|
|
83
|
+
const helpCommand = createHelpCommand();
|
|
84
|
+
program.addCommand(helpCommand);
|
|
85
|
+
program.addCommand(createCleanCommand());
|
|
86
|
+
program.addCommand(createUninstallCommand());
|
|
87
|
+
program.addCommand(createOptimizeCommand());
|
|
88
|
+
program.addCommand(createAnalyzeCommand());
|
|
89
|
+
program.addCommand(createStatusCommand());
|
|
90
|
+
program.addCommand(createPurgeCommand());
|
|
91
|
+
program.addCommand(createInstallerCommand());
|
|
92
|
+
program.addCommand(createTouchIdCommand());
|
|
93
|
+
program.addCommand(createCompletionCommand());
|
|
94
|
+
program.addCommand(createUpdateCommand());
|
|
95
|
+
program.addCommand(createRemoveCommand());
|
|
96
|
+
program.addCommand(createConfigCommand());
|
|
97
|
+
program.addCommand(createDoctorCommand());
|
|
98
|
+
program.addCommand(createBackupCommand());
|
|
99
|
+
program.addCommand(createRestoreCommand());
|
|
100
|
+
program.addCommand(createDuplicatesCommand());
|
|
101
|
+
program.addCommand(createScheduleCommand());
|
|
102
|
+
program.addCommand(createWatchCommand());
|
|
103
|
+
program.addCommand(createReportsCommand());
|
|
104
|
+
// Set the commands list for the help command
|
|
105
|
+
setCommandsList([
|
|
106
|
+
createCleanCommand(),
|
|
107
|
+
createUninstallCommand(),
|
|
108
|
+
createOptimizeCommand(),
|
|
109
|
+
createAnalyzeCommand(),
|
|
110
|
+
createStatusCommand(),
|
|
111
|
+
createPurgeCommand(),
|
|
112
|
+
createInstallerCommand(),
|
|
113
|
+
createTouchIdCommand(),
|
|
114
|
+
createCompletionCommand(),
|
|
115
|
+
createUpdateCommand(),
|
|
116
|
+
createRemoveCommand(),
|
|
117
|
+
createConfigCommand(),
|
|
118
|
+
createDoctorCommand(),
|
|
119
|
+
createBackupCommand(),
|
|
120
|
+
createRestoreCommand(),
|
|
121
|
+
createDuplicatesCommand(),
|
|
122
|
+
createScheduleCommand(),
|
|
123
|
+
createWatchCommand(),
|
|
124
|
+
createReportsCommand(),
|
|
125
|
+
]);
|
|
126
|
+
// Parse arguments
|
|
127
|
+
program.parse(process.argv);
|
|
128
|
+
// Show help if no command provided (only if no subcommand was executed)
|
|
129
|
+
if (process.argv.length === 2) {
|
|
130
|
+
console.log(description);
|
|
131
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { removeItems } from '../utils/fs.js';
|
|
2
|
+
export class BaseScanner {
|
|
3
|
+
async clean(items, dryRun = false) {
|
|
4
|
+
const result = await removeItems(items, dryRun);
|
|
5
|
+
return {
|
|
6
|
+
category: this.category,
|
|
7
|
+
cleanedItems: result.success,
|
|
8
|
+
freedSpace: result.freedSpace,
|
|
9
|
+
errors: result.failed > 0 ? [`Failed to remove ${result.failed} items`] : [],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
createResult(items, error) {
|
|
13
|
+
const totalSize = items.reduce((sum, item) => sum + item.size, 0);
|
|
14
|
+
return {
|
|
15
|
+
category: this.category,
|
|
16
|
+
items,
|
|
17
|
+
totalSize,
|
|
18
|
+
error,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser cache scanner
|
|
3
|
+
*/
|
|
4
|
+
import { readdir, stat } from 'fs/promises';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { BaseScanner } from './base.js';
|
|
7
|
+
import { paths } from '../utils/paths.js';
|
|
8
|
+
import { exists, getSize } from '../utils/fs.js';
|
|
9
|
+
export class BrowserCacheScanner extends BaseScanner {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
this.category = {
|
|
13
|
+
id: 'browser-cache',
|
|
14
|
+
name: 'Browser Cache',
|
|
15
|
+
group: 'Browsers',
|
|
16
|
+
description: 'Cache from Chrome, Safari, Firefox, Edge, Brave, Arc',
|
|
17
|
+
safetyLevel: 'safe',
|
|
18
|
+
};
|
|
19
|
+
this.browsers = [
|
|
20
|
+
{
|
|
21
|
+
name: 'Chrome',
|
|
22
|
+
paths: [paths.browserCache.chrome, paths.browserCache.chromeProfile],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'Safari',
|
|
26
|
+
paths: [paths.browserCache.safari],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'Firefox',
|
|
30
|
+
paths: [paths.browserCache.firefox],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'Edge',
|
|
34
|
+
paths: [paths.browserCache.edge, paths.browserCache.edgeProfile],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'Brave',
|
|
38
|
+
paths: [paths.browserCache.brave, paths.browserCache.braveProfile],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'Arc',
|
|
42
|
+
paths: [paths.browserCache.arc, paths.browserCache.arcProfile],
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
async scan(_options) {
|
|
47
|
+
const items = [];
|
|
48
|
+
for (const browser of this.browsers) {
|
|
49
|
+
for (const browserPath of browser.paths) {
|
|
50
|
+
try {
|
|
51
|
+
if (!exists(browserPath)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Check if it's a directory that contains cache
|
|
55
|
+
const stats = await stat(browserPath);
|
|
56
|
+
if (stats.isDirectory()) {
|
|
57
|
+
const entries = await readdir(browserPath);
|
|
58
|
+
// Look for cache-related directories
|
|
59
|
+
const cachePatterns = ['Cache', 'cache', 'GPUCache', 'ShaderCache', 'Code Cache'];
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (cachePatterns.some((p) => entry.includes(p)) || entry === 'Cache') {
|
|
62
|
+
const entryPath = join(browserPath, entry);
|
|
63
|
+
const entryStats = await stat(entryPath);
|
|
64
|
+
const size = await getSize(entryPath);
|
|
65
|
+
if (size > 0) {
|
|
66
|
+
items.push({
|
|
67
|
+
path: entryPath,
|
|
68
|
+
size,
|
|
69
|
+
name: `${browser.name} - ${entry}`,
|
|
70
|
+
isDirectory: entryStats.isDirectory(),
|
|
71
|
+
modifiedAt: entryStats.mtime,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Also add the main cache directory if it's not too nested
|
|
77
|
+
if (browserPath.includes('Cache')) {
|
|
78
|
+
const size = await getSize(browserPath);
|
|
79
|
+
if (size > 0) {
|
|
80
|
+
items.push({
|
|
81
|
+
path: browserPath,
|
|
82
|
+
size,
|
|
83
|
+
name: `${browser.name} Cache`,
|
|
84
|
+
isDirectory: true,
|
|
85
|
+
modifiedAt: stats.mtime,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Skip if cannot access
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Remove duplicates (prefer larger paths)
|
|
97
|
+
const uniqueItems = this.deduplicateItems(items);
|
|
98
|
+
uniqueItems.sort((a, b) => b.size - a.size);
|
|
99
|
+
return this.createResult(uniqueItems);
|
|
100
|
+
}
|
|
101
|
+
deduplicateItems(items) {
|
|
102
|
+
const seen = new Map();
|
|
103
|
+
for (const item of items) {
|
|
104
|
+
const existing = seen.get(item.path);
|
|
105
|
+
if (!existing || item.size > existing.size) {
|
|
106
|
+
seen.set(item.path, item);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return Array.from(seen.values());
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Development cache scanner
|
|
3
|
+
*/
|
|
4
|
+
import { stat } from 'fs/promises';
|
|
5
|
+
import { BaseScanner } from './base.js';
|
|
6
|
+
import { paths } from '../utils/paths.js';
|
|
7
|
+
import { exists, getSize } from '../utils/fs.js';
|
|
8
|
+
export class DevCacheScanner extends BaseScanner {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.category = {
|
|
12
|
+
id: 'dev-cache',
|
|
13
|
+
name: 'Development Cache',
|
|
14
|
+
group: 'Development',
|
|
15
|
+
description: 'Package manager caches (npm, yarn, pip, cargo, etc.)',
|
|
16
|
+
safetyLevel: 'moderate',
|
|
17
|
+
safetyNote: 'May require reinstalling packages',
|
|
18
|
+
};
|
|
19
|
+
this.devTools = [
|
|
20
|
+
{ name: 'npm cache', path: paths.devCache.npm, safeToClean: true },
|
|
21
|
+
{ name: 'npm _cacache', path: paths.devCache.npmCache, safeToClean: true },
|
|
22
|
+
{ name: 'Yarn cache', path: paths.devCache.yarn, safeToClean: true },
|
|
23
|
+
{ name: 'pnpm store', path: paths.devCache.pnpm, safeToClean: true },
|
|
24
|
+
{ name: 'Bun cache', path: paths.devCache.bun, safeToClean: true },
|
|
25
|
+
{ name: 'pip cache', path: paths.devCache.pip, safeToClean: true },
|
|
26
|
+
{ name: 'pip cache (alt)', path: paths.devCache.pipCache, safeToClean: true },
|
|
27
|
+
{ name: 'Cargo cache', path: paths.devCache.cargo, safeToClean: true },
|
|
28
|
+
{ name: 'Rustup downloads', path: paths.devCache.rustup, safeToClean: true },
|
|
29
|
+
{ name: 'Go mod cache', path: paths.devCache.go, safeToClean: true },
|
|
30
|
+
{ name: 'Gradle caches', path: paths.devCache.gradle, safeToClean: true },
|
|
31
|
+
{ name: 'Maven repository', path: paths.devCache.maven, safeToClean: false },
|
|
32
|
+
{ name: 'CocoaPods cache', path: paths.devCache.cocoapods, safeToClean: true },
|
|
33
|
+
{ name: 'Carthage cache', path: paths.devCache.carthage, safeToClean: true },
|
|
34
|
+
{ name: 'Composer cache', path: paths.devCache.composer, safeToClean: true },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
async scan(_options) {
|
|
38
|
+
const items = [];
|
|
39
|
+
for (const tool of this.devTools) {
|
|
40
|
+
try {
|
|
41
|
+
if (!exists(tool.path)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const stats = await stat(tool.path);
|
|
45
|
+
const size = await getSize(tool.path);
|
|
46
|
+
if (size > 1024 * 1024) {
|
|
47
|
+
// Only include if > 1MB
|
|
48
|
+
items.push({
|
|
49
|
+
path: tool.path,
|
|
50
|
+
size,
|
|
51
|
+
name: tool.name,
|
|
52
|
+
isDirectory: stats.isDirectory(),
|
|
53
|
+
modifiedAt: stats.mtime,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Skip if cannot access
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
items.sort((a, b) => b.size - a.size);
|
|
62
|
+
return this.createResult(items);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker cache scanner
|
|
3
|
+
*/
|
|
4
|
+
import { stat } from 'fs/promises';
|
|
5
|
+
import { BaseScanner } from './base.js';
|
|
6
|
+
import { paths } from '../utils/paths.js';
|
|
7
|
+
import { exists, getSize } from '../utils/fs.js';
|
|
8
|
+
import { exec } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
export class DockerScanner extends BaseScanner {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(...arguments);
|
|
14
|
+
this.category = {
|
|
15
|
+
id: 'docker',
|
|
16
|
+
name: 'Docker Data',
|
|
17
|
+
group: 'Development',
|
|
18
|
+
description: 'Docker images, containers, and volumes',
|
|
19
|
+
safetyLevel: 'risky',
|
|
20
|
+
safetyNote: 'Will remove all unused Docker data',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async scan(_options) {
|
|
24
|
+
const items = [];
|
|
25
|
+
try {
|
|
26
|
+
// Check Docker data directory
|
|
27
|
+
if (exists(paths.docker.data)) {
|
|
28
|
+
const stats = await stat(paths.docker.data);
|
|
29
|
+
const size = await getSize(paths.docker.data);
|
|
30
|
+
if (size > 0) {
|
|
31
|
+
items.push({
|
|
32
|
+
path: paths.docker.data,
|
|
33
|
+
size,
|
|
34
|
+
name: 'Docker Data',
|
|
35
|
+
isDirectory: true,
|
|
36
|
+
modifiedAt: stats.mtime,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Check VM disk specifically
|
|
41
|
+
if (exists(paths.docker.vmDisk)) {
|
|
42
|
+
const stats = await stat(paths.docker.vmDisk);
|
|
43
|
+
const size = await getSize(paths.docker.vmDisk);
|
|
44
|
+
if (size > 0) {
|
|
45
|
+
items.push({
|
|
46
|
+
path: paths.docker.vmDisk,
|
|
47
|
+
size,
|
|
48
|
+
name: 'Docker VM Disk',
|
|
49
|
+
isDirectory: true,
|
|
50
|
+
modifiedAt: stats.mtime,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Docker might not be installed
|
|
57
|
+
}
|
|
58
|
+
items.sort((a, b) => b.size - a.size);
|
|
59
|
+
// Deduplicate (vmDisk is inside data)
|
|
60
|
+
const uniqueItems = items.filter((item, index, self) => {
|
|
61
|
+
return !self.some((other, otherIndex) => otherIndex !== index && item.path.startsWith(other.path + '/'));
|
|
62
|
+
});
|
|
63
|
+
return this.createResult(uniqueItems);
|
|
64
|
+
}
|
|
65
|
+
async clean(items, dryRun = false) {
|
|
66
|
+
if (dryRun) {
|
|
67
|
+
const totalSize = items.reduce((sum, item) => sum + item.size, 0);
|
|
68
|
+
return {
|
|
69
|
+
category: this.category,
|
|
70
|
+
cleanedItems: items.length,
|
|
71
|
+
freedSpace: totalSize,
|
|
72
|
+
errors: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
// Try using docker system prune
|
|
77
|
+
await execAsync('docker system prune -af --volumes 2>/dev/null || true');
|
|
78
|
+
const totalSize = items.reduce((sum, item) => sum + item.size, 0);
|
|
79
|
+
return {
|
|
80
|
+
category: this.category,
|
|
81
|
+
cleanedItems: items.length,
|
|
82
|
+
freedSpace: totalSize,
|
|
83
|
+
errors: [],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Docker not running or not installed
|
|
88
|
+
return {
|
|
89
|
+
category: this.category,
|
|
90
|
+
cleanedItems: 0,
|
|
91
|
+
freedSpace: 0,
|
|
92
|
+
errors: ['Docker is not running or not installed'],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|