@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.
- package/dist/commands/analyze.js +432 -149
- package/dist/commands/clean.js +23 -2
- package/dist/commands/status.js +39 -211
- package/dist/index.js +10 -9
- package/package.json +8 -4
package/dist/commands/analyze.js
CHANGED
|
@@ -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 {
|
|
10
|
-
import {
|
|
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,
|
|
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
|
-
//
|
|
38
|
-
if (currentDepth
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
const
|
|
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:
|
|
78
|
+
isDirectory: false,
|
|
61
79
|
});
|
|
62
80
|
}
|
|
63
|
-
catch {
|
|
64
|
-
// Skip if cannot access
|
|
65
|
-
}
|
|
66
81
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
|
85
|
-
const percentage =
|
|
86
|
-
const
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 +=
|
|
150
|
+
bar += '{white-fg}░{/white-fg}';
|
|
107
151
|
}
|
|
108
152
|
}
|
|
109
|
-
|
|
110
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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,
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
.
|
|
366
|
-
|
|
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
|
}
|
package/dist/commands/clean.js
CHANGED
|
@@ -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
|
-
|
|
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');
|
package/dist/commands/status.js
CHANGED
|
@@ -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:
|
|
110
|
+
top: 0,
|
|
264
111
|
left: 0,
|
|
265
112
|
width: '100%',
|
|
266
|
-
height:
|
|
113
|
+
height: 2,
|
|
267
114
|
tags: true,
|
|
268
|
-
|
|
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:
|
|
120
|
+
top: 2,
|
|
276
121
|
left: 0,
|
|
277
|
-
width: '
|
|
278
|
-
height:
|
|
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:
|
|
288
|
-
left: '
|
|
289
|
-
width: '
|
|
290
|
-
height:
|
|
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:
|
|
300
|
-
left:
|
|
301
|
-
width: '
|
|
302
|
-
height:
|
|
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:
|
|
312
|
-
left:
|
|
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:
|
|
324
|
-
left:
|
|
325
|
-
width: '
|
|
326
|
-
height:
|
|
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 = `
|
|
387
|
-
`
|
|
388
|
-
`
|
|
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,
|
|
229
|
+
const coresToShow = cpuCores.slice(0, 4);
|
|
394
230
|
coresToShow.forEach((core, i) => {
|
|
395
|
-
const bar = createColoredBar(core.load,
|
|
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
|
|
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,
|
|
407
|
-
memContent += ` Free ${createColoredBar(100 - memFreePercent,
|
|
408
|
-
memContent += ` Swap ${createColoredBar(swapUsedPercent,
|
|
409
|
-
memContent += `\n
|
|
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,
|
|
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)}
|
|
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}
|
|
423
|
-
netContent += ` {red-fg}↑{/red-fg}
|
|
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,
|
|
429
|
-
const bar = createColoredBar(Math.min(proc.cpu, 100),
|
|
430
|
-
procContent += ` ${(i + 1).toString().padStart(2)}. ${proc.name.padEnd(
|
|
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.
|
|
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
|
|
53
|
-
${chalk.dim('$')} broom clean --dry-run
|
|
54
|
-
${chalk.dim('$')} broom clean --all
|
|
55
|
-
${chalk.dim('$')} broom uninstall
|
|
56
|
-
${chalk.dim('$')} broom optimize
|
|
57
|
-
${chalk.dim('$')} broom analyze
|
|
58
|
-
${chalk.dim('$')} broom
|
|
59
|
-
${chalk.dim('$')} broom
|
|
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.
|
|
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
|
}
|