@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,336 @@
1
+ /**
2
+ * Uninstall command - Remove applications and their leftovers
3
+ */
4
+ import chalk from 'chalk';
5
+ import { Command } from 'commander';
6
+ import { readdir, stat, rm } from 'fs/promises';
7
+ import { join } from 'path';
8
+ import { exec } from 'child_process';
9
+ import { promisify } from 'util';
10
+ import { enhanceCommandHelp } from '../utils/help.js';
11
+ import { paths } from '../utils/paths.js';
12
+ import { exists, getSize, formatSize, isProtectedPath, expandPath } from '../utils/fs.js';
13
+ import { printHeader, warning, error, info, createSpinner, succeedSpinner, failSpinner, printSummaryBlock, } from '../ui/output.js';
14
+ import { selectApp, selectFiles, confirmAction } from '../ui/prompts.js';
15
+ const execAsync = promisify(exec);
16
+ /**
17
+ * Get bundle ID from app's Info.plist
18
+ */
19
+ async function getBundleId(appPath) {
20
+ try {
21
+ const plistPath = join(appPath, 'Contents', 'Info.plist');
22
+ if (!exists(plistPath)) {
23
+ return null;
24
+ }
25
+ // Use plutil to convert plist to JSON
26
+ const { stdout } = await execAsync(`plutil -convert json -o - "${plistPath}"`);
27
+ const plist = JSON.parse(stdout);
28
+ return plist.CFBundleIdentifier || null;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ /**
35
+ * Scan for installed applications
36
+ */
37
+ async function scanApplications() {
38
+ const apps = [];
39
+ const appDirs = [paths.applications, paths.userApplications];
40
+ for (const appDir of appDirs) {
41
+ try {
42
+ if (!exists(appDir)) {
43
+ continue;
44
+ }
45
+ const entries = await readdir(appDir);
46
+ for (const entry of entries) {
47
+ if (!entry.endsWith('.app')) {
48
+ continue;
49
+ }
50
+ const appPath = join(appDir, entry);
51
+ try {
52
+ const stats = await stat(appPath);
53
+ if (!stats.isDirectory()) {
54
+ continue;
55
+ }
56
+ const size = await getSize(appPath);
57
+ const bundleId = await getBundleId(appPath);
58
+ const name = entry.replace('.app', '');
59
+ apps.push({
60
+ name,
61
+ path: appPath,
62
+ bundleId: bundleId || undefined,
63
+ size,
64
+ });
65
+ }
66
+ catch {
67
+ // Skip if cannot access
68
+ }
69
+ }
70
+ }
71
+ catch {
72
+ // Skip if cannot access directory
73
+ }
74
+ }
75
+ // Sort by name
76
+ apps.sort((a, b) => a.name.localeCompare(b.name));
77
+ return apps;
78
+ }
79
+ /**
80
+ * Find app-related files
81
+ */
82
+ async function findAppFiles(app) {
83
+ const items = [];
84
+ const searchTerms = [];
85
+ // Build search terms from app name and bundle ID
86
+ if (app.bundleId) {
87
+ searchTerms.push(app.bundleId);
88
+ // Also search for variations (e.g., com.company.app -> company.app)
89
+ const parts = app.bundleId.split('.');
90
+ if (parts.length >= 2) {
91
+ searchTerms.push(parts.slice(-2).join('.'));
92
+ }
93
+ }
94
+ // Add app name variations
95
+ searchTerms.push(app.name);
96
+ searchTerms.push(app.name.toLowerCase());
97
+ searchTerms.push(app.name.replace(/\s+/g, ''));
98
+ // Directories to search
99
+ const searchDirs = [
100
+ paths.userCache,
101
+ paths.applicationSupport,
102
+ paths.preferences,
103
+ paths.savedState,
104
+ paths.userLogs,
105
+ join(paths.applicationSupport, '..', 'Containers'),
106
+ join(paths.applicationSupport, '..', 'Group Containers'),
107
+ ];
108
+ for (const searchDir of searchDirs) {
109
+ try {
110
+ if (!exists(searchDir)) {
111
+ continue;
112
+ }
113
+ const entries = await readdir(searchDir);
114
+ for (const entry of entries) {
115
+ const entryLower = entry.toLowerCase();
116
+ // Check if entry matches any search term
117
+ const matches = searchTerms.some((term) => entryLower.includes(term.toLowerCase()));
118
+ if (!matches) {
119
+ continue;
120
+ }
121
+ const entryPath = join(searchDir, entry);
122
+ try {
123
+ const stats = await stat(entryPath);
124
+ const size = await getSize(entryPath);
125
+ items.push({
126
+ path: entryPath,
127
+ size,
128
+ name: entry,
129
+ isDirectory: stats.isDirectory(),
130
+ modifiedAt: stats.mtime,
131
+ });
132
+ }
133
+ catch {
134
+ // Skip if cannot access
135
+ }
136
+ }
137
+ }
138
+ catch {
139
+ // Skip if cannot access directory
140
+ }
141
+ }
142
+ // Also check for launch agents/daemons
143
+ const launchDirs = [
144
+ join(expandPath('~'), 'Library', 'LaunchAgents'),
145
+ '/Library/LaunchAgents',
146
+ '/Library/LaunchDaemons',
147
+ ];
148
+ for (const launchDir of launchDirs) {
149
+ try {
150
+ if (!exists(launchDir)) {
151
+ continue;
152
+ }
153
+ const entries = await readdir(launchDir);
154
+ for (const entry of entries) {
155
+ if (!entry.endsWith('.plist')) {
156
+ continue;
157
+ }
158
+ const entryLower = entry.toLowerCase();
159
+ const matches = searchTerms.some((term) => entryLower.includes(term.toLowerCase()));
160
+ if (!matches) {
161
+ continue;
162
+ }
163
+ const entryPath = join(launchDir, entry);
164
+ try {
165
+ const stats = await stat(entryPath);
166
+ items.push({
167
+ path: entryPath,
168
+ size: stats.size,
169
+ name: entry,
170
+ isDirectory: false,
171
+ modifiedAt: stats.mtime,
172
+ });
173
+ }
174
+ catch {
175
+ // Skip if cannot access
176
+ }
177
+ }
178
+ }
179
+ catch {
180
+ // Skip if cannot access directory
181
+ }
182
+ }
183
+ // Sort by size descending
184
+ items.sort((a, b) => b.size - a.size);
185
+ return items;
186
+ }
187
+ /**
188
+ * Remove app using Finder (moves to trash)
189
+ */
190
+ async function removeAppToTrash(appPath) {
191
+ try {
192
+ const script = `tell application "Finder" to delete POSIX file "${appPath}"`;
193
+ await execAsync(`osascript -e '${script}'`);
194
+ return true;
195
+ }
196
+ catch {
197
+ return false;
198
+ }
199
+ }
200
+ /**
201
+ * Execute uninstall command
202
+ */
203
+ export async function uninstallCommand(options) {
204
+ const isDryRun = options.dryRun || false;
205
+ printHeader(isDryRun ? '🗑️ Uninstall Apps (Dry Run)' : '🗑️ Uninstall Apps');
206
+ // Scan for apps
207
+ const spinner = createSpinner('Scanning applications...');
208
+ const apps = await scanApplications();
209
+ succeedSpinner(spinner, `Found ${apps.length} applications`);
210
+ if (apps.length === 0) {
211
+ warning('No applications found');
212
+ return;
213
+ }
214
+ // Select app
215
+ console.log();
216
+ const selectedApp = await selectApp(apps);
217
+ if (!selectedApp) {
218
+ warning('No application selected');
219
+ return;
220
+ }
221
+ // Find related files
222
+ console.log();
223
+ const searchSpinner = createSpinner(`Searching for ${selectedApp.name} related files...`);
224
+ const relatedFiles = await findAppFiles(selectedApp);
225
+ succeedSpinner(searchSpinner, `Found ${relatedFiles.length} related files`);
226
+ // Show app info
227
+ console.log();
228
+ console.log(chalk.bold(`Application: ${selectedApp.name}`));
229
+ console.log(` Path: ${chalk.dim(selectedApp.path)}`);
230
+ console.log(` Size: ${chalk.yellow(formatSize(selectedApp.size))}`);
231
+ if (selectedApp.bundleId) {
232
+ console.log(` Bundle ID: ${chalk.dim(selectedApp.bundleId)}`);
233
+ }
234
+ // Show related files
235
+ if (relatedFiles.length > 0) {
236
+ const totalRelatedSize = relatedFiles.reduce((sum, f) => sum + f.size, 0);
237
+ console.log();
238
+ console.log(chalk.bold(`Related Files (${formatSize(totalRelatedSize)}):`));
239
+ for (const file of relatedFiles.slice(0, 10)) {
240
+ console.log(` ${file.isDirectory ? '📁' : '📄'} ${chalk.dim(file.path)} ${chalk.yellow(formatSize(file.size))}`);
241
+ }
242
+ if (relatedFiles.length > 10) {
243
+ console.log(chalk.dim(` ... and ${relatedFiles.length - 10} more`));
244
+ }
245
+ }
246
+ // Confirm uninstall
247
+ console.log();
248
+ if (!options.yes) {
249
+ const confirmed = await confirmAction(`Uninstall ${selectedApp.name} and remove related files?`, false);
250
+ if (!confirmed) {
251
+ warning('Uninstall cancelled');
252
+ return;
253
+ }
254
+ }
255
+ // Select files to remove
256
+ let filesToRemove = relatedFiles;
257
+ if (!options.yes && relatedFiles.length > 0) {
258
+ console.log();
259
+ info('Select which related files to remove:');
260
+ filesToRemove = await selectFiles(relatedFiles);
261
+ }
262
+ // Execute uninstall
263
+ console.log();
264
+ const uninstallSpinner = createSpinner(isDryRun ? 'Simulating uninstall...' : 'Uninstalling...');
265
+ let appRemoved = false;
266
+ let filesRemoved = 0;
267
+ let freedSpace = 0;
268
+ const errors = [];
269
+ if (!isDryRun) {
270
+ // Remove app
271
+ appRemoved = await removeAppToTrash(selectedApp.path);
272
+ if (appRemoved) {
273
+ freedSpace += selectedApp.size;
274
+ }
275
+ else {
276
+ errors.push(`Failed to remove ${selectedApp.name}`);
277
+ }
278
+ // Remove related files
279
+ for (const file of filesToRemove) {
280
+ if (isProtectedPath(file.path)) {
281
+ errors.push(`Skipped protected path: ${file.path}`);
282
+ continue;
283
+ }
284
+ try {
285
+ await rm(file.path, { recursive: true, force: true });
286
+ filesRemoved++;
287
+ freedSpace += file.size;
288
+ }
289
+ catch (err) {
290
+ errors.push(`Failed to remove ${file.path}: ${err.message}`);
291
+ }
292
+ }
293
+ }
294
+ else {
295
+ // Dry run - just count
296
+ appRemoved = true;
297
+ freedSpace = selectedApp.size;
298
+ filesRemoved = filesToRemove.length;
299
+ freedSpace += filesToRemove.reduce((sum, f) => sum + f.size, 0);
300
+ }
301
+ if (errors.length === 0) {
302
+ succeedSpinner(uninstallSpinner, isDryRun ? 'Simulation complete' : 'Uninstall complete');
303
+ }
304
+ else {
305
+ failSpinner(uninstallSpinner, 'Uninstall completed with errors');
306
+ }
307
+ // Print summary
308
+ const summaryHeading = isDryRun ? 'Dry Run Complete' : 'Uninstall Complete';
309
+ const summaryDetails = [
310
+ `Application: ${appRemoved ? chalk.green('Removed') : chalk.red('Failed')}`,
311
+ `Related files removed: ${filesRemoved}`,
312
+ `Space freed: ${chalk.green(formatSize(freedSpace))}`,
313
+ ];
314
+ if (errors.length > 0) {
315
+ summaryDetails.push(`${chalk.red(`Errors: ${errors.length}`)}`);
316
+ }
317
+ printSummaryBlock(summaryHeading, summaryDetails);
318
+ if (errors.length > 0) {
319
+ console.log();
320
+ console.log(chalk.bold.red('Errors:'));
321
+ errors.forEach((err) => error(err));
322
+ }
323
+ }
324
+ /**
325
+ * Create uninstall command
326
+ */
327
+ export function createUninstallCommand() {
328
+ const cmd = new Command('uninstall')
329
+ .description('Remove apps and their leftovers')
330
+ .option('-n, --dry-run', 'Preview only, no deletions')
331
+ .option('-y, --yes', 'Skip confirmation prompts')
332
+ .action(async (options) => {
333
+ await uninstallCommand(options);
334
+ });
335
+ return enhanceCommandHelp(cmd);
336
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * update command - Self-update broom to the latest version
3
+ */
4
+ import { Command } from 'commander';
5
+ import { execSync } from 'child_process';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import { confirm } from '@inquirer/prompts';
9
+ import { enhanceCommandHelp } from '../utils/help.js';
10
+ const PACKAGE_NAME = 'broom-cli';
11
+ /**
12
+ * Get current installed version
13
+ */
14
+ function getCurrentVersion() {
15
+ try {
16
+ const packageJson = require('../../package.json');
17
+ return packageJson.version;
18
+ }
19
+ catch {
20
+ return 'unknown';
21
+ }
22
+ }
23
+ /**
24
+ * Get latest version from npm
25
+ */
26
+ function getLatestVersion() {
27
+ try {
28
+ // Try npm
29
+ const result = execSync(`npm view ${PACKAGE_NAME} version 2>/dev/null`, {
30
+ encoding: 'utf-8',
31
+ timeout: 10000,
32
+ });
33
+ return result.trim();
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /**
40
+ * Compare semver versions
41
+ */
42
+ function compareVersions(current, latest) {
43
+ const parseParts = (v) => v.split('.').map((n) => parseInt(n, 10) || 0);
44
+ const currentParts = parseParts(current);
45
+ const latestParts = parseParts(latest);
46
+ for (let i = 0; i < 3; i++) {
47
+ const c = currentParts[i] || 0;
48
+ const l = latestParts[i] || 0;
49
+ if (l > c) {
50
+ return 1; // newer
51
+ }
52
+ if (l < c) {
53
+ return -1; // older
54
+ }
55
+ }
56
+ return 0; // same
57
+ }
58
+ /**
59
+ * Check for updates
60
+ */
61
+ async function checkForUpdates() {
62
+ const spinner = ora('Checking for updates...').start();
63
+ const current = getCurrentVersion();
64
+ const latest = getLatestVersion();
65
+ if (!latest) {
66
+ spinner.warn('Could not fetch latest version');
67
+ console.log(chalk.dim('\nMake sure you have internet connection.'));
68
+ return;
69
+ }
70
+ spinner.stop();
71
+ console.log(chalk.bold('\n📦 broom Version Info\n'));
72
+ console.log(` Current version: ${chalk.cyan(current)}`);
73
+ console.log(` Latest version: ${chalk.green(latest)}`);
74
+ const cmp = compareVersions(current, latest);
75
+ if (cmp > 0) {
76
+ console.log(chalk.yellow('\n Update available!'));
77
+ console.log(chalk.dim(` Run 'broom update' to update.\n`));
78
+ }
79
+ else if (cmp < 0) {
80
+ console.log(chalk.cyan('\n You are running a newer version.\n'));
81
+ }
82
+ else {
83
+ console.log(chalk.green('\n You are up to date!\n'));
84
+ }
85
+ }
86
+ /**
87
+ * Detect package manager
88
+ */
89
+ function detectPackageManager() {
90
+ const managers = ['bun', 'pnpm', 'yarn', 'npm'];
91
+ for (const manager of managers) {
92
+ try {
93
+ execSync(`which ${manager}`, { stdio: 'pipe' });
94
+ return manager;
95
+ }
96
+ catch {
97
+ continue;
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+ /**
103
+ * Perform update
104
+ */
105
+ async function performUpdate(skipConfirm) {
106
+ const current = getCurrentVersion();
107
+ const spinner = ora('Checking for updates...').start();
108
+ const latest = getLatestVersion();
109
+ if (!latest) {
110
+ spinner.fail('Could not fetch latest version');
111
+ return;
112
+ }
113
+ const cmp = compareVersions(current, latest);
114
+ if (cmp <= 0) {
115
+ spinner.succeed(`Already up to date (v${current})`);
116
+ return;
117
+ }
118
+ spinner.info(`Update available: ${current} → ${latest}`);
119
+ if (!skipConfirm) {
120
+ const confirmed = await confirm({
121
+ message: `Update broom to v${latest}?`,
122
+ default: true,
123
+ });
124
+ if (!confirmed) {
125
+ console.log(chalk.yellow('Update cancelled'));
126
+ return;
127
+ }
128
+ }
129
+ const pm = detectPackageManager();
130
+ if (!pm) {
131
+ console.error(chalk.red('No package manager found'));
132
+ console.log(chalk.dim('Install npm, yarn, pnpm, or bun first.'));
133
+ return;
134
+ }
135
+ const updateSpinner = ora(`Updating broom using ${pm}...`).start();
136
+ try {
137
+ let cmd;
138
+ switch (pm) {
139
+ case 'npm':
140
+ cmd = `npm update -g ${PACKAGE_NAME}`;
141
+ break;
142
+ case 'yarn':
143
+ cmd = `yarn global upgrade ${PACKAGE_NAME}`;
144
+ break;
145
+ case 'pnpm':
146
+ cmd = `pnpm update -g ${PACKAGE_NAME}`;
147
+ break;
148
+ case 'bun':
149
+ cmd = `bun update -g ${PACKAGE_NAME}`;
150
+ break;
151
+ }
152
+ execSync(cmd, { stdio: 'pipe' });
153
+ updateSpinner.succeed(`Successfully updated to v${latest}`);
154
+ console.log(chalk.dim('\nChangelog: https://github.com/your-username/broom/releases'));
155
+ }
156
+ catch (error) {
157
+ updateSpinner.fail('Update failed');
158
+ if (error instanceof Error) {
159
+ console.error(chalk.red(error.message));
160
+ }
161
+ console.log(chalk.dim('\nTry running manually:'));
162
+ console.log(chalk.dim(` npm install -g ${PACKAGE_NAME}@latest`));
163
+ }
164
+ }
165
+ /**
166
+ * Create update command
167
+ */
168
+ export function createUpdateCommand() {
169
+ const cmd = new Command('update')
170
+ .description('Self-update broom to the latest version')
171
+ .option('-c, --check', 'Check for updates only (no install)')
172
+ .option('-y, --yes', 'Skip confirmation prompts')
173
+ .action(async (opts) => {
174
+ if (opts.check) {
175
+ await checkForUpdates();
176
+ }
177
+ else {
178
+ await performUpdate(opts.yes);
179
+ }
180
+ });
181
+ return enhanceCommandHelp(cmd);
182
+ }