@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,266 @@
1
+ /**
2
+ * Installer command - Find and remove installer files
3
+ */
4
+ import chalk from 'chalk';
5
+ import { Command } from 'commander';
6
+ import { readdir, stat, rm } from 'fs/promises';
7
+ import { join, basename, extname } from 'path';
8
+ import fg from 'fast-glob';
9
+ import { enhanceCommandHelp } from '../utils/help.js';
10
+ import { exists, getSize, formatSize, expandPath } from '../utils/fs.js';
11
+ import { printHeader, success, warning, error, info, separator, createSpinner, succeedSpinner, failSpinner, } from '../ui/output.js';
12
+ import { confirmAction, selectFiles } from '../ui/prompts.js';
13
+ /**
14
+ * Installer file extensions
15
+ */
16
+ const INSTALLER_EXTENSIONS = ['.dmg', '.pkg', '.zip', '.app', '.tar.gz', '.tgz'];
17
+ /**
18
+ * Directories to search for installers
19
+ */
20
+ const SEARCH_DIRS = [
21
+ '~/Downloads',
22
+ '~/Desktop',
23
+ '~/Library/Caches/Homebrew/downloads',
24
+ '~/Library/Mobile Documents/com~apple~CloudDocs/Downloads',
25
+ '~/Library/Application Support/Steam/steamapps',
26
+ ];
27
+ /**
28
+ * Get source label from path
29
+ */
30
+ function getSourceLabel(filePath) {
31
+ const home = expandPath('~');
32
+ const path = filePath.toLowerCase();
33
+ if (path.includes('/downloads')) {
34
+ return 'Downloads';
35
+ }
36
+ if (path.includes('/desktop')) {
37
+ return 'Desktop';
38
+ }
39
+ if (path.includes('/homebrew')) {
40
+ return 'Homebrew';
41
+ }
42
+ if (path.includes('/clouddocs') || path.includes('/icloud')) {
43
+ return 'iCloud';
44
+ }
45
+ if (path.includes('/steam')) {
46
+ return 'Steam';
47
+ }
48
+ if (path.includes('/mail')) {
49
+ return 'Mail';
50
+ }
51
+ return 'Other';
52
+ }
53
+ /**
54
+ * Check if file is an installer
55
+ */
56
+ function isInstaller(filename) {
57
+ const ext = extname(filename).toLowerCase();
58
+ const name = filename.toLowerCase();
59
+ // Check extension
60
+ if (INSTALLER_EXTENSIONS.includes(ext)) {
61
+ return true;
62
+ }
63
+ // Check for .tar.gz
64
+ if (name.endsWith('.tar.gz') || name.endsWith('.tgz')) {
65
+ return true;
66
+ }
67
+ return false;
68
+ }
69
+ /**
70
+ * Scan for installer files
71
+ */
72
+ async function scanInstallers() {
73
+ const installers = [];
74
+ for (const searchDir of SEARCH_DIRS) {
75
+ const dir = expandPath(searchDir);
76
+ if (!exists(dir)) {
77
+ continue;
78
+ }
79
+ try {
80
+ const entries = await readdir(dir);
81
+ for (const entry of entries) {
82
+ if (!isInstaller(entry)) {
83
+ continue;
84
+ }
85
+ const filePath = join(dir, entry);
86
+ try {
87
+ const stats = await stat(filePath);
88
+ const size = stats.isDirectory() ? await getSize(filePath) : stats.size;
89
+ // Only include files larger than 10MB
90
+ if (size < 10 * 1024 * 1024) {
91
+ continue;
92
+ }
93
+ installers.push({
94
+ path: filePath,
95
+ name: entry,
96
+ size,
97
+ isDirectory: stats.isDirectory(),
98
+ modifiedAt: stats.mtime,
99
+ source: getSourceLabel(filePath),
100
+ });
101
+ }
102
+ catch {
103
+ // Skip if cannot access
104
+ }
105
+ }
106
+ }
107
+ catch {
108
+ // Skip if cannot access directory
109
+ }
110
+ }
111
+ // Also search recursively in Downloads for nested installers
112
+ const downloadsDir = expandPath('~/Downloads');
113
+ if (exists(downloadsDir)) {
114
+ try {
115
+ const patterns = INSTALLER_EXTENSIONS.map((ext) => `**/*${ext}`);
116
+ const matches = await fg(patterns, {
117
+ cwd: downloadsDir,
118
+ absolute: true,
119
+ deep: 3, // Max depth
120
+ ignore: ['**/node_modules/**'],
121
+ });
122
+ for (const match of matches) {
123
+ // Skip if already added
124
+ if (installers.some((i) => i.path === match)) {
125
+ continue;
126
+ }
127
+ try {
128
+ const stats = await stat(match);
129
+ const size = stats.isDirectory() ? await getSize(match) : stats.size;
130
+ if (size < 10 * 1024 * 1024) {
131
+ continue;
132
+ }
133
+ installers.push({
134
+ path: match,
135
+ name: basename(match),
136
+ size,
137
+ isDirectory: stats.isDirectory(),
138
+ modifiedAt: stats.mtime,
139
+ source: 'Downloads',
140
+ });
141
+ }
142
+ catch {
143
+ // Skip
144
+ }
145
+ }
146
+ }
147
+ catch {
148
+ // Skip
149
+ }
150
+ }
151
+ // Sort by size descending
152
+ installers.sort((a, b) => b.size - a.size);
153
+ return installers;
154
+ }
155
+ /**
156
+ * Execute installer command
157
+ */
158
+ export async function installerCommand(options) {
159
+ const isDryRun = options.dryRun || false;
160
+ printHeader(isDryRun ? '💿 Installer Files (Dry Run)' : '💿 Installer Files');
161
+ // Scan for installers
162
+ const spinner = createSpinner('Scanning for installer files...');
163
+ const installers = await scanInstallers();
164
+ succeedSpinner(spinner, `Found ${installers.length} installer files`);
165
+ if (installers.length === 0) {
166
+ console.log();
167
+ success('No installer files found!');
168
+ return;
169
+ }
170
+ // Calculate total size
171
+ const totalSize = installers.reduce((sum, i) => sum + i.size, 0);
172
+ // Show found installers
173
+ console.log();
174
+ console.log(chalk.bold(`Found ${installers.length} installer files (${formatSize(totalSize)}):`));
175
+ console.log();
176
+ for (const installer of installers.slice(0, 15)) {
177
+ const sizeStr = formatSize(installer.size).padStart(10);
178
+ const sourceStr = chalk.dim(`| ${installer.source}`);
179
+ console.log(` ${chalk.cyan('●')} ${installer.name.slice(0, 30).padEnd(32)} ${chalk.yellow(sizeStr)} ${sourceStr}`);
180
+ }
181
+ if (installers.length > 15) {
182
+ console.log(chalk.dim(` ... and ${installers.length - 15} more`));
183
+ }
184
+ // Select files to remove
185
+ console.log();
186
+ let selectedInstallers;
187
+ if (options.yes) {
188
+ selectedInstallers = installers;
189
+ }
190
+ else {
191
+ info('Select installers to remove:');
192
+ selectedInstallers = (await selectFiles(installers));
193
+ }
194
+ if (selectedInstallers.length === 0) {
195
+ warning('No installers selected');
196
+ return;
197
+ }
198
+ const selectedSize = selectedInstallers.reduce((sum, i) => sum + i.size, 0);
199
+ // Confirm
200
+ if (!options.yes) {
201
+ console.log();
202
+ const confirmed = await confirmAction(`${isDryRun ? 'Simulate removing' : 'Remove'} ${selectedInstallers.length} installers (${formatSize(selectedSize)})?`, false);
203
+ if (!confirmed) {
204
+ warning('Operation cancelled');
205
+ return;
206
+ }
207
+ }
208
+ // Execute removal
209
+ console.log();
210
+ const removeSpinner = createSpinner(isDryRun ? 'Simulating removal...' : 'Removing installers...');
211
+ let removedCount = 0;
212
+ let removedSize = 0;
213
+ const errors = [];
214
+ for (const installer of selectedInstallers) {
215
+ if (!isDryRun) {
216
+ try {
217
+ await rm(installer.path, { recursive: true, force: true });
218
+ removedCount++;
219
+ removedSize += installer.size;
220
+ }
221
+ catch (err) {
222
+ errors.push(`${installer.name}: ${err.message}`);
223
+ }
224
+ }
225
+ else {
226
+ removedCount++;
227
+ removedSize += installer.size;
228
+ }
229
+ }
230
+ if (errors.length === 0) {
231
+ succeedSpinner(removeSpinner, isDryRun ? 'Simulation complete' : 'Removal complete');
232
+ }
233
+ else {
234
+ failSpinner(removeSpinner, 'Completed with errors');
235
+ }
236
+ // Summary
237
+ console.log();
238
+ separator('═');
239
+ console.log(chalk.bold(isDryRun ? 'Dry Run Complete' : 'Removal Complete'));
240
+ separator('─');
241
+ console.log(` Installers ${isDryRun ? 'would be ' : ''}removed: ${chalk.green(removedCount)}`);
242
+ console.log(` Space ${isDryRun ? 'would be ' : ''}freed: ${chalk.green(formatSize(removedSize))}`);
243
+ separator('═');
244
+ if (errors.length > 0) {
245
+ console.log();
246
+ console.log(chalk.bold.red('Errors:'));
247
+ errors.forEach((err) => error(err));
248
+ }
249
+ if (isDryRun) {
250
+ console.log();
251
+ info('This was a dry run. No files were deleted.');
252
+ }
253
+ }
254
+ /**
255
+ * Create installer command
256
+ */
257
+ export function createInstallerCommand() {
258
+ const cmd = new Command('installer')
259
+ .description('Find and remove installer files (dmg, pkg, zip)')
260
+ .option('-n, --dry-run', 'Preview only, no deletions')
261
+ .option('-y, --yes', 'Skip confirmation prompts')
262
+ .action(async (options) => {
263
+ await installerCommand(options);
264
+ });
265
+ return enhanceCommandHelp(cmd);
266
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Optimize command - System maintenance and optimization
3
+ */
4
+ import chalk from 'chalk';
5
+ import { Command } from 'commander';
6
+ import { exec } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import { enhanceCommandHelp } from '../utils/help.js';
9
+ import { printHeader, warning, info, separator, createSpinner, succeedSpinner, failSpinner, } from '../ui/output.js';
10
+ import { confirmAction, selectItems } from '../ui/prompts.js';
11
+ import { debug, debugSection, debugObj } from '../utils/debug.js';
12
+ const execAsync = promisify(exec);
13
+ /**
14
+ * Available optimization tasks
15
+ */
16
+ const tasks = [
17
+ {
18
+ id: 'flush-dns',
19
+ name: 'Flush DNS Cache',
20
+ description: 'Clear DNS resolver cache',
21
+ requiresSudo: true,
22
+ action: async () => {
23
+ await execAsync('sudo dscacheutil -flushcache');
24
+ await execAsync('sudo killall -HUP mDNSResponder');
25
+ return 'DNS cache flushed';
26
+ },
27
+ },
28
+ {
29
+ id: 'rebuild-spotlight',
30
+ name: 'Rebuild Spotlight Index',
31
+ description: 'Rebuild the Spotlight search index (may take a while)',
32
+ requiresSudo: true,
33
+ action: async () => {
34
+ await execAsync('sudo mdutil -E /');
35
+ return 'Spotlight reindexing started';
36
+ },
37
+ },
38
+ {
39
+ id: 'rebuild-launch-services',
40
+ name: 'Rebuild Launch Services',
41
+ description: 'Rebuild the Launch Services database',
42
+ requiresSudo: false,
43
+ action: async () => {
44
+ await execAsync('/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user');
45
+ return 'Launch Services database rebuilt';
46
+ },
47
+ },
48
+ {
49
+ id: 'purge-memory',
50
+ name: 'Purge Memory',
51
+ description: 'Release inactive memory',
52
+ requiresSudo: true,
53
+ action: async () => {
54
+ await execAsync('sudo purge');
55
+ return 'Memory purged';
56
+ },
57
+ },
58
+ {
59
+ id: 'verify-disk',
60
+ name: 'Verify Disk',
61
+ description: 'Verify the boot disk',
62
+ requiresSudo: false,
63
+ action: async () => {
64
+ const { stdout } = await execAsync('diskutil verifyVolume /');
65
+ return stdout.trim() || 'Disk verification completed';
66
+ },
67
+ },
68
+ {
69
+ id: 'clear-font-cache',
70
+ name: 'Clear Font Cache',
71
+ description: 'Clear system font caches',
72
+ requiresSudo: true,
73
+ action: async () => {
74
+ await execAsync('sudo atsutil databases -remove');
75
+ return 'Font caches cleared (restart may be required)';
76
+ },
77
+ },
78
+ {
79
+ id: 'rebuild-mail-index',
80
+ name: 'Rebuild Mail Index',
81
+ description: 'Rebuild Mail.app envelope index',
82
+ requiresSudo: false,
83
+ action: async () => {
84
+ const mailEnvelope = '~/Library/Mail/V*/MailData/Envelope Index';
85
+ await execAsync(`rm -rf ${mailEnvelope} 2>/dev/null || true`);
86
+ return 'Mail index will be rebuilt on next launch';
87
+ },
88
+ },
89
+ {
90
+ id: 'clear-quicklook',
91
+ name: 'Clear QuickLook Cache',
92
+ description: 'Reset QuickLook server',
93
+ requiresSudo: false,
94
+ action: async () => {
95
+ await execAsync('qlmanage -r cache');
96
+ await execAsync('qlmanage -r');
97
+ return 'QuickLook cache cleared';
98
+ },
99
+ },
100
+ {
101
+ id: 'reset-bluetooth',
102
+ name: 'Reset Bluetooth Module',
103
+ description: 'Reset the Bluetooth controller',
104
+ requiresSudo: true,
105
+ action: async () => {
106
+ await execAsync('sudo pkill -9 bluetoothd || true');
107
+ return 'Bluetooth module reset';
108
+ },
109
+ },
110
+ {
111
+ id: 'flush-network',
112
+ name: 'Flush Network Settings',
113
+ description: 'Reset network interfaces',
114
+ requiresSudo: true,
115
+ action: async () => {
116
+ await execAsync('sudo ifconfig en0 down && sudo ifconfig en0 up');
117
+ return 'Network interface reset';
118
+ },
119
+ },
120
+ {
121
+ id: 'clear-asl-logs',
122
+ name: 'Clear ASL Logs',
123
+ description: 'Clear Apple System Logs',
124
+ requiresSudo: true,
125
+ action: async () => {
126
+ await execAsync('sudo rm -rf /private/var/log/asl/*.asl');
127
+ return 'ASL logs cleared';
128
+ },
129
+ },
130
+ {
131
+ id: 'enable-trim',
132
+ name: 'Enable TRIM (SSD)',
133
+ description: 'Enable TRIM for non-Apple SSDs',
134
+ requiresSudo: true,
135
+ action: async () => {
136
+ await execAsync('sudo trimforce enable');
137
+ return 'TRIM enabled';
138
+ },
139
+ },
140
+ ];
141
+ /**
142
+ * Execute optimize command
143
+ */
144
+ export async function optimizeCommand(options) {
145
+ const isDryRun = options.dryRun || false;
146
+ debugSection('Optimize Command');
147
+ debugObj('Options', options);
148
+ debug(`Available tasks: ${tasks.length}`);
149
+ printHeader(isDryRun ? '⚡ System Optimization (Dry Run)' : '⚡ System Optimization');
150
+ // Show available tasks
151
+ console.log(chalk.bold('Available optimization tasks:'));
152
+ console.log();
153
+ tasks.forEach((task, index) => {
154
+ const sudo = task.requiresSudo ? chalk.yellow('[sudo]') : '';
155
+ console.log(` ${chalk.cyan(`${index + 1}.`)} ${chalk.bold(task.name)} ${sudo}`);
156
+ console.log(` ${chalk.dim(task.description)}`);
157
+ });
158
+ console.log();
159
+ // Select tasks
160
+ let selectedTasks;
161
+ if (options.all) {
162
+ selectedTasks = tasks;
163
+ info(`Running all ${tasks.length} optimization tasks`);
164
+ }
165
+ else {
166
+ const taskChoices = tasks.map((task) => ({
167
+ name: `${task.name}${task.requiresSudo ? ' [sudo]' : ''}`,
168
+ value: task.id,
169
+ description: task.description,
170
+ }));
171
+ const selectedIds = await selectItems('Select optimization tasks to run:', taskChoices);
172
+ if (selectedIds.length === 0) {
173
+ warning('No tasks selected');
174
+ return;
175
+ }
176
+ selectedTasks = tasks.filter((t) => selectedIds.includes(t.id));
177
+ }
178
+ // Check for sudo tasks
179
+ const sudoTasks = selectedTasks.filter((t) => t.requiresSudo);
180
+ if (sudoTasks.length > 0 && !isDryRun) {
181
+ console.log();
182
+ warning(`${sudoTasks.length} task(s) require administrator privileges`);
183
+ if (!options.yes) {
184
+ const confirmed = await confirmAction('Continue with sudo tasks?', true);
185
+ if (!confirmed) {
186
+ // Filter out sudo tasks
187
+ selectedTasks = selectedTasks.filter((t) => !t.requiresSudo);
188
+ if (selectedTasks.length === 0) {
189
+ warning('No tasks remaining');
190
+ return;
191
+ }
192
+ }
193
+ }
194
+ }
195
+ // Confirm execution
196
+ if (!options.yes && !isDryRun) {
197
+ console.log();
198
+ const confirmed = await confirmAction(`Run ${selectedTasks.length} optimization task(s)?`, true);
199
+ if (!confirmed) {
200
+ warning('Optimization cancelled');
201
+ return;
202
+ }
203
+ }
204
+ // Execute tasks
205
+ console.log();
206
+ separator();
207
+ let successCount = 0;
208
+ let failCount = 0;
209
+ const results = [];
210
+ for (const task of selectedTasks) {
211
+ debug(`Executing task: ${task.id} - ${task.name}`);
212
+ debug(`Requires sudo: ${task.requiresSudo}`);
213
+ const spinner = createSpinner(`${task.name}...`);
214
+ if (isDryRun) {
215
+ debug(`Task ${task.id} skipped (dry run)`);
216
+ succeedSpinner(spinner, `${task.name} ${chalk.dim('(dry run)')}`);
217
+ results.push({ task, success: true, message: 'Dry run - skipped' });
218
+ successCount++;
219
+ continue;
220
+ }
221
+ try {
222
+ const message = await task.action();
223
+ debug(`Task ${task.id} completed: ${message}`);
224
+ succeedSpinner(spinner, `${task.name}: ${chalk.green(message)}`);
225
+ results.push({ task, success: true, message });
226
+ successCount++;
227
+ }
228
+ catch (err) {
229
+ const errorMsg = err.message || 'Unknown error';
230
+ debug(`Task ${task.id} failed: ${errorMsg}`);
231
+ failSpinner(spinner, `${task.name}: ${chalk.red(errorMsg)}`);
232
+ results.push({ task, success: false, message: errorMsg });
233
+ failCount++;
234
+ }
235
+ }
236
+ // Print summary
237
+ console.log();
238
+ separator();
239
+ console.log();
240
+ console.log(chalk.bold('Optimization Summary'));
241
+ console.log();
242
+ console.log(` ${chalk.green('✓')} Successful: ${chalk.green(successCount)}`);
243
+ if (failCount > 0) {
244
+ console.log(` ${chalk.red('✗')} Failed: ${chalk.red(failCount)}`);
245
+ }
246
+ if (isDryRun) {
247
+ console.log();
248
+ info('This was a dry run. No changes were made.');
249
+ }
250
+ // Show recommendations
251
+ console.log();
252
+ console.log(chalk.bold('💡 Recommendations:'));
253
+ console.log(chalk.dim(' - Run optimization monthly for best performance'));
254
+ console.log(chalk.dim(' - Some changes may require a restart to take effect'));
255
+ console.log(chalk.dim(' - Use "broom clean" to free up disk space'));
256
+ }
257
+ /**
258
+ * Create optimize command
259
+ */
260
+ export function createOptimizeCommand() {
261
+ const cmd = new Command('optimize')
262
+ .description('System maintenance and optimization')
263
+ .option('-n, --dry-run', 'Preview only, no changes')
264
+ .option('-y, --yes', 'Skip confirmation prompts')
265
+ .option('-a, --all', 'Run all optimization tasks')
266
+ .action(async (options) => {
267
+ await optimizeCommand(options);
268
+ });
269
+ return enhanceCommandHelp(cmd);
270
+ }