@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/dist/commands/analyze.js +371 -0
  4. package/dist/commands/backup.js +257 -0
  5. package/dist/commands/clean.js +255 -0
  6. package/dist/commands/completion.js +714 -0
  7. package/dist/commands/config.js +474 -0
  8. package/dist/commands/doctor.js +280 -0
  9. package/dist/commands/duplicates.js +325 -0
  10. package/dist/commands/help.js +34 -0
  11. package/dist/commands/index.js +22 -0
  12. package/dist/commands/installer.js +266 -0
  13. package/dist/commands/optimize.js +270 -0
  14. package/dist/commands/purge.js +271 -0
  15. package/dist/commands/remove.js +184 -0
  16. package/dist/commands/reports.js +173 -0
  17. package/dist/commands/schedule.js +249 -0
  18. package/dist/commands/status.js +468 -0
  19. package/dist/commands/touchid.js +230 -0
  20. package/dist/commands/uninstall.js +336 -0
  21. package/dist/commands/update.js +182 -0
  22. package/dist/commands/watch.js +258 -0
  23. package/dist/index.js +131 -0
  24. package/dist/scanners/base.js +21 -0
  25. package/dist/scanners/browser-cache.js +111 -0
  26. package/dist/scanners/dev-cache.js +64 -0
  27. package/dist/scanners/docker.js +96 -0
  28. package/dist/scanners/downloads.js +66 -0
  29. package/dist/scanners/homebrew.js +82 -0
  30. package/dist/scanners/index.js +126 -0
  31. package/dist/scanners/installer.js +87 -0
  32. package/dist/scanners/ios-backups.js +82 -0
  33. package/dist/scanners/node-modules.js +75 -0
  34. package/dist/scanners/temp-files.js +65 -0
  35. package/dist/scanners/trash.js +90 -0
  36. package/dist/scanners/user-cache.js +62 -0
  37. package/dist/scanners/user-logs.js +53 -0
  38. package/dist/scanners/xcode.js +124 -0
  39. package/dist/types/index.js +23 -0
  40. package/dist/ui/index.js +5 -0
  41. package/dist/ui/monitors.js +345 -0
  42. package/dist/ui/output.js +304 -0
  43. package/dist/ui/prompts.js +270 -0
  44. package/dist/utils/config.js +133 -0
  45. package/dist/utils/debug.js +119 -0
  46. package/dist/utils/fs.js +283 -0
  47. package/dist/utils/help.js +265 -0
  48. package/dist/utils/index.js +6 -0
  49. package/dist/utils/paths.js +142 -0
  50. package/dist/utils/report.js +404 -0
  51. package/package.json +87 -0
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Clean command - Deep system cleanup
3
+ */
4
+ import chalk from 'chalk';
5
+ import { Command } from 'commander';
6
+ import { runAllScans, getScanner, getAllScanners } from '../scanners/index.js';
7
+ import { formatSize } from '../utils/fs.js';
8
+ import { loadConfig, isWhitelisted } from '../utils/config.js';
9
+ import { debug, debugSection, debugObj, debugFile, debugRisk } from '../utils/debug.js';
10
+ import { enhanceCommandHelp } from '../utils/help.js';
11
+ import { ReportGenerator } from '../utils/report.js';
12
+ import { join } from 'path';
13
+ import { homedir } from 'os';
14
+ import { exec } from 'child_process';
15
+ import { promisify } from 'util';
16
+ const execAsync = promisify(exec);
17
+ import { printHeader, success, warning, info, separator, createSpinner, succeedSpinner, createProgress, printSummaryBlock, ICONS, } from '../ui/output.js';
18
+ import { selectCategories, confirmRemoval } from '../ui/prompts.js';
19
+ /**
20
+ * Filter whitelisted items
21
+ */
22
+ function filterWhitelisted(items, whitelist) {
23
+ return items.filter((item) => !isWhitelisted(item.path, whitelist));
24
+ }
25
+ /**
26
+ * Print scan results
27
+ */
28
+ function printScanResults(results) {
29
+ console.log();
30
+ console.log(chalk.bold('Scan Results:'));
31
+ separator('─', 60);
32
+ for (const result of results) {
33
+ const sizeStr = formatSize(result.totalSize);
34
+ const countStr = `${result.items.length} items`;
35
+ if (result.items.length > 0) {
36
+ const safetyIcon = result.category.safetyLevel === 'safe'
37
+ ? chalk.green('●')
38
+ : result.category.safetyLevel === 'moderate'
39
+ ? chalk.yellow('●')
40
+ : chalk.red('●');
41
+ console.log(` ${safetyIcon} ${result.category.name.padEnd(30)} ${chalk.yellow(sizeStr.padStart(12))} ${chalk.dim(`(${countStr})`)}`);
42
+ }
43
+ }
44
+ separator('─', 60);
45
+ }
46
+ /**
47
+ * Execute clean command
48
+ */
49
+ export async function cleanCommand(options) {
50
+ const config = await loadConfig();
51
+ const isDryRun = options.dryRun || config.dryRun;
52
+ debugSection('Clean Command');
53
+ debugObj('Options', options);
54
+ debugObj('Config', config);
55
+ printHeader(isDryRun ? '🧹 Clean Your Mac (Dry Run)' : '🧹 Clean Your Mac');
56
+ if (isDryRun) {
57
+ console.log(chalk.yellow(`${ICONS.dryRun} Dry Run Mode - Preview only, no deletions`));
58
+ console.log();
59
+ }
60
+ // Scan phase
61
+ const scanners = getAllScanners();
62
+ const progress = createProgress(scanners.length);
63
+ console.log(chalk.cyan('Scanning for cleanable files...\n'));
64
+ debug(`Starting scan with ${scanners.length} scanners`);
65
+ const summary = await runAllScans({
66
+ parallel: true,
67
+ concurrency: 4,
68
+ onProgress: (completed, total, scanner) => {
69
+ debug(`Scan progress: ${completed}/${total} - ${scanner.category.name}`);
70
+ progress.update(completed, `Scanning ${scanner.category.name}...`);
71
+ },
72
+ });
73
+ progress.finish('Scan complete');
74
+ // Filter out empty results and apply whitelist
75
+ let resultsWithItems = summary.results
76
+ .filter((r) => r.items.length > 0)
77
+ .map((r) => ({
78
+ ...r,
79
+ items: filterWhitelisted(r.items, config.whitelist),
80
+ }))
81
+ .filter((r) => r.items.length > 0);
82
+ // Recalculate totals after filtering
83
+ resultsWithItems = resultsWithItems.map((r) => ({
84
+ ...r,
85
+ totalSize: r.items.reduce((sum, item) => sum + item.size, 0),
86
+ }));
87
+ if (resultsWithItems.length === 0) {
88
+ console.log();
89
+ success('Your Mac is already clean!');
90
+ return;
91
+ }
92
+ // Filter risky categories unless --unsafe
93
+ const riskyResults = resultsWithItems.filter((r) => r.category.safetyLevel === 'risky');
94
+ const safeResults = resultsWithItems.filter((r) => r.category.safetyLevel !== 'risky');
95
+ if (!options.unsafe && riskyResults.length > 0) {
96
+ const riskySize = riskyResults.reduce((sum, r) => sum + r.totalSize, 0);
97
+ console.log();
98
+ console.log(chalk.yellow(`${ICONS.warning} Skipping risky categories (use --unsafe to include):`));
99
+ for (const result of riskyResults) {
100
+ console.log(chalk.dim(` ${chalk.red('●')} ${result.category.name}: ${formatSize(result.totalSize)}`));
101
+ if (result.category.safetyNote) {
102
+ console.log(chalk.dim.italic(` ${result.category.safetyNote}`));
103
+ }
104
+ }
105
+ console.log(chalk.dim(` Total skipped: ${formatSize(riskySize)}`));
106
+ resultsWithItems = safeResults;
107
+ }
108
+ if (resultsWithItems.length === 0) {
109
+ console.log();
110
+ success('Nothing safe to clean!');
111
+ return;
112
+ }
113
+ printScanResults(resultsWithItems);
114
+ const totalSize = resultsWithItems.reduce((sum, r) => sum + r.totalSize, 0);
115
+ const totalItems = resultsWithItems.reduce((sum, r) => sum + r.items.length, 0);
116
+ info(`Found ${formatSize(totalSize)} in ${totalItems} items`);
117
+ console.log();
118
+ // Select categories
119
+ let selectedResults;
120
+ if (options.all) {
121
+ selectedResults = resultsWithItems;
122
+ }
123
+ else {
124
+ selectedResults = await selectCategories(resultsWithItems);
125
+ }
126
+ if (selectedResults.length === 0) {
127
+ warning('No categories selected');
128
+ return;
129
+ }
130
+ const selectedSize = selectedResults.reduce((sum, r) => sum + r.totalSize, 0);
131
+ const selectedItems = selectedResults.reduce((sum, r) => sum + r.items.length, 0);
132
+ // Confirm removal
133
+ if (!options.yes && !isDryRun) {
134
+ const confirmed = await confirmRemoval(selectedItems, selectedSize);
135
+ if (!confirmed) {
136
+ warning('Cleanup cancelled');
137
+ return;
138
+ }
139
+ }
140
+ // Initialize report generator if requested
141
+ let reportGen = null;
142
+ let diskBefore = null;
143
+ if (options.report) {
144
+ reportGen = new ReportGenerator();
145
+ // Get disk info before cleanup
146
+ try {
147
+ const { stdout } = await execAsync("df -k / | tail -1 | awk '{print $2,$3,$4}'");
148
+ const [total, used, free] = stdout
149
+ .trim()
150
+ .split(' ')
151
+ .map((n) => parseInt(n) * 1024);
152
+ diskBefore = {
153
+ total,
154
+ used,
155
+ free,
156
+ percentage: (used / total) * 100,
157
+ };
158
+ reportGen.recordDiskBefore(diskBefore);
159
+ }
160
+ catch (e) {
161
+ debug('Failed to get disk info:', e);
162
+ }
163
+ }
164
+ // Clean phase
165
+ console.log();
166
+ const cleanSpinner = createSpinner(isDryRun ? 'Simulating cleanup...' : 'Cleaning...');
167
+ const cleanResults = {
168
+ results: [],
169
+ totalFreedSpace: 0,
170
+ totalCleanedItems: 0,
171
+ totalErrors: 0,
172
+ };
173
+ for (const result of selectedResults) {
174
+ const scanner = getScanner(result.category.id);
175
+ if (!scanner) {
176
+ debug(`Scanner not found for ${result.category.id}`);
177
+ continue;
178
+ }
179
+ debug(`Cleaning ${result.category.name}: ${result.items.length} items`);
180
+ for (const item of result.items) {
181
+ debugFile('delete', item.path, formatSize(item.size));
182
+ debugRisk(item.path, result.category.safetyLevel);
183
+ // Record in report
184
+ if (reportGen) {
185
+ reportGen.recordDeletion(item.path, item.size, result.category.name);
186
+ }
187
+ }
188
+ const cleanResult = await scanner.clean(result.items, isDryRun);
189
+ debug(`Cleaned ${cleanResult.cleanedItems} items, freed ${formatSize(cleanResult.freedSpace)}`);
190
+ cleanResults.results.push(cleanResult);
191
+ cleanResults.totalFreedSpace += cleanResult.freedSpace;
192
+ cleanResults.totalCleanedItems += cleanResult.cleanedItems;
193
+ cleanResults.totalErrors += cleanResult.errors.length;
194
+ }
195
+ succeedSpinner(cleanSpinner, isDryRun ? 'Simulation complete' : 'Cleanup complete');
196
+ // Print summary
197
+ const summaryHeading = isDryRun ? 'Dry Run Complete - No Changes Made' : 'Cleanup Complete';
198
+ const summaryDetails = [
199
+ `Space ${isDryRun ? 'would be ' : ''}freed: ${chalk.green(formatSize(cleanResults.totalFreedSpace))}`,
200
+ `Items cleaned: ${cleanResults.totalCleanedItems}`,
201
+ `Categories: ${cleanResults.results.length}`,
202
+ ];
203
+ if (cleanResults.totalErrors > 0) {
204
+ summaryDetails.push(`${chalk.red(`Errors: ${cleanResults.totalErrors}`)}`);
205
+ }
206
+ printSummaryBlock(summaryHeading, summaryDetails);
207
+ // Generate HTML report if requested
208
+ if (reportGen) {
209
+ try {
210
+ const reportDir = join(homedir(), '.broom', 'reports');
211
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
212
+ const reportPath = join(reportDir, `cleanup-${timestamp}.html`);
213
+ // Get disk info after cleanup
214
+ const { stdout } = await execAsync("df -k / | tail -1 | awk '{print $2,$3,$4}'");
215
+ const [total, used, free] = stdout
216
+ .trim()
217
+ .split(' ')
218
+ .map((n) => parseInt(n) * 1024);
219
+ const diskAfter = {
220
+ total,
221
+ used,
222
+ free,
223
+ percentage: (used / total) * 100,
224
+ };
225
+ await reportGen.generate(reportPath, diskAfter);
226
+ console.log();
227
+ success(`Report generated: ${chalk.cyan(reportPath)}`);
228
+ // Open in browser if requested
229
+ if (options.open) {
230
+ await execAsync(`open "${reportPath}"`);
231
+ info('Report opened in browser');
232
+ }
233
+ }
234
+ catch (e) {
235
+ warning(`Failed to generate report: ${e}`);
236
+ }
237
+ }
238
+ }
239
+ /**
240
+ * Create clean command
241
+ */
242
+ export function createCleanCommand() {
243
+ const cmd = new Command('clean')
244
+ .description('Deep system cleanup')
245
+ .option('-n, --dry-run', 'Preview only, no deletions')
246
+ .option('-a, --all', 'Clean all categories without prompting')
247
+ .option('-y, --yes', 'Skip confirmation prompts')
248
+ .option('--unsafe', 'Include risky categories in cleanup')
249
+ .option('-r, --report', 'Generate HTML report after cleanup')
250
+ .option('-o, --open', 'Open report in browser after generation')
251
+ .action(async (options) => {
252
+ await cleanCommand(options);
253
+ });
254
+ return enhanceCommandHelp(cmd);
255
+ }