@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,271 @@
1
+ /**
2
+ * Purge command - Clean project-specific artifacts
3
+ */
4
+ import chalk from 'chalk';
5
+ import { Command } from 'commander';
6
+ import { stat, rm } from 'fs/promises';
7
+ import { basename } from 'path';
8
+ import fg from 'fast-glob';
9
+ import { enhanceCommandHelp } from '../utils/help.js';
10
+ import { getSize, formatSize, expandPath } from '../utils/fs.js';
11
+ import { printHeader, success, warning, error, info, separator, createSpinner, succeedSpinner, printProgressBar, } from '../ui/output.js';
12
+ import { confirmAction, selectItems } from '../ui/prompts.js';
13
+ import { debug, debugSection, debugObj } from '../utils/debug.js';
14
+ /**
15
+ * Project artifact targets
16
+ */
17
+ const purgeTargets = [
18
+ {
19
+ id: 'node_modules',
20
+ name: 'Node.js (node_modules)',
21
+ patterns: ['**/node_modules'],
22
+ description: 'npm/yarn/pnpm packages',
23
+ },
24
+ {
25
+ id: 'build',
26
+ name: 'Build outputs (dist, build)',
27
+ patterns: ['**/dist', '**/build', '**/out', '**/.next', '**/.nuxt'],
28
+ description: 'Compiled/bundled files',
29
+ },
30
+ {
31
+ id: 'cache',
32
+ name: 'Project caches',
33
+ patterns: ['**/.cache', '**/.parcel-cache', '**/.turbo', '**/.eslintcache'],
34
+ description: 'Build and tool caches',
35
+ },
36
+ {
37
+ id: 'coverage',
38
+ name: 'Test coverage',
39
+ patterns: ['**/coverage', '**/.nyc_output'],
40
+ description: 'Code coverage reports',
41
+ },
42
+ {
43
+ id: 'python',
44
+ name: 'Python artifacts',
45
+ patterns: [
46
+ '**/__pycache__',
47
+ '**/*.pyc',
48
+ '**/.pytest_cache',
49
+ '**/.mypy_cache',
50
+ '**/venv',
51
+ '**/.venv',
52
+ ],
53
+ description: 'Python caches and venvs',
54
+ },
55
+ {
56
+ id: 'rust',
57
+ name: 'Rust (target)',
58
+ patterns: ['**/target'],
59
+ description: 'Cargo build artifacts',
60
+ },
61
+ {
62
+ id: 'java',
63
+ name: 'Java/Gradle/Maven',
64
+ patterns: ['**/.gradle', '**/build', '**/target'],
65
+ description: 'Java build artifacts',
66
+ },
67
+ {
68
+ id: 'ios',
69
+ name: 'iOS/Xcode',
70
+ patterns: ['**/DerivedData', '**/Pods', '**/.build'],
71
+ description: 'Xcode build data and CocoaPods',
72
+ },
73
+ {
74
+ id: 'logs',
75
+ name: 'Log files',
76
+ patterns: ['**/*.log', '**/logs', '**/.logs'],
77
+ description: 'Application logs',
78
+ },
79
+ {
80
+ id: 'temp',
81
+ name: 'Temporary files',
82
+ patterns: ['**/.tmp', '**/tmp', '**/*.tmp', '**/*.temp'],
83
+ description: 'Temporary files',
84
+ },
85
+ ];
86
+ /**
87
+ * Find matching items for a target
88
+ */
89
+ async function findTargetItems(basePath, target, recursive) {
90
+ const items = [];
91
+ for (const pattern of target.patterns) {
92
+ const searchPattern = recursive ? pattern : pattern.replace('**/', '');
93
+ try {
94
+ const matches = await fg(searchPattern, {
95
+ cwd: basePath,
96
+ absolute: true,
97
+ onlyDirectories: !pattern.includes('*.*'),
98
+ dot: true,
99
+ ignore: ['**/node_modules/**/node_modules'], // Avoid nested node_modules
100
+ });
101
+ for (const match of matches) {
102
+ try {
103
+ const stats = await stat(match);
104
+ const size = await getSize(match);
105
+ items.push({
106
+ path: match,
107
+ name: basename(match),
108
+ size,
109
+ isDirectory: stats.isDirectory(),
110
+ modifiedAt: stats.mtime,
111
+ });
112
+ }
113
+ catch {
114
+ // Skip if cannot access
115
+ }
116
+ }
117
+ }
118
+ catch {
119
+ // Pattern match error
120
+ }
121
+ }
122
+ // Deduplicate by path
123
+ const seen = new Set();
124
+ return items.filter((item) => {
125
+ if (seen.has(item.path)) {
126
+ return false;
127
+ }
128
+ seen.add(item.path);
129
+ return true;
130
+ });
131
+ }
132
+ /**
133
+ * Execute purge command
134
+ */
135
+ export async function purgeCommand(options) {
136
+ const isDryRun = options.dryRun || false;
137
+ const basePath = options.path ? expandPath(options.path) : process.cwd();
138
+ const recursive = options.recursive ?? true;
139
+ debugSection('Purge Command');
140
+ debugObj('Options', options);
141
+ debug(`Base path: ${basePath}`);
142
+ debug(`Recursive: ${recursive}`);
143
+ printHeader(isDryRun ? '🧹 Purge Project Artifacts (Dry Run)' : '🧹 Purge Project Artifacts');
144
+ console.log(`Scanning: ${chalk.cyan(basePath)}`);
145
+ console.log(`Mode: ${recursive ? 'Recursive' : 'Current directory only'}`);
146
+ console.log();
147
+ // Select targets
148
+ const targetChoices = purgeTargets.map((t) => ({
149
+ name: t.name,
150
+ value: t.id,
151
+ description: t.description,
152
+ }));
153
+ const selectedIds = await selectItems('Select artifact types to purge:', targetChoices);
154
+ if (selectedIds.length === 0) {
155
+ warning('No targets selected');
156
+ return;
157
+ }
158
+ const selectedTargets = purgeTargets.filter((t) => selectedIds.includes(t.id));
159
+ // Scan for items
160
+ console.log();
161
+ const spinner = createSpinner('Scanning for artifacts...');
162
+ const allItems = new Map();
163
+ let totalSize = 0;
164
+ let totalCount = 0;
165
+ for (const target of selectedTargets) {
166
+ const items = await findTargetItems(basePath, target, recursive);
167
+ if (items.length > 0) {
168
+ allItems.set(target.id, items);
169
+ totalCount += items.length;
170
+ totalSize += items.reduce((sum, i) => sum + i.size, 0);
171
+ }
172
+ }
173
+ succeedSpinner(spinner, `Found ${totalCount} items (${formatSize(totalSize)})`);
174
+ if (totalCount === 0) {
175
+ console.log();
176
+ success('No artifacts found to purge!');
177
+ return;
178
+ }
179
+ // Show found items
180
+ console.log();
181
+ console.log(chalk.bold('Found artifacts:'));
182
+ console.log();
183
+ for (const [targetId, items] of allItems) {
184
+ const target = purgeTargets.find((t) => t.id === targetId);
185
+ const targetSize = items.reduce((sum, i) => sum + i.size, 0);
186
+ console.log(chalk.bold(` ${target.name}`));
187
+ console.log(` Items: ${items.length} Size: ${chalk.yellow(formatSize(targetSize))}`);
188
+ // Show first few items
189
+ for (const item of items.slice(0, 3)) {
190
+ const relativePath = item.path.replace(basePath, '.');
191
+ console.log(` ${chalk.dim(relativePath)} ${chalk.dim(`(${formatSize(item.size)})`)}`);
192
+ }
193
+ if (items.length > 3) {
194
+ console.log(chalk.dim(` ... and ${items.length - 3} more`));
195
+ }
196
+ console.log();
197
+ }
198
+ // Confirm
199
+ if (!options.yes) {
200
+ console.log();
201
+ const confirmed = await confirmAction(`${isDryRun ? 'Simulate' : 'Purge'} ${totalCount} items (${formatSize(totalSize)})?`, false);
202
+ if (!confirmed) {
203
+ warning('Purge cancelled');
204
+ return;
205
+ }
206
+ }
207
+ // Execute purge
208
+ console.log();
209
+ separator();
210
+ let removedCount = 0;
211
+ let removedSize = 0;
212
+ const errors = [];
213
+ const allItemsFlat = Array.from(allItems.values()).flat();
214
+ for (let i = 0; i < allItemsFlat.length; i++) {
215
+ const item = allItemsFlat[i];
216
+ const progress = ((i + 1) / allItemsFlat.length) * 100;
217
+ printProgressBar(progress, 30, `Purging: ${basename(item.path)}`);
218
+ if (!isDryRun) {
219
+ try {
220
+ await rm(item.path, { recursive: true, force: true });
221
+ removedCount++;
222
+ removedSize += item.size;
223
+ }
224
+ catch (err) {
225
+ errors.push(`${item.path}: ${err.message}`);
226
+ }
227
+ }
228
+ else {
229
+ removedCount++;
230
+ removedSize += item.size;
231
+ }
232
+ }
233
+ // Clear progress line
234
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
235
+ // Summary
236
+ console.log();
237
+ separator();
238
+ console.log();
239
+ console.log(chalk.bold(isDryRun ? '🧪 Dry Run Complete' : '✨ Purge Complete'));
240
+ console.log();
241
+ console.log(` Items ${isDryRun ? 'would be ' : ''}removed: ${chalk.green(removedCount)}`);
242
+ console.log(` Space ${isDryRun ? 'would be ' : ''}freed: ${chalk.green(formatSize(removedSize))}`);
243
+ if (errors.length > 0) {
244
+ console.log(` ${chalk.red(`Errors: ${errors.length}`)}`);
245
+ console.log();
246
+ errors.slice(0, 5).forEach((err) => error(err));
247
+ if (errors.length > 5) {
248
+ console.log(chalk.dim(` ... and ${errors.length - 5} more errors`));
249
+ }
250
+ }
251
+ if (isDryRun) {
252
+ console.log();
253
+ info('This was a dry run. No files were deleted.');
254
+ info('Run without --dry-run to actually purge artifacts.');
255
+ }
256
+ }
257
+ /**
258
+ * Create purge command
259
+ */
260
+ export function createPurgeCommand() {
261
+ const cmd = new Command('purge')
262
+ .description('Clean project-specific build artifacts')
263
+ .option('-n, --dry-run', 'Preview only, no deletions')
264
+ .option('-y, --yes', 'Skip confirmation prompts')
265
+ .option('-p, --path <path>', 'Path to scan (default: current directory)')
266
+ .option('--no-recursive', 'Only scan current directory')
267
+ .action(async (options) => {
268
+ await purgeCommand(options);
269
+ });
270
+ return enhanceCommandHelp(cmd);
271
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * remove command - Uninstall broom from the system
3
+ */
4
+ import { Command } from 'commander';
5
+ import { execSync } from 'child_process';
6
+ import { existsSync, rmSync } from 'fs';
7
+ import { homedir } from 'os';
8
+ import { join } from 'path';
9
+ import chalk from 'chalk';
10
+ import ora from 'ora';
11
+ import { confirm } from '@inquirer/prompts';
12
+ import { enhanceCommandHelp } from '../utils/help.js';
13
+ const CONFIG_DIR = join(homedir(), '.config', 'broom');
14
+ const CACHE_DIR = join(homedir(), '.cache', 'broom');
15
+ const DATA_DIR = join(homedir(), '.local', 'share', 'broom');
16
+ /**
17
+ * Detect how broom was installed
18
+ */
19
+ function detectInstallMethod() {
20
+ try {
21
+ // Check if installed globally via npm/yarn/pnpm
22
+ const npmList = execSync('npm list -g --depth=0 2>/dev/null', { encoding: 'utf-8' });
23
+ if (npmList.includes('broom')) {
24
+ return 'npm';
25
+ }
26
+ }
27
+ catch { }
28
+ try {
29
+ const yarnList = execSync('yarn global list 2>/dev/null', { encoding: 'utf-8' });
30
+ if (yarnList.includes('broom')) {
31
+ return 'yarn';
32
+ }
33
+ }
34
+ catch { }
35
+ try {
36
+ const pnpmList = execSync('pnpm list -g 2>/dev/null', { encoding: 'utf-8' });
37
+ if (pnpmList.includes('broom')) {
38
+ return 'pnpm';
39
+ }
40
+ }
41
+ catch { }
42
+ try {
43
+ const bunList = execSync('bun pm ls -g 2>/dev/null', { encoding: 'utf-8' });
44
+ if (bunList.includes('broom')) {
45
+ return 'bun';
46
+ }
47
+ }
48
+ catch { }
49
+ // Check if running from local development
50
+ const scriptPath = process.argv[1];
51
+ if (scriptPath && scriptPath.includes('/dist/index.js')) {
52
+ return 'local';
53
+ }
54
+ return 'unknown';
55
+ }
56
+ /**
57
+ * Get files/directories that would be removed
58
+ */
59
+ function getRemovableItems(keepConfig) {
60
+ const items = [
61
+ { path: CACHE_DIR, exists: existsSync(CACHE_DIR), description: 'Cache directory' },
62
+ { path: DATA_DIR, exists: existsSync(DATA_DIR), description: 'Data directory' },
63
+ ];
64
+ if (!keepConfig) {
65
+ items.push({
66
+ path: CONFIG_DIR,
67
+ exists: existsSync(CONFIG_DIR),
68
+ description: 'Config directory',
69
+ });
70
+ }
71
+ return items;
72
+ }
73
+ /**
74
+ * Remove user data directories
75
+ */
76
+ function removeUserData(keepConfig) {
77
+ let removed = 0;
78
+ if (existsSync(CACHE_DIR)) {
79
+ rmSync(CACHE_DIR, { recursive: true, force: true });
80
+ removed++;
81
+ }
82
+ if (existsSync(DATA_DIR)) {
83
+ rmSync(DATA_DIR, { recursive: true, force: true });
84
+ removed++;
85
+ }
86
+ if (!keepConfig && existsSync(CONFIG_DIR)) {
87
+ rmSync(CONFIG_DIR, { recursive: true, force: true });
88
+ removed++;
89
+ }
90
+ return removed;
91
+ }
92
+ /**
93
+ * Uninstall broom
94
+ */
95
+ async function uninstallBroom(opts) {
96
+ console.log(chalk.bold('\n🗑️ Uninstall broom\n'));
97
+ const installMethod = detectInstallMethod();
98
+ const items = getRemovableItems(opts.keepConfig);
99
+ const existingItems = items.filter((i) => i.exists);
100
+ console.log(chalk.dim('Installation method: ') + installMethod);
101
+ console.log();
102
+ if (installMethod === 'local') {
103
+ console.log(chalk.yellow('broom appears to be running from a local development directory.'));
104
+ console.log(chalk.dim('To remove, simply delete the project directory.\n'));
105
+ }
106
+ if (existingItems.length > 0) {
107
+ console.log(chalk.bold('User data to be removed:'));
108
+ for (const item of existingItems) {
109
+ console.log(` ${chalk.red('•')} ${item.description}: ${chalk.dim(item.path)}`);
110
+ }
111
+ console.log();
112
+ }
113
+ if (opts.keepConfig) {
114
+ console.log(chalk.dim('Configuration will be preserved.\n'));
115
+ }
116
+ if (!opts.yes) {
117
+ const confirmed = await confirm({
118
+ message: 'Are you sure you want to uninstall broom?',
119
+ default: false,
120
+ });
121
+ if (!confirmed) {
122
+ console.log(chalk.yellow('Uninstall cancelled'));
123
+ return;
124
+ }
125
+ }
126
+ // Remove user data first
127
+ if (existingItems.length > 0) {
128
+ const dataSpinner = ora('Removing user data...').start();
129
+ const removed = removeUserData(opts.keepConfig);
130
+ dataSpinner.succeed(`Removed ${removed} data ${removed === 1 ? 'directory' : 'directories'}`);
131
+ }
132
+ // Uninstall package
133
+ if (installMethod !== 'local' && installMethod !== 'unknown') {
134
+ const uninstallSpinner = ora(`Uninstalling broom via ${installMethod}...`).start();
135
+ try {
136
+ let cmd;
137
+ switch (installMethod) {
138
+ case 'npm':
139
+ cmd = 'npm uninstall -g broom';
140
+ break;
141
+ case 'yarn':
142
+ cmd = 'yarn global remove broom';
143
+ break;
144
+ case 'pnpm':
145
+ cmd = 'pnpm remove -g broom';
146
+ break;
147
+ case 'bun':
148
+ cmd = 'bun remove -g broom';
149
+ break;
150
+ default:
151
+ cmd = 'npm uninstall -g broom';
152
+ }
153
+ execSync(cmd, { stdio: 'pipe' });
154
+ uninstallSpinner.succeed('broom uninstalled successfully');
155
+ }
156
+ catch (error) {
157
+ uninstallSpinner.fail('Failed to uninstall broom package');
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 uninstall -g broom'));
163
+ }
164
+ }
165
+ console.log(chalk.green('\n👋 broom has been removed. Thanks for using broom!\n'));
166
+ // Remove shell completion
167
+ console.log(chalk.dim('Remember to remove any shell completion scripts you may have added:'));
168
+ console.log(chalk.dim(' - ~/.bashrc or ~/.bash_profile'));
169
+ console.log(chalk.dim(' - ~/.zshrc or ~/.zsh/completions/_broom'));
170
+ console.log(chalk.dim(' - ~/.config/fish/completions/broom.fish\n'));
171
+ }
172
+ /**
173
+ * Create remove command
174
+ */
175
+ export function createRemoveCommand() {
176
+ const cmd = new Command('remove')
177
+ .description('Uninstall broom from the system')
178
+ .option('-y, --yes', 'Skip confirmation prompts')
179
+ .option('-k, --keep-config', 'Keep configuration files')
180
+ .action(async (opts) => {
181
+ await uninstallBroom(opts);
182
+ });
183
+ return enhanceCommandHelp(cmd);
184
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Reports command - Manage cleanup reports
3
+ */
4
+ import chalk from 'chalk';
5
+ import { Command } from 'commander';
6
+ import { enhanceCommandHelp } from '../utils/help.js';
7
+ import { readdir, stat, unlink } from 'fs/promises';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+ import { exists, formatSize } from '../utils/fs.js';
11
+ import { printHeader, success, warning, info, separator, createSpinner, succeedSpinner, } from '../ui/output.js';
12
+ import { confirm } from '../ui/prompts.js';
13
+ /**
14
+ * Get all report files
15
+ */
16
+ async function getReportFiles() {
17
+ const reportsDir = join(homedir(), '.broom', 'reports');
18
+ if (!exists(reportsDir)) {
19
+ return [];
20
+ }
21
+ const files = [];
22
+ try {
23
+ const entries = await readdir(reportsDir);
24
+ for (const entry of entries) {
25
+ if (!entry.endsWith('.html'))
26
+ continue;
27
+ const filePath = join(reportsDir, entry);
28
+ const stats = await stat(filePath);
29
+ files.push({
30
+ path: filePath,
31
+ name: entry,
32
+ size: stats.size,
33
+ date: stats.mtime,
34
+ });
35
+ }
36
+ // Sort by date descending (newest first)
37
+ files.sort((a, b) => b.date.getTime() - a.date.getTime());
38
+ return files;
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ }
44
+ /**
45
+ * List all reports
46
+ */
47
+ async function listReports() {
48
+ printHeader('📊 Cleanup Reports');
49
+ const files = await getReportFiles();
50
+ if (files.length === 0) {
51
+ info('No reports found');
52
+ console.log();
53
+ console.log(chalk.dim('Reports are created with: broom clean --report'));
54
+ return;
55
+ }
56
+ console.log(chalk.bold(`Found ${files.length} report(s):`));
57
+ console.log();
58
+ const totalSize = files.reduce((sum, file) => sum + file.size, 0);
59
+ for (let i = 0; i < files.length; i++) {
60
+ const file = files[i];
61
+ const dateStr = file.date.toLocaleString('ja-JP');
62
+ const sizeStr = formatSize(file.size);
63
+ console.log(` ${i + 1}. ${chalk.cyan(file.name)}`);
64
+ console.log(` ${chalk.dim(`Date: ${dateStr} | Size: ${sizeStr}`)}`);
65
+ }
66
+ console.log();
67
+ separator();
68
+ console.log();
69
+ console.log(`Total size: ${chalk.yellow(formatSize(totalSize))}`);
70
+ console.log();
71
+ console.log(chalk.dim('To delete reports: broom reports clean'));
72
+ }
73
+ /**
74
+ * Clean all reports
75
+ */
76
+ async function cleanReports(options) {
77
+ printHeader('🗑️ Clean Reports');
78
+ const files = await getReportFiles();
79
+ if (files.length === 0) {
80
+ info('No reports to delete');
81
+ return;
82
+ }
83
+ const totalSize = files.reduce((sum, file) => sum + file.size, 0);
84
+ console.log(chalk.bold(`Found ${files.length} report(s) to delete`));
85
+ console.log(`Total size: ${chalk.yellow(formatSize(totalSize))}`);
86
+ console.log();
87
+ // Confirm deletion
88
+ if (!options.yes) {
89
+ const shouldDelete = await confirm({
90
+ message: `Delete all ${files.length} report file(s)?`,
91
+ default: false,
92
+ });
93
+ if (!shouldDelete) {
94
+ info('Cancelled');
95
+ return;
96
+ }
97
+ }
98
+ // Delete files
99
+ const spinner = createSpinner('Deleting reports...');
100
+ let deleted = 0;
101
+ let failed = 0;
102
+ for (const file of files) {
103
+ try {
104
+ await unlink(file.path);
105
+ deleted++;
106
+ }
107
+ catch {
108
+ failed++;
109
+ }
110
+ }
111
+ succeedSpinner(spinner, 'Deletion complete');
112
+ console.log();
113
+ success(`Deleted ${deleted} report(s)`);
114
+ console.log(`Freed space: ${chalk.green(formatSize(totalSize))}`);
115
+ if (failed > 0) {
116
+ warning(`Failed to delete ${failed} file(s)`);
117
+ }
118
+ }
119
+ /**
120
+ * Open latest report
121
+ */
122
+ async function openLatestReport() {
123
+ const files = await getReportFiles();
124
+ if (files.length === 0) {
125
+ warning('No reports found');
126
+ return;
127
+ }
128
+ const latest = files[0];
129
+ console.log(chalk.dim(`Opening: ${latest.name}`));
130
+ const { exec } = await import('child_process');
131
+ const { promisify } = await import('util');
132
+ const execAsync = promisify(exec);
133
+ try {
134
+ await execAsync(`open "${latest.path}"`);
135
+ success('Report opened in browser');
136
+ }
137
+ catch (error) {
138
+ warning('Failed to open report');
139
+ console.log(chalk.dim(`Path: ${latest.path}`));
140
+ }
141
+ }
142
+ /**
143
+ * Execute reports command
144
+ */
145
+ async function reportsCommand(subcommand, options) {
146
+ switch (subcommand) {
147
+ case 'list':
148
+ await listReports();
149
+ break;
150
+ case 'clean':
151
+ await cleanReports(options);
152
+ break;
153
+ case 'open':
154
+ await openLatestReport();
155
+ break;
156
+ default:
157
+ await listReports();
158
+ break;
159
+ }
160
+ }
161
+ /**
162
+ * Create reports command
163
+ */
164
+ export function createReportsCommand() {
165
+ const cmd = new Command('reports')
166
+ .description('Manage cleanup reports')
167
+ .argument('[subcommand]', 'Subcommand: list, clean, open', 'list')
168
+ .option('-y, --yes', 'Skip confirmation prompts')
169
+ .action(async (subcommand, options) => {
170
+ await reportsCommand(subcommand, options);
171
+ });
172
+ return enhanceCommandHelp(cmd);
173
+ }