@tukuyomil032/broom 1.0.1 → 1.0.5

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.
@@ -3,18 +3,22 @@
3
3
  */
4
4
  import chalk from 'chalk';
5
5
  import { Command } from 'commander';
6
+ import blessed from 'blessed';
6
7
  import { enhanceCommandHelp } from '../utils/help.js';
7
8
  import { readdir, stat } from 'fs/promises';
8
- import { join, basename } from 'path';
9
- import { exists, getSize, formatSize, expandPath } from '../utils/fs.js';
10
- import { printHeader, warning, info, separator, createSpinner, succeedSpinner, failSpinner, } from '../ui/output.js';
9
+ import { join, basename, dirname } from 'path';
10
+ import { getSize, formatSize, expandPath } from '../utils/fs.js';
11
+ import { exec } from 'child_process';
12
+ import { promisify } from 'util';
13
+ import { createSpinner, succeedSpinner, failSpinner, } from '../ui/output.js';
14
+ const execAsync = promisify(exec);
11
15
  // Fixed column widths for aligned display
12
16
  const NAME_WIDTH = 25;
13
17
  const BAR_WIDTH = 30;
14
18
  /**
15
- * Scan directory for sizes
19
+ * Scan directory for sizes - recursive with optional depth limit
16
20
  */
17
- async function scanDirectory(dirPath, depth, currentDepth = 0) {
21
+ async function scanDirectory(dirPath, currentDepth = 0, maxDepth) {
18
22
  try {
19
23
  const stats = await stat(dirPath);
20
24
  const name = basename(dirPath) || dirPath;
@@ -24,6 +28,7 @@ async function scanDirectory(dirPath, depth, currentDepth = 0) {
24
28
  name,
25
29
  size: stats.size,
26
30
  isDirectory: false,
31
+ mtime: stats.mtime,
27
32
  };
28
33
  }
29
34
  // Get total size
@@ -33,44 +38,57 @@ async function scanDirectory(dirPath, depth, currentDepth = 0) {
33
38
  name,
34
39
  size,
35
40
  isDirectory: true,
41
+ mtime: stats.mtime,
36
42
  };
37
- // Get children if within depth
38
- if (currentDepth < depth) {
39
- try {
40
- const entries = await readdir(dirPath);
41
- const children = [];
42
- for (const entry of entries) {
43
- // Skip hidden files at top level
44
- if (entry.startsWith('.') && currentDepth === 0) {
45
- continue;
46
- }
47
- const childPath = join(dirPath, entry);
48
- // Skip excluded paths (iCloud Drive, etc.)
49
- const { isExcludedPath } = await import('../utils/fs.js');
50
- if (isExcludedPath(childPath)) {
51
- continue;
43
+ // Check depth limit
44
+ if (maxDepth !== undefined && currentDepth >= maxDepth) {
45
+ info.children = [];
46
+ return info;
47
+ }
48
+ // Recursively scan subdirectories
49
+ try {
50
+ const entries = await readdir(dirPath);
51
+ const children = [];
52
+ for (const entry of entries) {
53
+ // Skip hidden files at top level
54
+ if (entry.startsWith('.') && currentDepth === 0) {
55
+ continue;
56
+ }
57
+ const childPath = join(dirPath, entry);
58
+ // Skip excluded paths (iCloud Drive, etc.)
59
+ const { isExcludedPath } = await import('../utils/fs.js');
60
+ if (isExcludedPath(childPath)) {
61
+ continue;
62
+ }
63
+ try {
64
+ const childStats = await stat(childPath);
65
+ if (childStats.isDirectory()) {
66
+ // Recursively scan subdirectories
67
+ const childInfo = await scanDirectory(childPath, currentDepth + 1, maxDepth);
68
+ if (childInfo) {
69
+ children.push(childInfo);
70
+ }
52
71
  }
53
- try {
54
- const childStats = await stat(childPath);
55
- const childSize = childStats.isDirectory() ? await getSize(childPath) : childStats.size;
72
+ else {
73
+ const childSize = childStats.size;
56
74
  children.push({
57
75
  path: childPath,
58
76
  name: entry,
59
77
  size: childSize,
60
- isDirectory: childStats.isDirectory(),
78
+ isDirectory: false,
61
79
  });
62
80
  }
63
- catch {
64
- // Skip if cannot access
65
- }
66
81
  }
67
- // Sort by size descending
68
- children.sort((a, b) => b.size - a.size);
69
- info.children = children;
70
- }
71
- catch {
72
- // Cannot read directory
82
+ catch {
83
+ // Skip if cannot access
84
+ }
73
85
  }
86
+ // Sort by size descending
87
+ children.sort((a, b) => b.size - a.size);
88
+ info.children = children;
89
+ }
90
+ catch {
91
+ // Cannot read directory
74
92
  }
75
93
  return info;
76
94
  }
@@ -79,35 +97,95 @@ async function scanDirectory(dirPath, depth, currentDepth = 0) {
79
97
  }
80
98
  }
81
99
  /**
82
- * Generate size bar for Quick Analysis - with borders and gridlines
100
+ * Generate disk usage bar graph with chalk colors.
101
+ * Safe to use in listBox with tags:false (ANSI codes rendered by terminal directly).
83
102
  */
84
- function generateQuickAnalysisBar(size, maxSize, width = 20) {
85
- const percentage = maxSize > 0 ? size / maxSize : 0;
86
- const filledWidth = Math.round(percentage * width);
103
+ function generateUsageBar(size, parentSize, width = 20) {
104
+ const percentage = parentSize > 0 ? size / parentSize : 0;
105
+ const clamped = Math.min(percentage, 1);
106
+ const filledWidth = Math.round(clamped * width);
87
107
  let bar = '';
88
- // Color based on relative size
89
- let color;
90
- if (percentage > 0.7) {
91
- color = chalk.red;
92
- }
93
- else if (percentage > 0.3) {
94
- color = chalk.hex('#FFA500'); // Orange
95
- }
96
- else {
97
- color = chalk.gray;
108
+ for (let i = 0; i < width; i++) {
109
+ if (i < filledWidth) {
110
+ const ratio = i / width;
111
+ if (ratio < 0.5) {
112
+ bar += chalk.green('█');
113
+ }
114
+ else if (ratio < 0.75) {
115
+ bar += chalk.yellow('█');
116
+ }
117
+ else {
118
+ bar += chalk.red('█');
119
+ }
120
+ }
121
+ else {
122
+ bar += chalk.gray('░');
123
+ }
98
124
  }
99
- // Build bar with gridlines every 20%
125
+ const pct = (clamped * 100).toFixed(1);
126
+ return `${bar} ${pct}%`;
127
+ }
128
+ /**
129
+ * Generate disk usage bar using blessed tags (for info box display)
130
+ */
131
+ function generateUsageBarBlessed(size, parentSize, width = 28) {
132
+ const percentage = parentSize > 0 ? size / parentSize : 0;
133
+ const clamped = Math.min(percentage, 1);
134
+ const filledWidth = Math.round(clamped * width);
135
+ let bar = '';
100
136
  for (let i = 0; i < width; i++) {
101
- const isGridline = i > 0 && i % (width / 5) === 0;
102
137
  if (i < filledWidth) {
103
- bar += isGridline ? chalk.white('│') : color('█');
138
+ const ratio = i / width;
139
+ if (ratio < 0.5) {
140
+ bar += '{green-fg}█{/green-fg}';
141
+ }
142
+ else if (ratio < 0.75) {
143
+ bar += '{yellow-fg}█{/yellow-fg}';
144
+ }
145
+ else {
146
+ bar += '{red-fg}█{/red-fg}';
147
+ }
104
148
  }
105
149
  else {
106
- bar += isGridline ? chalk.gray('│') : chalk.gray('░');
150
+ bar += '{white-fg}{/white-fg}';
107
151
  }
108
152
  }
109
- // Add horizontal borders
110
- return chalk.gray('│') + bar + chalk.gray('│');
153
+ const pct = (clamped * 100).toFixed(1);
154
+ // NOTE: percentage is plain text, NOT wrapped in blessed tags
155
+ return `${bar} ${pct}%`;
156
+ }
157
+ /**
158
+ * Normalize path for comparison
159
+ */
160
+ function normalizePath(p) {
161
+ return p.replace(/\/+$/, '') || '/'; // Remove trailing slashes
162
+ }
163
+ /**
164
+ * Get folder at path from DirInfo tree
165
+ */
166
+ function getFolderAtPath(root, targetPath) {
167
+ const normalizedTarget = normalizePath(targetPath);
168
+ const normalizedRoot = normalizePath(root.path);
169
+ if (normalizedRoot === normalizedTarget) {
170
+ return root;
171
+ }
172
+ if (!root.children) {
173
+ return null;
174
+ }
175
+ for (const child of root.children) {
176
+ const result = getFolderAtPath(child, targetPath);
177
+ if (result) {
178
+ return result;
179
+ }
180
+ }
181
+ return null;
182
+ }
183
+ /**
184
+ * Get immediate children of a folder
185
+ */
186
+ function getChildrenAtPath(root, targetPath) {
187
+ const folder = getFolderAtPath(root, targetPath);
188
+ return folder?.children || [];
111
189
  }
112
190
  /**
113
191
  * Generate size bar for tree display with gridlines
@@ -210,7 +288,7 @@ function generateDiskBar(used, total, width = 40) {
210
288
  /**
211
289
  * Print directory tree with aligned columns
212
290
  */
213
- function printTree(items, maxSize, limit, indent = '') {
291
+ function printTree(items, parentSize, limit, indent = '') {
214
292
  const displayed = items.slice(0, limit);
215
293
  const remaining = items.length - limit;
216
294
  // Calculate maximum name width from displayed items (with reasonable limit)
@@ -218,6 +296,8 @@ function printTree(items, maxSize, limit, indent = '') {
218
296
  const maxNameLength = Math.min(Math.max(...displayed.map((item) => item.name.length), NAME_WIDTH // Minimum width
219
297
  ), MAX_NAME_WIDTH // Maximum width
220
298
  );
299
+ // Find max size for bar visualization (visual reference only)
300
+ const maxSizeForBar = Math.max(...displayed.map((item) => item.size), 1);
221
301
  for (let i = 0; i < displayed.length; i++) {
222
302
  const item = displayed[i];
223
303
  const isLast = i === displayed.length - 1 && remaining <= 0;
@@ -233,9 +313,11 @@ function printTree(items, maxSize, limit, indent = '') {
233
313
  displayName.substring(displayName.length - keepLength);
234
314
  }
235
315
  displayName = displayName.padEnd(maxNameLength);
236
- const bar = generateTreeBar(item.size, maxSize);
316
+ // Use parent size as reference for bar visualization
317
+ const bar = generateTreeBar(item.size, parentSize);
237
318
  const sizeStr = formatSize(item.size).padStart(10);
238
- const percentage = maxSize > 0 ? ((item.size / maxSize) * 100).toFixed(1).padStart(5) + '%' : ' 0.0%';
319
+ // Calculate percentage relative to parent directory total size
320
+ const percentage = parentSize > 0 ? ((item.size / parentSize) * 100).toFixed(1).padStart(5) + '%' : ' 0.0%';
239
321
  console.log(`${indent}${prefix}${icon} ${chalk.bold(displayName)} ${bar} ${chalk.cyan(sizeStr)} ${chalk.dim(percentage)}`);
240
322
  }
241
323
  if (remaining > 0) {
@@ -257,103 +339,305 @@ async function getDiskUsage() {
257
339
  }
258
340
  }
259
341
  /**
260
- * Execute analyze command
342
+ * Execute analyze command with interactive UI and optimized loading
261
343
  */
262
344
  export async function analyzeCommand(options) {
263
- const targetPath = options.path ? expandPath(options.path) : expandPath('~');
264
- const depth = options.depth ?? 1;
265
- const limit = options.limit ?? 15;
266
- printHeader(`📊 Disk Space Analysis`);
267
- // Show disk usage with improved bar
268
- const diskUsage = await getDiskUsage();
269
- if (diskUsage) {
270
- console.log(chalk.bold('💾 Disk Usage:'));
271
- const usedPercent = (diskUsage.used / diskUsage.total) * 100;
272
- console.log(generateDiskBar(diskUsage.used, diskUsage.total));
273
- console.log();
274
- console.log(` Used: ${chalk.yellow(formatSize(diskUsage.used))} / ${formatSize(diskUsage.total)} (${usedPercent.toFixed(1)}%)`);
275
- console.log(` Free: ${chalk.green(formatSize(diskUsage.free))}`);
276
- console.log();
277
- }
278
- // Scan target directory
279
- info(`Analyzing system disk...`);
280
- console.log(chalk.bold(`Target: ${chalk.cyan(targetPath)}`));
281
- console.log();
282
- const spinner = createSpinner('Scanning directory sizes...');
283
- const dirInfo = await scanDirectory(targetPath, depth);
284
- if (!dirInfo) {
345
+ const rawPath = options.positionalPath || options.path;
346
+ const targetPath = rawPath ? expandPath(rawPath) : expandPath('~');
347
+ // First, show initial message in console
348
+ console.clear();
349
+ console.log(chalk.bold.cyan('\n🧹 Broom - Disk Space Analyzer\n'));
350
+ const spinner = createSpinner('Scanning directory structure (initial scan)...');
351
+ // First pass: shallow scan for quick display
352
+ let rootInfo = await scanDirectory(targetPath, 0, 2);
353
+ if (!rootInfo) {
285
354
  failSpinner(spinner, 'Failed to scan directory');
286
355
  return;
287
356
  }
288
- succeedSpinner(spinner, `Scanned ${dirInfo.children?.length ?? 0} items`);
289
- // Print results
290
- console.log();
291
- console.log(chalk.bold(`📁 ${dirInfo.name}`));
292
- console.log(` Total size: ${chalk.yellow(formatSize(dirInfo.size))}`);
293
- console.log();
294
- // Calculate maximum name width for header alignment (with reasonable limit)
295
- const MAX_NAME_WIDTH = 40;
296
- const maxNameLength = dirInfo.children && dirInfo.children.length > 0
297
- ? Math.min(Math.max(...dirInfo.children.slice(0, limit).map((item) => item.name.length), NAME_WIDTH), MAX_NAME_WIDTH)
298
- : NAME_WIDTH;
299
- // Print header for aligned columns
300
- const headerName = 'Name'.padEnd(maxNameLength);
301
- console.log(chalk.dim(` ${headerName} ${'0% 20% 40% 60% 80% 100%'.padStart(BAR_WIDTH + 2)} Size Ratio`));
302
- console.log();
303
- if (dirInfo.children && dirInfo.children.length > 0) {
304
- const maxSize = dirInfo.children[0]?.size ?? 1;
305
- printTree(dirInfo.children, maxSize, limit);
306
- }
307
- else {
308
- warning('No items found in directory');
357
+ succeedSpinner(spinner, 'Initial scan complete');
358
+ if (!rootInfo.children || rootInfo.children.length === 0) {
359
+ console.log(chalk.yellow('No items found in directory'));
360
+ return;
309
361
  }
310
- // Show common large directories
311
- console.log();
312
- separator();
313
- console.log();
314
- console.log(chalk.bold('💡 Quick Analysis:'));
315
- console.log();
316
- // Header for Quick Analysis with scale
317
- console.log(chalk.dim(' Location 0% 20% 40% 60% 80% 100% Size'));
318
- console.log();
319
- const quickPaths = [
320
- { path: '~/Library/Caches', label: 'User Caches' },
321
- { path: '~/Library/Application Support', label: 'App Support' },
322
- { path: '~/.Trash', label: 'Trash' },
323
- { path: '~/Downloads', label: 'Downloads' },
324
- { path: '~/Library/Developer', label: 'Developer Data' },
325
- ];
326
- const quickResults = [];
327
- for (const item of quickPaths) {
328
- const fullPath = expandPath(item.path);
329
- if (exists(fullPath)) {
330
- const size = await getSize(fullPath);
331
- quickResults.push({ label: item.label, size });
362
+ // Start UI display
363
+ let currentPath = targetPath;
364
+ function displayDirectory() {
365
+ // Normalize current path for consistency
366
+ const normalizedPath = normalizePath(currentPath);
367
+ // Get current folder and its direct children
368
+ const currentFolder = getFolderAtPath(rootInfo, normalizedPath);
369
+ if (!currentFolder) {
370
+ console.log(chalk.red(`Error: Could not find folder at ${normalizedPath}. Current root: ${normalizePath(rootInfo.path)}`));
371
+ return;
332
372
  }
373
+ const items = currentFolder.children || [];
374
+ // Create blessed screen for interactive UI
375
+ const screen = blessed.screen({
376
+ smartCSR: true,
377
+ title: 'Broom - Disk Space Analyzer',
378
+ fullUnicode: true,
379
+ });
380
+ // Header box
381
+ const headerBox = blessed.box({
382
+ parent: screen,
383
+ top: 0,
384
+ left: 0,
385
+ width: '100%',
386
+ height: 3,
387
+ tags: true,
388
+ border: { type: 'line' },
389
+ style: { border: { fg: 'cyan' } },
390
+ content: ` {bold}📊 ${normalizedPath}{/bold}\n {dim}Total: ${formatSize(currentFolder.size)} | Items: ${items.length}{/dim}`,
391
+ });
392
+ // Main list box - folders/files in current directory
393
+ // tags: false to prevent ANSI codes in item text from being misinterpreted
394
+ const listBox = blessed.list({
395
+ parent: screen,
396
+ top: 3,
397
+ left: 0,
398
+ width: '60%',
399
+ height: '100%-6',
400
+ label: ' Contents ',
401
+ tags: false,
402
+ border: { type: 'line' },
403
+ style: {
404
+ border: { fg: 'cyan' },
405
+ selected: { bg: 'blue', fg: 'white', bold: true },
406
+ },
407
+ mouse: true,
408
+ keys: true,
409
+ });
410
+ // Info box - details of selected item
411
+ const infoBox = blessed.box({
412
+ parent: screen,
413
+ top: 3,
414
+ left: '60%',
415
+ width: '40%',
416
+ height: '100%-6',
417
+ label: ' {cyan-fg}↕{/cyan-fg} Details ',
418
+ tags: true,
419
+ border: { type: 'line' },
420
+ style: { border: { fg: 'cyan' } },
421
+ scrollable: true,
422
+ mouse: true,
423
+ });
424
+ // Footer box
425
+ blessed.box({
426
+ parent: screen,
427
+ bottom: 0,
428
+ left: 0,
429
+ width: '100%',
430
+ height: 3,
431
+ tags: true,
432
+ border: { type: 'line' },
433
+ style: { border: { fg: 'cyan' } },
434
+ content: '{cyan-fg}↑↓{/cyan-fg} Navigate | {green-fg}Enter{/green-fg} Open folder | {red-fg}o{/red-fg} Open in Finder | {yellow-fg}b{/yellow-fg} Back | {red-fg}q/Esc{/red-fg} Quit',
435
+ });
436
+ // Populate list with items (plain text only - no ANSI/tags to avoid index corruption)
437
+ let selectedIndex = 0;
438
+ items.forEach((item) => {
439
+ const icon = item.isDirectory ? '>' : ' ';
440
+ const name = item.name.substring(0, 24).padEnd(24);
441
+ const bar = generateUsageBar(item.size, currentFolder.size, 18);
442
+ listBox.addItem(`${icon} ${name} ${bar}`);
443
+ });
444
+ // Update info box when selection changes
445
+ const updateInfoBox = (index) => {
446
+ if (index >= items.length || index < 0)
447
+ return;
448
+ selectedIndex = index;
449
+ const selectedItem = items[index];
450
+ if (!selectedItem)
451
+ return;
452
+ const usageBar = generateUsageBarBlessed(selectedItem.size, currentFolder.size, 24);
453
+ const pct = currentFolder.size > 0
454
+ ? ((selectedItem.size / currentFolder.size) * 100).toFixed(1)
455
+ : '0.0';
456
+ // Rank in parent by size
457
+ const rank = items.indexOf(selectedItem) + 1;
458
+ // Children breakdown
459
+ const childDirs = selectedItem.children?.filter((c) => c.isDirectory).length ?? 0;
460
+ const childFiles = selectedItem.children?.filter((c) => !c.isDirectory).length ?? 0;
461
+ const hasChildren = childDirs + childFiles > 0;
462
+ // Last modified
463
+ const mtimeStr = selectedItem.mtime
464
+ ? selectedItem.mtime.toLocaleString('ja-JP', {
465
+ year: 'numeric',
466
+ month: '2-digit',
467
+ day: '2-digit',
468
+ hour: '2-digit',
469
+ minute: '2-digit',
470
+ })
471
+ : 'Unknown';
472
+ // Size comparison label
473
+ let sizeLabel = '';
474
+ if (pct !== '0.0') {
475
+ const p = parseFloat(pct);
476
+ if (p >= 30)
477
+ sizeLabel = ' {red-fg}(Very large){/red-fg}';
478
+ else if (p >= 10)
479
+ sizeLabel = ' {yellow-fg}(Large){/yellow-fg}';
480
+ else if (p >= 1)
481
+ sizeLabel = ' {green-fg}(Medium){/green-fg}';
482
+ else
483
+ sizeLabel = ' {cyan-fg}(Small){/cyan-fg}';
484
+ }
485
+ // Icon
486
+ const icon = selectedItem.isDirectory ? '📁' : '📄';
487
+ const typeLabel = selectedItem.isDirectory ? 'Directory' : 'File';
488
+ // Path - wrap at 30 chars
489
+ const pathParts = [];
490
+ let remaining = selectedItem.path;
491
+ while (remaining.length > 30) {
492
+ pathParts.push(remaining.slice(0, 30));
493
+ remaining = remaining.slice(30);
494
+ }
495
+ pathParts.push(remaining);
496
+ const wrappedPath = pathParts.join('\n ');
497
+ const divider = '{cyan-fg}' + '─'.repeat(28) + '{/cyan-fg}';
498
+ const infoText =
499
+ // Title
500
+ `${icon} {bold}{underline}${selectedItem.name}{/underline}{/bold}\n` +
501
+ `${divider}\n\n` +
502
+ // Usage
503
+ `{yellow-fg}📊 Usage{/yellow-fg}\n` +
504
+ ` ${usageBar}\n` +
505
+ ` {bold}${pct}%{/bold} of parent${sizeLabel}\n\n` +
506
+ // Rank
507
+ `{yellow-fg}🏆 Rank{/yellow-fg}\n` +
508
+ ` {bold}#${rank}{/bold} of ${items.length} items\n\n` +
509
+ // Size
510
+ `{yellow-fg}💾 Size{/yellow-fg}\n` +
511
+ ` {bold}${formatSize(selectedItem.size)}{/bold}\n\n` +
512
+ // Type
513
+ `{yellow-fg}📂 Type{/yellow-fg}\n` +
514
+ ` ${typeLabel}\n\n` +
515
+ // Contents (directories only)
516
+ (selectedItem.isDirectory
517
+ ? `{yellow-fg}📋 Contents{/yellow-fg}\n` +
518
+ (hasChildren
519
+ ? ` {green-fg}${childDirs} folders{/green-fg}, {cyan-fg}${childFiles} files{/cyan-fg}\n\n`
520
+ : ` {gray-fg}(Enter to scan){/gray-fg}\n\n`)
521
+ : '') +
522
+ // Last modified
523
+ `{yellow-fg}🕐 Modified{/yellow-fg}\n` +
524
+ ` ${mtimeStr}\n\n` +
525
+ // Path
526
+ `{yellow-fg}📍 Path{/yellow-fg}\n` +
527
+ ` {gray-fg}${wrappedPath}{/gray-fg}\n\n` +
528
+ `${divider}\n` +
529
+ // Actions
530
+ `{yellow-fg}⌨️ Actions{/yellow-fg}\n` +
531
+ (selectedItem.isDirectory ? ` {green-fg}[Enter]{/green-fg} Open folder\n` : '') +
532
+ ` {magenta-fg}[o]{/magenta-fg} Open in Finder\n` +
533
+ (normalizedPath !== normalizePath(targetPath) ? ` {cyan-fg}[b]{/cyan-fg} Go back\n` : '') +
534
+ ` {red-fg}[q]{/red-fg} Quit`;
535
+ infoBox.setContent(infoText);
536
+ screen.render();
537
+ };
538
+ // 'select item' fires on arrow key navigation in blessed List
539
+ // 'select' fires only on Enter - so use 'select item' for live updates
540
+ listBox.on('select item', (_item, index) => {
541
+ updateInfoBox(index);
542
+ });
543
+ // Open folder (navigate into it)
544
+ const openFolder = async () => {
545
+ const selectedItem = items[selectedIndex];
546
+ if (selectedItem && selectedItem.isDirectory) {
547
+ currentPath = selectedItem.path;
548
+ screen.destroy();
549
+ // If folder children are empty (not yet scanned due to shallow initial scan),
550
+ // scan this specific folder on-demand
551
+ if (!selectedItem.children || selectedItem.children.length === 0) {
552
+ const subSpinner = createSpinner(`Scanning ${selectedItem.name}...`);
553
+ const scanned = await scanDirectory(selectedItem.path, 0, 2);
554
+ if (scanned?.children && scanned.children.length > 0) {
555
+ selectedItem.children = scanned.children;
556
+ selectedItem.size = scanned.size;
557
+ }
558
+ succeedSpinner(subSpinner, `Scanned ${selectedItem.name}`);
559
+ }
560
+ displayDirectory();
561
+ }
562
+ };
563
+ // Open in Finder - keep screen, show result in info box
564
+ const openInFinder = async () => {
565
+ const selectedItem = items[selectedIndex];
566
+ if (selectedItem) {
567
+ try {
568
+ await execAsync(`open "${selectedItem.path}"`);
569
+ infoBox.setContent(infoBox.content + '\n\n{green-fg}✓ Opened in Finder{/green-fg}');
570
+ }
571
+ catch (err) {
572
+ infoBox.setContent(infoBox.content + '\n\n{red-fg}✗ Failed to open{/red-fg}');
573
+ }
574
+ screen.render();
575
+ }
576
+ };
577
+ // Go back to parent directory (never navigate above scan root)
578
+ const goBack = () => {
579
+ const normalizedPath = normalizePath(currentPath);
580
+ const normalizedTarget = normalizePath(targetPath);
581
+ // Already at scan root - do nothing
582
+ if (normalizedPath === normalizedTarget) {
583
+ return;
584
+ }
585
+ const parent = dirname(normalizedPath);
586
+ if (parent !== normalizedPath) {
587
+ currentPath = parent;
588
+ screen.destroy();
589
+ displayDirectory();
590
+ }
591
+ };
592
+ // Keyboard handlers
593
+ listBox.key(['enter'], () => {
594
+ void openFolder();
595
+ });
596
+ listBox.key(['o'], () => {
597
+ void openInFinder();
598
+ });
599
+ listBox.key(['b'], () => {
600
+ goBack();
601
+ });
602
+ listBox.key(['q', 'escape', 'C-c'], () => {
603
+ screen.destroy();
604
+ process.exit(0);
605
+ });
606
+ // Mouse double-click to open folder
607
+ let lastClickTime = 0;
608
+ listBox.on('click', () => {
609
+ const selectedItem = items[selectedIndex];
610
+ if (selectedItem?.isDirectory) {
611
+ const now = Date.now();
612
+ if (now - lastClickTime < 300) {
613
+ void openFolder();
614
+ }
615
+ lastClickTime = now;
616
+ }
617
+ });
618
+ // Focus on list initially
619
+ listBox.focus();
620
+ // Set initial selection
621
+ if (items.length > 0) {
622
+ listBox.select(0);
623
+ updateInfoBox(0);
624
+ }
625
+ // Render the screen
626
+ screen.render();
333
627
  }
334
- // Sort by size
335
- quickResults.sort((a, b) => b.size - a.size);
336
- const maxQuickSize = quickResults[0]?.size ?? 1;
337
- for (const result of quickResults) {
338
- const bar = generateQuickAnalysisBar(result.size, maxQuickSize, 30);
339
- const sizeStr = formatSize(result.size).padStart(10);
340
- console.log(` ${result.label.padEnd(15)} ${bar} ${chalk.cyan(sizeStr)}`);
341
- }
342
- // Recommendations
343
- console.log();
344
- console.log(chalk.bold('📋 Recommendations:'));
345
- const totalQuickSize = quickResults.reduce((sum, r) => sum + r.size, 0);
346
- if (totalQuickSize > 5 * 1024 * 1024 * 1024) {
347
- // > 5GB
348
- console.log(chalk.yellow(` ⚠️ You have ${formatSize(totalQuickSize)} in common cleanup locations`));
349
- console.log(chalk.dim(' Run "broom clean" to free up space'));
350
- }
351
- else {
352
- console.log(chalk.green(' ✓ Your disk looks reasonably clean'));
353
- }
354
- console.log();
355
- console.log(chalk.dim('Tip: Use "broom analyze --path /path/to/dir" to analyze a specific directory'));
356
- console.log(chalk.dim(' Use "broom analyze --depth 2" to scan deeper'));
628
+ displayDirectory();
629
+ // Background: Continue deeper scan to fill in missing data
630
+ setImmediate(async () => {
631
+ try {
632
+ const fullInfo = await scanDirectory(targetPath);
633
+ if (fullInfo) {
634
+ rootInfo = fullInfo;
635
+ }
636
+ }
637
+ catch {
638
+ // Silently fail - user can still use shallow scan
639
+ }
640
+ });
357
641
  }
358
642
  /**
359
643
  * Create analyze command
@@ -361,11 +645,10 @@ export async function analyzeCommand(options) {
361
645
  export function createAnalyzeCommand() {
362
646
  const cmd = new Command('analyze')
363
647
  .description('Analyze disk space usage')
648
+ .argument('[path]', 'Path to analyze (default: home directory)')
364
649
  .option('-p, --path <path>', 'Path to analyze (default: home directory)')
365
- .option('-d, --depth <number>', 'Scan depth (default: 1)', parseInt)
366
- .option('-l, --limit <number>', 'Max items to show (default: 15)', parseInt)
367
- .action(async (options) => {
368
- await analyzeCommand(options);
650
+ .action(async (positionalPath, options) => {
651
+ await analyzeCommand({ ...options, positionalPath });
369
652
  });
370
653
  return enhanceCommandHelp(cmd);
371
654
  }
@@ -60,14 +60,35 @@ export async function cleanCommand(options) {
60
60
  // Scan phase
61
61
  const scanners = getAllScanners();
62
62
  const progress = createProgress(scanners.length);
63
- console.log(chalk.cyan('Scanning for cleanable files...\n'));
63
+ // Display scan targets
64
+ console.log(chalk.bold('\n📍 Scan Targets:'));
65
+ console.log(chalk.dim('The following locations will be scanned:\n'));
66
+ const scanTargets = [
67
+ { icon: '📁', name: 'User Caches', path: '~/Library/Caches' },
68
+ { icon: '📋', name: 'User Logs', path: '~/Library/Logs' },
69
+ { icon: '🗑', name: 'Trash', path: '~/.Trash' },
70
+ { icon: '🌐', name: 'Browser Caches', path: '~/Library/**/Cache' },
71
+ { icon: '📦', name: 'Development Caches', path: 'Various dev tool caches' },
72
+ { icon: '🛠', name: 'Xcode Caches', path: '~/Library/Developer' },
73
+ { icon: '⬇️', name: 'Old Downloads', path: '~/Downloads' },
74
+ { icon: '🍺', name: 'Homebrew Caches', path: '/Library/Caches/Homebrew' },
75
+ { icon: '🐳', name: 'Docker Artifacts', path: '~/.docker' },
76
+ { icon: '📱', name: 'iOS Backups', path: '~/Library/Caches/com.apple.bird*' },
77
+ { icon: '❄️', name: 'Temporary Files', path: '/var/tmp, /tmp' },
78
+ { icon: '🔗', name: 'Node Modules', path: 'Large node_modules directories' },
79
+ { icon: '🔧', name: 'Old Installers', path: 'Failed/old installer files' },
80
+ ];
81
+ for (const target of scanTargets) {
82
+ console.log(` ${target.icon} ${target.name.padEnd(25)} ${chalk.dim(target.path)}`);
83
+ }
84
+ console.log(chalk.cyan('\nScanning for cleanable files...\n'));
64
85
  debug(`Starting scan with ${scanners.length} scanners`);
65
86
  const summary = await runAllScans({
66
87
  parallel: true,
67
88
  concurrency: 4,
68
89
  onProgress: (completed, total, scanner) => {
69
90
  debug(`Scan progress: ${completed}/${total} - ${scanner.category.name}`);
70
- progress.update(completed, `Scanning ${scanner.category.name}...`);
91
+ progress.update(completed, `Scanning ${scanner.category.name}... (${completed}/${total})`);
71
92
  },
72
93
  });
73
94
  progress.finish('Scan complete');
@@ -8,147 +8,6 @@ import { exec } from 'child_process';
8
8
  import { promisify } from 'util';
9
9
  import { enhanceCommandHelp } from '../utils/help.js';
10
10
  const execAsync = promisify(exec);
11
- // Broom character ASCII art animation frames (like mole's cat)
12
- const BROOM_FRAMES = [
13
- // Frame 0: Idle
14
- `
15
- ╭─────────────────╮
16
- │ ╭───────╮ │
17
- │ │ ◕ ◕ │ │
18
- │ │ ─── │ │
19
- │ ╰───────╯ │
20
- │ ╭───────╮ │
21
- │ /│ BROOM │\\ │
22
- │ / ╰───────╯ \\ │
23
- │ / ||||||| \\ │
24
- │ ||||||| │
25
- ╰─────────────────╯
26
- `,
27
- // Frame 1: Sweeping right
28
- `
29
- ╭─────────────────╮
30
- │ ╭───────╮ │
31
- │ │ ◕ ◕ │ │
32
- │ │ ─── │░░░ │
33
- │ ╰───────╯░░░ │
34
- │ ╭───────╮░░ │
35
- │ /│ BROOM │\\ │
36
- │ / ╰───────╯ \\ │
37
- │ /|||||||\\ │
38
- │ ||||||| │
39
- ╰─────────────────╯
40
- `,
41
- // Frame 2: Sweeping
42
- `
43
- ╭─────────────────╮
44
- │ ╭───────╮ │
45
- │ │ ◕ᴗ◕ │ │
46
- │ ░░ │ ~~~ │ │
47
- │░░░ ╰───────╯ │
48
- │░░ ╭───────╮ │
49
- │ /│ BROOM │\\ │
50
- │ / ╰───────╯ \\ │
51
- │ / ||||||| \\ │
52
- │ ||||||| │
53
- ╰─────────────────╯
54
- `,
55
- // Frame 3: Dust cloud
56
- `
57
- ╭─────────────────╮
58
- │ ╭───────╮ · │
59
- │ · │ ^ ^ │ · │
60
- │ · │ ◡◡ │ · │
61
- │ · ·╰───────╯· · │
62
- │ · ╭───────╮ · │
63
- │ /│ BROOM │\\ │
64
- │ / ╰───────╯ \\ │
65
- │ /|||||||\\ │
66
- │ ||||||| │
67
- ╰─────────────────╯
68
- `,
69
- // Frame 4: Sparkle clean
70
- `
71
- ╭─────────────────╮
72
- │ ╭───────╮ ✨ │
73
- │ ✨ │ ✧ ✧ │ │
74
- │ │ ◡◡◡ │ ✨ │
75
- │ ╰───────╯ │
76
- │ ✨ ╭───────╮ │
77
- │ /│ CLEAN │\\ │
78
- │ / ╰───────╯ \\ ✨│
79
- │ /|||||||\\ │
80
- │ ||||||| │
81
- ╰─────────────────╯
82
- `,
83
- ];
84
- // Simpler frames for better compatibility
85
- const BROOM_SIMPLE_FRAMES = [
86
- // Frame 0: Idle
87
- [
88
- ' ╭─────────╮ ',
89
- ' │ ◕ ◕ │ ',
90
- ' │ ── │ ',
91
- ' ╰────┬───╯ ',
92
- ' ╭────┴───╮ ',
93
- ' │ BROOM │ ',
94
- ' ╰────────╯ ',
95
- ' ▓▓ ',
96
- ' ▓▓▓▓ ',
97
- ' ▓▓▓▓▓▓ ',
98
- ],
99
- // Frame 1: Sweeping right
100
- [
101
- ' ╭─────────╮ ',
102
- ' │ ◕ ◕ │ ░ ',
103
- ' │ ── │░░░',
104
- ' ╰────┬───╯ ░░',
105
- ' ╭────┴───╮ ░ ',
106
- ' │ BROOM │ ',
107
- ' ╰────────╯ ',
108
- ' ▓▓ ',
109
- ' ▓▓▓▓\\ ',
110
- ' ▓▓▓▓▓▓\\ ',
111
- ],
112
- // Frame 2: Sweeping left
113
- [
114
- ' ╭─────────╮ ',
115
- ' ░ │ ◕ᴗ◕ │ ',
116
- '░░░│ ~~ │ ',
117
- '░░ ╰────┬───╯ ',
118
- ' ░ ╭────┴───╮ ',
119
- ' │ BROOM │ ',
120
- ' ╰────────╯ ',
121
- ' ▓▓ ',
122
- ' /▓▓▓▓ ',
123
- ' /▓▓▓▓▓▓ ',
124
- ],
125
- // Frame 3: Dust cloud
126
- [
127
- ' ╭─────────╮ · ',
128
- ' · │ ^ ^ │ · ',
129
- ' ·│ ◡◡ │· ',
130
- ' · ╰────┬───╯ · ',
131
- ' ·╭────┴───╮· ',
132
- ' │ BROOM │ ',
133
- ' ╰────────╯ ',
134
- ' ▓▓ ',
135
- ' ▓▓▓▓ ',
136
- ' ▓▓▓▓▓▓ ',
137
- ],
138
- // Frame 4: Sparkle
139
- [
140
- ' ╭─────────╮ ✨',
141
- ' ✨│ ✧ ✧ │ ',
142
- ' │ ◡◡◡ │ ✨',
143
- ' ╰────┬───╯ ',
144
- ' ✨╭────┴───╮ ',
145
- ' │ CLEAN! │ ✨',
146
- ' ╰────────╯ ',
147
- ' ▓▓ ',
148
- ' ▓▓▓▓ ',
149
- ' ▓▓▓▓▓▓ ',
150
- ],
151
- ];
152
11
  /**
153
12
  * Format bytes to human readable
154
13
  */
@@ -239,43 +98,29 @@ function calculateHealth(cpuUsage, memUsage, diskUsage, batteryPercent) {
239
98
  */
240
99
  export async function statusCommand(options) {
241
100
  const interval = (options.interval ?? 2) * 1000;
242
- const showBroom = options.broom !== false;
243
- let broomFrame = 0;
244
101
  // Create blessed screen
245
102
  const screen = blessed.screen({
246
103
  smartCSR: true,
247
104
  title: 'Broom System Status',
248
105
  fullUnicode: true,
249
106
  });
250
- // Animation box at top
251
- const animBox = blessed.box({
252
- parent: screen,
253
- top: 0,
254
- left: 'center',
255
- width: 25,
256
- height: showBroom ? 12 : 0,
257
- tags: true,
258
- style: { fg: 'cyan' },
259
- });
260
107
  // Header
261
108
  const headerBox = blessed.box({
262
109
  parent: screen,
263
- top: showBroom ? 12 : 0,
110
+ top: 0,
264
111
  left: 0,
265
112
  width: '100%',
266
- height: 3,
113
+ height: 2,
267
114
  tags: true,
268
- border: { type: 'line' },
269
- style: { border: { fg: 'cyan' } },
270
- label: ' {bold}🧹 Broom System Status{/bold} ',
115
+ style: { fg: 'cyan' },
271
116
  });
272
117
  // CPU Box
273
118
  const cpuBox = blessed.box({
274
119
  parent: screen,
275
- top: showBroom ? 15 : 3,
120
+ top: 2,
276
121
  left: 0,
277
- width: '50%-1',
278
- height: 10,
122
+ width: '33%-1',
123
+ height: 8,
279
124
  label: ' {yellow-fg}●{/yellow-fg} CPU ',
280
125
  tags: true,
281
126
  border: { type: 'line' },
@@ -284,10 +129,10 @@ export async function statusCommand(options) {
284
129
  // Memory Box
285
130
  const memBox = blessed.box({
286
131
  parent: screen,
287
- top: showBroom ? 15 : 3,
288
- left: '50%',
289
- width: '50%',
290
- height: 10,
132
+ top: 2,
133
+ left: '33%',
134
+ width: '34%-1',
135
+ height: 8,
291
136
  label: ' {red-fg}▣{/red-fg} Memory ',
292
137
  tags: true,
293
138
  border: { type: 'line' },
@@ -296,10 +141,10 @@ export async function statusCommand(options) {
296
141
  // Disk Box
297
142
  const diskBox = blessed.box({
298
143
  parent: screen,
299
- top: showBroom ? 25 : 13,
300
- left: 0,
301
- width: '50%-1',
302
- height: 6,
144
+ top: 2,
145
+ left: '67%-1',
146
+ width: '33%',
147
+ height: 8,
303
148
  label: ' {blue-fg}▣{/blue-fg} Disk ',
304
149
  tags: true,
305
150
  border: { type: 'line' },
@@ -308,9 +153,9 @@ export async function statusCommand(options) {
308
153
  // Network Box
309
154
  const netBox = blessed.box({
310
155
  parent: screen,
311
- top: showBroom ? 25 : 13,
312
- left: '50%',
313
- width: '50%',
156
+ top: 10,
157
+ left: 0,
158
+ width: '50%-1',
314
159
  height: 6,
315
160
  label: ' {cyan-fg}↕{/cyan-fg} Network ',
316
161
  tags: true,
@@ -320,10 +165,10 @@ export async function statusCommand(options) {
320
165
  // Processes Box
321
166
  const procBox = blessed.box({
322
167
  parent: screen,
323
- top: showBroom ? 31 : 19,
324
- left: 0,
325
- width: '100%',
326
- height: 9,
168
+ top: 10,
169
+ left: '50%',
170
+ width: '50%',
171
+ height: 6,
327
172
  label: ' {magenta-fg}●{/magenta-fg} Top Processes ',
328
173
  tags: true,
329
174
  border: { type: 'line' },
@@ -344,16 +189,6 @@ export async function statusCommand(options) {
344
189
  screen.key(['escape', 'q', 'C-c'], () => {
345
190
  process.exit(0);
346
191
  });
347
- /**
348
- * Update animation
349
- */
350
- function updateAnimation() {
351
- if (!showBroom)
352
- return;
353
- const frame = BROOM_SIMPLE_FRAMES[broomFrame % BROOM_SIMPLE_FRAMES.length];
354
- animBox.setContent('{cyan-fg}' + frame.join('\n') + '{/cyan-fg}');
355
- broomFrame++;
356
- }
357
192
  /**
358
193
  * Update all data
359
194
  */
@@ -383,51 +218,52 @@ export async function statusCommand(options) {
383
218
  // Header content
384
219
  const gpu = graphics.controllers?.[0];
385
220
  const gpuName = gpu?.model ? `(${gpu.model.replace('Apple ', '').split(' ')[0]})` : '';
386
- const headerContent = ` {bold}Health:{/bold} {${health > 70 ? 'green' : health > 40 ? 'yellow' : 'red'}-fg}${health}%{/} | ` +
387
- `{bold}Host:{/bold} ${osInfo.hostname.split('.')[0]} | ` +
388
- `{bold}CPU:{/bold} ${cpuInfo.manufacturer} ${cpuInfo.brand} ${gpuName}`;
221
+ const headerContent = `{bold}🧹 Broom System Status{/bold}\n` +
222
+ `Health: {${health > 70 ? 'green' : health > 40 ? 'yellow' : 'red'}-fg}${health}%{/} | ` +
223
+ `Host: {cyan-fg}${osInfo.hostname.split('.')[0]}{/} | ` +
224
+ `CPU: ${cpuInfo.manufacturer} ${cpuInfo.brand} ${gpuName}`;
389
225
  headerBox.setContent(headerContent);
390
226
  // CPU content
391
227
  const cpuCores = cpuLoad.cpus || [];
392
228
  let cpuContent = '';
393
- const coresToShow = cpuCores.slice(0, 6);
229
+ const coresToShow = cpuCores.slice(0, 4);
394
230
  coresToShow.forEach((core, i) => {
395
- const bar = createColoredBar(core.load, 15);
231
+ const bar = createColoredBar(core.load, 12);
396
232
  cpuContent += ` Core${(i + 1).toString().padStart(2)} ${bar} ${core.load.toFixed(1).padStart(5)}%\n`;
397
233
  });
398
234
  const temp = cpuTemp.main > 0 ? ` @ ${cpuTemp.main.toFixed(0)}°C` : '';
399
- cpuContent += `\n {gray-fg}Load: ${cpuLoad.currentLoad.toFixed(1)}%${temp}{/gray-fg}`;
235
+ cpuContent += `\n{gray-fg}Load: ${cpuLoad.currentLoad.toFixed(1)}%${temp}{/gray-fg}`;
400
236
  cpuBox.setContent(cpuContent);
401
237
  // Memory content
402
238
  const memUsedPercent = (mem.used / mem.total) * 100;
403
239
  const memFreePercent = (mem.free / mem.total) * 100;
404
240
  const swapUsedPercent = mem.swaptotal > 0 ? (mem.swapused / mem.swaptotal) * 100 : 0;
405
241
  let memContent = '';
406
- memContent += ` Used ${createColoredBar(memUsedPercent, 15)} ${memUsedPercent.toFixed(1).padStart(5)}%\n`;
407
- memContent += ` Free ${createColoredBar(100 - memFreePercent, 15)} ${memFreePercent.toFixed(1).padStart(5)}%\n`;
408
- memContent += ` Swap ${createColoredBar(swapUsedPercent, 15)} ${swapUsedPercent.toFixed(1).padStart(5)}%\n`;
409
- memContent += `\n {gray-fg}Total: ${formatBytes(mem.total)} | Avail: ${formatBytes(mem.available)}{/gray-fg}`;
242
+ memContent += ` Used ${createColoredBar(memUsedPercent, 12)} ${memUsedPercent.toFixed(1).padStart(5)}%\n`;
243
+ memContent += ` Free ${createColoredBar(100 - memFreePercent, 12)} ${memFreePercent.toFixed(1).padStart(5)}%\n`;
244
+ memContent += ` Swap ${createColoredBar(swapUsedPercent, 12)} ${swapUsedPercent.toFixed(1).padStart(5)}%\n`;
245
+ memContent += `\n{gray-fg}Total: ${formatBytes(mem.total)}{/gray-fg}`;
410
246
  memBox.setContent(memContent);
411
247
  // Disk content
412
- const diskBar = createColoredBar(diskUsage, 20);
248
+ const diskBar = createColoredBar(diskUsage, 15);
413
249
  let diskContent = '';
414
250
  diskContent += ` Usage ${diskBar} ${diskUsage.toFixed(1).padStart(5)}%\n`;
415
- diskContent += ` {gray-fg}Used: ${formatBytes(mainDisk?.used ?? 0)} / ${formatBytes(mainDisk?.size ?? 0)}{/gray-fg}\n`;
251
+ diskContent += ` {gray-fg}Used: ${formatBytes(mainDisk?.used ?? 0)}{/gray-fg}\n`;
416
252
  diskContent += ` {gray-fg}Free: ${formatBytes((mainDisk?.size ?? 0) - (mainDisk?.used ?? 0))}{/gray-fg}`;
417
253
  diskBox.setContent(diskContent);
418
254
  // Network content
419
255
  const rxSpeed = activeNet?.rx_sec ?? 0;
420
256
  const txSpeed = activeNet?.tx_sec ?? 0;
421
257
  let netContent = '';
422
- netContent += ` {green-fg}↓{/green-fg} Download: ${formatSpeed(rxSpeed).padStart(12)}\n`;
423
- netContent += ` {red-fg}↑{/red-fg} Upload: ${formatSpeed(txSpeed).padStart(12)}\n`;
258
+ netContent += ` {green-fg}↓{/green-fg} ${formatSpeed(rxSpeed).padStart(12)}\n`;
259
+ netContent += ` {red-fg}↑{/red-fg} ${formatSpeed(txSpeed).padStart(12)}\n`;
424
260
  netContent += ` {gray-fg}IP: ${localIP}{/gray-fg}`;
425
261
  netBox.setContent(netContent);
426
262
  // Processes content
427
263
  let procContent = '';
428
- topProcs.slice(0, 6).forEach((proc, i) => {
429
- const bar = createColoredBar(Math.min(proc.cpu, 100), 10);
430
- procContent += ` ${(i + 1).toString().padStart(2)}. ${proc.name.padEnd(20)} ${bar} ${proc.cpu.toFixed(1).padStart(5)}%\n`;
264
+ topProcs.slice(0, 4).forEach((proc, i) => {
265
+ const bar = createColoredBar(Math.min(proc.cpu, 100), 8);
266
+ procContent += ` ${(i + 1).toString().padStart(2)}. ${proc.name.padEnd(18)} ${bar} ${proc.cpu.toFixed(1).padStart(5)}%\n`;
431
267
  });
432
268
  procBox.setContent(procContent);
433
269
  screen.render();
@@ -437,19 +273,12 @@ export async function statusCommand(options) {
437
273
  }
438
274
  }
439
275
  // Initial update
440
- updateAnimation();
441
276
  await update();
442
277
  // Set interval for updates
443
278
  const updateInterval = setInterval(update, interval);
444
- // Animation interval (faster for smoother animation)
445
- const animInterval = setInterval(() => {
446
- updateAnimation();
447
- screen.render();
448
- }, 500);
449
279
  // Cleanup on exit
450
280
  screen.on('destroy', () => {
451
281
  clearInterval(updateInterval);
452
- clearInterval(animInterval);
453
282
  });
454
283
  screen.render();
455
284
  }
@@ -460,7 +289,6 @@ export function createStatusCommand() {
460
289
  const cmd = new Command('status')
461
290
  .description('Real-time system monitoring dashboard')
462
291
  .option('-i, --interval <seconds>', 'Update interval in seconds (default: 2)', parseInt)
463
- .option('--no-broom', 'Disable broom animation')
464
292
  .action(async (options) => {
465
293
  await statusCommand(options);
466
294
  });
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import chalk from 'chalk';
10
10
  import { createCleanCommand, createUninstallCommand, createOptimizeCommand, createAnalyzeCommand, createStatusCommand, createPurgeCommand, createInstallerCommand, createTouchIdCommand, createCompletionCommand, createUpdateCommand, createRemoveCommand, createConfigCommand, createDoctorCommand, createBackupCommand, createRestoreCommand, createDuplicatesCommand, createScheduleCommand, createWatchCommand, createReportsCommand, createHelpCommand, setCommandsList, } from './commands/index.js';
11
11
  import { enableDebug, debug } from './utils/debug.js';
12
12
  import { getGlobalOptionsTable } from './utils/help.js';
13
- const VERSION = '1.0.1';
13
+ const VERSION = '1.0.5';
14
14
  // ASCII art logo
15
15
  const logo = chalk.cyan(`
16
16
  ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗
@@ -49,14 +49,15 @@ ${chalk.bold('Commands:')}
49
49
  reports Manage cleanup reports
50
50
 
51
51
  ${chalk.bold('Examples:')}
52
- ${chalk.dim('$')} broom clean Interactive cleanup
53
- ${chalk.dim('$')} broom clean --dry-run Preview what would be cleaned
54
- ${chalk.dim('$')} broom clean --all Clean all categories
55
- ${chalk.dim('$')} broom uninstall Remove an app completely
56
- ${chalk.dim('$')} broom optimize Run system optimization tasks
57
- ${chalk.dim('$')} broom analyze See what's using disk space
58
- ${chalk.dim('$')} broom status --watch Live system monitoring
59
- ${chalk.dim('$')} broom purge Clean project artifacts
52
+ ${chalk.dim('$')} broom clean Interactive cleanup
53
+ ${chalk.dim('$')} broom clean --dry-run Preview what would be cleaned
54
+ ${chalk.dim('$')} broom clean --all Clean all categories
55
+ ${chalk.dim('$')} broom uninstall Remove an app completely
56
+ ${chalk.dim('$')} broom optimize Run system optimization tasks
57
+ ${chalk.dim('$')} broom analyze See what's using disk space
58
+ ${chalk.dim('$')} broom analyze --path ~/Library Analyze a Library
59
+ ${chalk.dim('$')} broom status --watch Live system monitoring
60
+ ${chalk.dim('$')} broom purge Clean project artifacts
60
61
 
61
62
  ${getGlobalOptionsTable()}
62
63
  `;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tukuyomil032/broom",
3
3
  "private": false,
4
- "version": "1.0.1",
4
+ "version": "1.0.5",
5
5
  "description": "🧹 macOS Disk Cleanup CLI - Clean up caches, logs, trash, browser data, dev artifacts, and more",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -44,6 +44,12 @@
44
44
  "url": "https://github.com/tukuyomil032/broom/issues"
45
45
  },
46
46
  "homepage": "https://github.com/tukuyomil032/broom#readme",
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ },
50
+ "os": [
51
+ "darwin"
52
+ ],
47
53
  "dependencies": {
48
54
  "@inquirer/prompts": "^8.2.0",
49
55
  "@types/handlebars": "^4.1.0",
@@ -61,7 +67,6 @@
61
67
  "yargs": "^18.0.0"
62
68
  },
63
69
  "devDependencies": {
64
- "@eslint/json": "^0.14.0",
65
70
  "@types/blessed": "^0.1.27",
66
71
  "@types/node": "^25.0.10",
67
72
  "@typescript-eslint/eslint-plugin": "^8.53.1",
@@ -82,6 +87,5 @@
82
87
  "eslint --fix",
83
88
  "prettier --write"
84
89
  ]
85
- },
86
- "packageManager": "bun@1.3.6"
90
+ }
87
91
  }