@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,280 @@
1
+ /**
2
+ * Doctor command - System health diagnostics
3
+ */
4
+ import chalk from 'chalk';
5
+ import { Command } from 'commander';
6
+ import { enhanceCommandHelp } from '../utils/help.js';
7
+ import { execSync } from 'child_process';
8
+ import { expandPath, exists, formatSize } from '../utils/fs.js';
9
+ import { printHeader, separator, warning, success, error } from '../ui/output.js';
10
+ /**
11
+ * Check disk usage
12
+ */
13
+ async function checkDiskUsage() {
14
+ try {
15
+ const output = execSync("df -k / | tail -1 | awk '{print $2, $3, $4, $5}'").toString().trim();
16
+ const [total, used, free, percentStr] = output.split(' ');
17
+ const percent = parseInt(percentStr);
18
+ let status = 'healthy';
19
+ let message = `Disk usage: ${percent}%`;
20
+ let recommendation;
21
+ if (percent >= 90) {
22
+ status = 'critical';
23
+ recommendation = 'Critically low disk space! Run "broom clean" immediately';
24
+ }
25
+ else if (percent >= 80) {
26
+ status = 'warning';
27
+ recommendation = 'Disk space running low. Consider cleaning up';
28
+ }
29
+ return {
30
+ name: 'Disk Usage',
31
+ status,
32
+ message,
33
+ recommendation,
34
+ value: `${formatSize(parseInt(used) * 1024)} / ${formatSize(parseInt(total) * 1024)}`,
35
+ };
36
+ }
37
+ catch {
38
+ return {
39
+ name: 'Disk Usage',
40
+ status: 'warning',
41
+ message: 'Unable to check disk usage',
42
+ };
43
+ }
44
+ }
45
+ /**
46
+ * Find large files (>1GB)
47
+ */
48
+ async function checkLargeFiles() {
49
+ try {
50
+ const homePath = expandPath('~');
51
+ const output = execSync(`find "${homePath}" -type f -size +1G 2>/dev/null | head -20`).toString();
52
+ const files = output
53
+ .trim()
54
+ .split('\n')
55
+ .filter((f) => f);
56
+ let status = 'healthy';
57
+ let message = 'No large files found';
58
+ let recommendation;
59
+ if (files.length > 0) {
60
+ status = files.length > 10 ? 'warning' : 'healthy';
61
+ message = `${files.length} files larger than 1GB`;
62
+ if (files.length > 10) {
63
+ recommendation = 'Review and clean up large files';
64
+ }
65
+ }
66
+ return {
67
+ name: 'Large Files',
68
+ status,
69
+ message,
70
+ recommendation,
71
+ value: files.length > 0 ? `${files.length} files` : undefined,
72
+ };
73
+ }
74
+ catch {
75
+ return {
76
+ name: 'Large Files',
77
+ status: 'healthy',
78
+ message: 'No large files detected',
79
+ };
80
+ }
81
+ }
82
+ /**
83
+ * Check for broken symlinks
84
+ */
85
+ async function checkBrokenSymlinks() {
86
+ try {
87
+ const homePath = expandPath('~');
88
+ const output = execSync(`find "${homePath}" -type l ! -exec test -e {} \\; -print 2>/dev/null | head -50`).toString();
89
+ const brokenLinks = output
90
+ .trim()
91
+ .split('\n')
92
+ .filter((f) => f);
93
+ let status = 'healthy';
94
+ let message = 'No broken symlinks';
95
+ let recommendation;
96
+ if (brokenLinks.length > 0) {
97
+ status = brokenLinks.length > 10 ? 'warning' : 'healthy';
98
+ message = `${brokenLinks.length} broken symlinks detected`;
99
+ if (brokenLinks.length > 10) {
100
+ recommendation = 'Clean up broken symlinks';
101
+ }
102
+ }
103
+ return {
104
+ name: 'Broken Symlinks',
105
+ status,
106
+ message,
107
+ recommendation,
108
+ value: brokenLinks.length > 0 ? `${brokenLinks.length} links` : undefined,
109
+ };
110
+ }
111
+ catch {
112
+ return {
113
+ name: 'Broken Symlinks',
114
+ status: 'healthy',
115
+ message: 'No broken symlinks found',
116
+ };
117
+ }
118
+ }
119
+ /**
120
+ * Check for old files (not accessed in 1 year)
121
+ */
122
+ async function checkOldFiles() {
123
+ try {
124
+ const homePath = expandPath('~');
125
+ const output = execSync(`find "${homePath}/Downloads" "${homePath}/Documents" -type f -atime +365 2>/dev/null | head -100`).toString();
126
+ const oldFiles = output
127
+ .trim()
128
+ .split('\n')
129
+ .filter((f) => f);
130
+ let status = 'healthy';
131
+ let message = 'No old files found';
132
+ let recommendation;
133
+ if (oldFiles.length > 0) {
134
+ status = oldFiles.length > 50 ? 'warning' : 'healthy';
135
+ message = `${oldFiles.length}+ files not accessed in 1 year`;
136
+ if (oldFiles.length > 50) {
137
+ recommendation = 'Consider archiving or removing old files';
138
+ }
139
+ }
140
+ return {
141
+ name: 'Old Files',
142
+ status,
143
+ message,
144
+ recommendation,
145
+ value: oldFiles.length > 0 ? `${oldFiles.length}+ files` : undefined,
146
+ };
147
+ }
148
+ catch {
149
+ return {
150
+ name: 'Old Files',
151
+ status: 'healthy',
152
+ message: 'No old files detected',
153
+ };
154
+ }
155
+ }
156
+ /**
157
+ * Check common cache directories
158
+ */
159
+ async function checkCacheSizes() {
160
+ const cachePaths = [
161
+ expandPath('~/Library/Caches'),
162
+ expandPath('~/Library/Logs'),
163
+ expandPath('~/.npm'),
164
+ expandPath('~/.cache'),
165
+ ];
166
+ let totalSize = 0;
167
+ for (const path of cachePaths) {
168
+ if (exists(path)) {
169
+ try {
170
+ const output = execSync(`du -sk "${path}" 2>/dev/null`).toString();
171
+ const size = parseInt(output.split('\t')[0]) * 1024;
172
+ totalSize += size;
173
+ }
174
+ catch {
175
+ // Skip if cannot read
176
+ }
177
+ }
178
+ }
179
+ let status = 'healthy';
180
+ let message = 'Cache size is reasonable';
181
+ let recommendation;
182
+ const GB = 1024 * 1024 * 1024;
183
+ if (totalSize > 10 * GB) {
184
+ status = 'warning';
185
+ message = `Large cache detected: ${formatSize(totalSize)}`;
186
+ recommendation = 'Run "broom clean" to free up cache space';
187
+ }
188
+ else if (totalSize > 5 * GB) {
189
+ status = 'healthy';
190
+ message = `Cache size: ${formatSize(totalSize)}`;
191
+ }
192
+ return {
193
+ name: 'Cache Size',
194
+ status,
195
+ message,
196
+ recommendation,
197
+ value: formatSize(totalSize),
198
+ };
199
+ }
200
+ /**
201
+ * Get status icon
202
+ */
203
+ function getStatusIcon(status) {
204
+ switch (status) {
205
+ case 'healthy':
206
+ return chalk.green('✓');
207
+ case 'warning':
208
+ return chalk.yellow('⚠️ ');
209
+ case 'critical':
210
+ return chalk.red('❌');
211
+ }
212
+ }
213
+ /**
214
+ * Execute doctor command
215
+ */
216
+ export async function doctorCommand(options) {
217
+ printHeader('🏥 System Health Check');
218
+ console.log(chalk.dim('Running diagnostics...'));
219
+ console.log();
220
+ // Run all health checks
221
+ const checks = await Promise.all([
222
+ checkDiskUsage(),
223
+ checkCacheSizes(),
224
+ checkLargeFiles(),
225
+ checkBrokenSymlinks(),
226
+ checkOldFiles(),
227
+ ]);
228
+ // Display results
229
+ for (const check of checks) {
230
+ const icon = getStatusIcon(check.status);
231
+ const statusColor = check.status === 'healthy'
232
+ ? chalk.green
233
+ : check.status === 'warning'
234
+ ? chalk.yellow
235
+ : chalk.red;
236
+ console.log(`${icon} ${chalk.bold(check.name)}: ${check.message}`);
237
+ if (check.value && options.verbose) {
238
+ console.log(chalk.dim(` Value: ${check.value}`));
239
+ }
240
+ if (check.recommendation) {
241
+ console.log(chalk.dim(` → ${check.recommendation}`));
242
+ }
243
+ console.log();
244
+ }
245
+ // Summary
246
+ separator();
247
+ console.log();
248
+ const healthyCount = checks.filter((c) => c.status === 'healthy').length;
249
+ const warningCount = checks.filter((c) => c.status === 'warning').length;
250
+ const criticalCount = checks.filter((c) => c.status === 'critical').length;
251
+ console.log(chalk.bold('📊 Summary:'));
252
+ console.log(` ${chalk.green('●')} Healthy: ${healthyCount}`);
253
+ console.log(` ${chalk.yellow('●')} Warning: ${warningCount}`);
254
+ console.log(` ${chalk.red('●')} Critical: ${criticalCount}`);
255
+ console.log();
256
+ if (criticalCount > 0) {
257
+ error('Critical issues detected! Please take action immediately.');
258
+ }
259
+ else if (warningCount > 0) {
260
+ warning('Some issues detected. Consider cleaning up.');
261
+ }
262
+ else {
263
+ success('Your system looks healthy!');
264
+ }
265
+ console.log();
266
+ console.log(chalk.dim('Tip: Run "broom clean" to free up space'));
267
+ console.log(chalk.dim(' Run "broom doctor --verbose" for detailed information'));
268
+ }
269
+ /**
270
+ * Create doctor command
271
+ */
272
+ export function createDoctorCommand() {
273
+ const cmd = new Command('doctor')
274
+ .description('Run system health diagnostics')
275
+ .option('-v, --verbose', 'Show detailed information')
276
+ .action(async (options) => {
277
+ await doctorCommand(options);
278
+ });
279
+ return enhanceCommandHelp(cmd);
280
+ }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Duplicates command - Find and remove duplicate files
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, createSpinner, succeedSpinner, } from '../ui/output.js';
9
+ import { readdir, stat, unlink } from 'fs/promises';
10
+ import { join, relative } from 'path';
11
+ import { createHash } from 'crypto';
12
+ import { createReadStream } from 'fs';
13
+ /**
14
+ * Parse size string (e.g., "1MB", "500KB", "2GB")
15
+ */
16
+ function parseSize(sizeStr) {
17
+ const match = sizeStr.match(/^(\d+(?:\.\d+)?)(KB|MB|GB)?$/i);
18
+ if (!match) {
19
+ throw new Error(`Invalid size format: ${sizeStr}`);
20
+ }
21
+ const value = parseFloat(match[1]);
22
+ const unit = (match[2] || 'B').toUpperCase();
23
+ switch (unit) {
24
+ case 'KB':
25
+ return value * 1024;
26
+ case 'MB':
27
+ return value * 1024 * 1024;
28
+ case 'GB':
29
+ return value * 1024 * 1024 * 1024;
30
+ default:
31
+ return value;
32
+ }
33
+ }
34
+ /**
35
+ * Calculate file hash
36
+ */
37
+ async function calculateFileHash(filePath, algorithm = 'sha256') {
38
+ return new Promise((resolve, reject) => {
39
+ const hash = createHash(algorithm);
40
+ const stream = createReadStream(filePath);
41
+ stream.on('data', (data) => hash.update(data));
42
+ stream.on('end', () => resolve(hash.digest('hex')));
43
+ stream.on('error', reject);
44
+ });
45
+ }
46
+ /**
47
+ * Optimized hash calculation for large files
48
+ */
49
+ async function calculateSmartHash(filePath, fileSize) {
50
+ // For small files (<1MB), use full hash
51
+ if (fileSize < 1024 * 1024) {
52
+ return await calculateFileHash(filePath, 'sha256');
53
+ }
54
+ // For large files, use size + partial hash
55
+ const partialHash = await calculatePartialHash(filePath);
56
+ return `${fileSize}:${partialHash}`;
57
+ }
58
+ /**
59
+ * Calculate partial hash (first and last 64KB)
60
+ */
61
+ async function calculatePartialHash(filePath) {
62
+ return new Promise((resolve, reject) => {
63
+ const hash = createHash('sha256');
64
+ const stream = createReadStream(filePath, {
65
+ start: 0,
66
+ end: 64 * 1024, // First 64KB
67
+ });
68
+ stream.on('data', (data) => hash.update(data));
69
+ stream.on('end', () => {
70
+ // Also read last 64KB
71
+ const endStream = createReadStream(filePath, {
72
+ start: -64 * 1024,
73
+ });
74
+ endStream.on('data', (data) => hash.update(data));
75
+ endStream.on('end', () => resolve(hash.digest('hex')));
76
+ endStream.on('error', reject);
77
+ });
78
+ stream.on('error', reject);
79
+ });
80
+ }
81
+ /**
82
+ * Get all files recursively
83
+ */
84
+ async function getAllFiles(dirPath, minSize = 0, files = []) {
85
+ try {
86
+ const entries = await readdir(dirPath, { withFileTypes: true });
87
+ for (const entry of entries) {
88
+ const fullPath = join(dirPath, entry.name);
89
+ // Skip hidden files and system directories
90
+ if (entry.name.startsWith('.'))
91
+ continue;
92
+ if (entry.isDirectory()) {
93
+ await getAllFiles(fullPath, minSize, files);
94
+ }
95
+ else if (entry.isFile()) {
96
+ const stats = await stat(fullPath);
97
+ if (stats.size >= minSize) {
98
+ files.push(fullPath);
99
+ }
100
+ }
101
+ }
102
+ }
103
+ catch {
104
+ // Skip directories we cannot read
105
+ }
106
+ return files;
107
+ }
108
+ /**
109
+ * Find duplicates
110
+ */
111
+ async function findDuplicates(paths, minSize = 0, hashAlgorithm = 'sha256') {
112
+ const spinner = createSpinner('Scanning files...');
113
+ // Get all files
114
+ const allFiles = [];
115
+ for (const path of paths) {
116
+ await getAllFiles(path, minSize, allFiles);
117
+ }
118
+ succeedSpinner(spinner, `Found ${allFiles.length} files`);
119
+ // Group by size first (optimization)
120
+ const sizeGroups = new Map();
121
+ for (const file of allFiles) {
122
+ const stats = await stat(file);
123
+ const size = stats.size;
124
+ if (!sizeGroups.has(size)) {
125
+ sizeGroups.set(size, []);
126
+ }
127
+ sizeGroups.get(size).push(file);
128
+ }
129
+ // Only hash files with duplicate sizes
130
+ const hashSpinner = createSpinner('Calculating hashes...');
131
+ const hashMap = new Map();
132
+ let processed = 0;
133
+ for (const [size, files] of sizeGroups.entries()) {
134
+ if (files.length > 1) {
135
+ // Multiple files with same size - need to hash
136
+ for (const file of files) {
137
+ const hash = await calculateSmartHash(file, size);
138
+ if (!hashMap.has(hash)) {
139
+ hashMap.set(hash, []);
140
+ }
141
+ hashMap.get(hash).push(file);
142
+ processed++;
143
+ if (processed % 10 === 0) {
144
+ hashSpinner.text = `Hashing files... (${processed}/${files.length * sizeGroups.size})`;
145
+ }
146
+ }
147
+ }
148
+ }
149
+ succeedSpinner(hashSpinner, `Hashed ${processed} files`);
150
+ // Return only groups with duplicates
151
+ return new Map([...hashMap.entries()].filter(([_, files]) => files.length > 1));
152
+ }
153
+ /**
154
+ * Interactive duplicate handler
155
+ */
156
+ async function handleDuplicatesInteractive(duplicates) {
157
+ const { select } = await import('../ui/prompts.js');
158
+ const cwd = process.cwd();
159
+ let groupIndex = 1;
160
+ for (const [hash, files] of duplicates.entries()) {
161
+ console.log();
162
+ console.log(chalk.bold(`Group ${groupIndex} (${files.length} files, ${formatSize((await stat(files[0])).size)} each):`));
163
+ console.log();
164
+ for (let i = 0; i < files.length; i++) {
165
+ const file = files[i];
166
+ const stats = await stat(file);
167
+ const relPath = relative(cwd, file);
168
+ const modTime = new Date(stats.mtime).toLocaleString('ja-JP');
169
+ // Create clickable file link (Cmd+Click in terminal opens in Finder)
170
+ console.log(` ${i + 1}. ${chalk.cyan.underline(`file://${file}`)}`);
171
+ console.log(` ${chalk.dim(`└─ ${relPath}`)}`);
172
+ console.log(` ${chalk.dim(` Modified: ${modTime}`)}`);
173
+ }
174
+ console.log();
175
+ const action = await select({
176
+ message: 'What would you like to do?',
177
+ choices: [
178
+ { name: 'Keep first, delete others', value: 'delete-others' },
179
+ { name: 'Keep last, delete others', value: 'delete-except-last' },
180
+ { name: 'Skip this group', value: 'skip' },
181
+ { name: 'Hardlink duplicates', value: 'hardlink' },
182
+ { name: 'Exit', value: 'exit' },
183
+ ],
184
+ });
185
+ if (action === 'exit') {
186
+ break;
187
+ }
188
+ if (action === 'skip') {
189
+ groupIndex++;
190
+ continue;
191
+ }
192
+ if (action === 'delete-others') {
193
+ for (let i = 1; i < files.length; i++) {
194
+ await unlink(files[i]);
195
+ console.log(chalk.dim(` Deleted: ${files[i]}`));
196
+ }
197
+ success(`Kept: ${files[0]}`);
198
+ }
199
+ else if (action === 'delete-except-last') {
200
+ for (let i = 0; i < files.length - 1; i++) {
201
+ await unlink(files[i]);
202
+ console.log(chalk.dim(` Deleted: ${files[i]}`));
203
+ }
204
+ success(`Kept: ${files[files.length - 1]}`);
205
+ }
206
+ else if (action === 'hardlink') {
207
+ // Keep first, hardlink others
208
+ for (let i = 1; i < files.length; i++) {
209
+ await unlink(files[i]);
210
+ const { execSync } = await import('child_process');
211
+ execSync(`ln "${files[0]}" "${files[i]}"`);
212
+ console.log(chalk.dim(` Hardlinked: ${files[i]} -> ${files[0]}`));
213
+ }
214
+ success('Created hardlinks');
215
+ }
216
+ groupIndex++;
217
+ }
218
+ }
219
+ /**
220
+ * Validate and get scan path
221
+ */
222
+ async function getValidPath(initialPath) {
223
+ const { input } = await import('../ui/prompts.js');
224
+ let path = initialPath ? expandPath(initialPath) : expandPath('~');
225
+ // If path provided via option, validate it
226
+ if (initialPath) {
227
+ const pathExists = await exists(path);
228
+ if (!pathExists) {
229
+ error(`Path does not exist: ${path}`);
230
+ console.log();
231
+ // Prompt for valid path
232
+ while (true) {
233
+ const newPath = await input({
234
+ message: 'Enter a valid path to scan:',
235
+ default: expandPath('~'),
236
+ });
237
+ const expandedPath = expandPath(newPath);
238
+ const pathExists = await exists(expandedPath);
239
+ if (pathExists) {
240
+ path = expandedPath;
241
+ break;
242
+ }
243
+ else {
244
+ error(`Path does not exist: ${expandedPath}`);
245
+ console.log();
246
+ }
247
+ }
248
+ }
249
+ }
250
+ return path;
251
+ }
252
+ /**
253
+ * Execute duplicates command
254
+ */
255
+ export async function duplicatesCommand(options) {
256
+ const path = await getValidPath(options.path);
257
+ const paths = [path];
258
+ const minSize = options.minSize ? parseSize(options.minSize) : 1024 * 1024; // Default 1MB
259
+ const hashAlgorithm = options.hash || 'sha256';
260
+ printHeader('🔍 Duplicate File Finder');
261
+ console.log(chalk.dim(`Scanning: ${paths.join(', ')}`));
262
+ console.log(chalk.dim(`Min size: ${formatSize(minSize)}`));
263
+ console.log(chalk.dim(`Hash algorithm: ${hashAlgorithm}`));
264
+ console.log();
265
+ const duplicates = await findDuplicates(paths, minSize, hashAlgorithm);
266
+ if (duplicates.size === 0) {
267
+ success('No duplicates found!');
268
+ return;
269
+ }
270
+ // Calculate potential space savings
271
+ let totalSavings = 0;
272
+ for (const [_, files] of duplicates.entries()) {
273
+ const fileSize = (await stat(files[0])).size;
274
+ totalSavings += fileSize * (files.length - 1);
275
+ }
276
+ console.log();
277
+ separator();
278
+ console.log();
279
+ console.log(chalk.bold('📊 Summary:'));
280
+ console.log(` Duplicate groups: ${duplicates.size}`);
281
+ console.log(` Potential savings: ${chalk.yellow(formatSize(totalSavings))}`);
282
+ console.log();
283
+ if (options.interactive || options.delete) {
284
+ await handleDuplicatesInteractive(duplicates);
285
+ }
286
+ else {
287
+ // Just list duplicates with details
288
+ const cwd = process.cwd();
289
+ let groupIndex = 1;
290
+ for (const [_, files] of duplicates.entries()) {
291
+ const fileSize = (await stat(files[0])).size;
292
+ console.log(chalk.bold(`Group ${groupIndex} (${files.length} files, ${formatSize(fileSize)} each):`));
293
+ console.log();
294
+ for (let i = 0; i < files.length; i++) {
295
+ const file = files[i];
296
+ const stats = await stat(file);
297
+ const relPath = relative(cwd, file);
298
+ const modTime = new Date(stats.mtime).toLocaleString('ja-JP');
299
+ // Create clickable file link
300
+ console.log(` ${i + 1}. ${chalk.cyan.underline(`file://${file}`)}`);
301
+ console.log(` ${chalk.dim(`└─ ${relPath}`)}`);
302
+ console.log(` ${chalk.dim(` Modified: ${modTime}`)}`);
303
+ }
304
+ console.log();
305
+ groupIndex++;
306
+ }
307
+ console.log(chalk.dim('Use --interactive to select which files to keep/delete'));
308
+ }
309
+ }
310
+ /**
311
+ * Create duplicates command
312
+ */
313
+ export function createDuplicatesCommand() {
314
+ const cmd = new Command('duplicates')
315
+ .description('Find and remove duplicate files')
316
+ .option('-p, --path <path>', 'Path to scan (default: home directory)')
317
+ .option('--min-size <size>', 'Minimum file size (e.g., 1MB, 500KB)', '1MB')
318
+ .option('--hash <algorithm>', 'Hash algorithm (md5 or sha256)', 'sha256')
319
+ .option('-i, --interactive', 'Interactive mode to choose which files to delete')
320
+ .option('-d, --delete', 'Automatically delete duplicates (keep first)')
321
+ .action(async (options) => {
322
+ await duplicatesCommand(options);
323
+ });
324
+ return enhanceCommandHelp(cmd);
325
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Help command - Display help for any command
3
+ */
4
+ import { Command } from 'commander';
5
+ import { formatCommandHelp } from '../utils/help.js';
6
+ import { info } from '../ui/output.js';
7
+ let allCommands = [];
8
+ export function setCommandsList(commands) {
9
+ allCommands = commands;
10
+ }
11
+ export function createHelpCommand() {
12
+ return new Command('help')
13
+ .description('Display help for a command')
14
+ .argument('[command]', 'The command to get help for')
15
+ .action((commandName) => {
16
+ if (!commandName) {
17
+ info('Usage: broom help <command>');
18
+ info('');
19
+ info('Examples:');
20
+ info(' broom help clean');
21
+ info(' broom help analyze');
22
+ info(' broom help uninstall');
23
+ return;
24
+ }
25
+ // Find the specified command
26
+ const command = allCommands.find((cmd) => cmd.name() === commandName);
27
+ if (!command) {
28
+ info(`Error: Unknown command "${commandName}"`);
29
+ process.exit(1);
30
+ }
31
+ // Display the command help
32
+ info(formatCommandHelp(command));
33
+ });
34
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Commands module exports
3
+ */
4
+ export { createCleanCommand } from './clean.js';
5
+ export { createUninstallCommand } from './uninstall.js';
6
+ export { createOptimizeCommand } from './optimize.js';
7
+ export { createAnalyzeCommand } from './analyze.js';
8
+ export { createStatusCommand } from './status.js';
9
+ export { createPurgeCommand } from './purge.js';
10
+ export { createInstallerCommand } from './installer.js';
11
+ export { createTouchIdCommand } from './touchid.js';
12
+ export { createCompletionCommand } from './completion.js';
13
+ export { createUpdateCommand } from './update.js';
14
+ export { createRemoveCommand } from './remove.js';
15
+ export { createConfigCommand } from './config.js';
16
+ export { createDoctorCommand } from './doctor.js';
17
+ export { createBackupCommand, createRestoreCommand } from './backup.js';
18
+ export { createDuplicatesCommand } from './duplicates.js';
19
+ export { createScheduleCommand } from './schedule.js';
20
+ export { createWatchCommand } from './watch.js';
21
+ export { createReportsCommand } from './reports.js';
22
+ export { createHelpCommand, setCommandsList } from './help.js';