filemayor 2.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.
- package/core/categories.js +235 -0
- package/core/cleaner.js +527 -0
- package/core/config.js +562 -0
- package/core/index.js +79 -0
- package/core/organizer.js +528 -0
- package/core/reporter.js +572 -0
- package/core/scanner.js +436 -0
- package/core/security.js +317 -0
- package/core/sop-parser.js +565 -0
- package/core/watcher.js +478 -0
- package/index.js +536 -0
- package/package.json +55 -0
package/core/reporter.js
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
* FILEMAYOR CORE — REPORTER
|
|
6
|
+
* Multi-format output (table, JSON, CSV, minimal) with colors,
|
|
7
|
+
* summary statistics, and machine-readable formats for piping.
|
|
8
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const { formatBytes } = require('./scanner');
|
|
14
|
+
|
|
15
|
+
// ─── ANSI Color Codes ─────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const COLORS = {
|
|
18
|
+
reset: '\x1b[0m',
|
|
19
|
+
bold: '\x1b[1m',
|
|
20
|
+
dim: '\x1b[2m',
|
|
21
|
+
italic: '\x1b[3m',
|
|
22
|
+
underline: '\x1b[4m',
|
|
23
|
+
|
|
24
|
+
// Foreground
|
|
25
|
+
black: '\x1b[30m',
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
yellow: '\x1b[33m',
|
|
29
|
+
blue: '\x1b[34m',
|
|
30
|
+
magenta: '\x1b[35m',
|
|
31
|
+
cyan: '\x1b[36m',
|
|
32
|
+
white: '\x1b[37m',
|
|
33
|
+
gray: '\x1b[90m',
|
|
34
|
+
|
|
35
|
+
// Bright
|
|
36
|
+
brightRed: '\x1b[91m',
|
|
37
|
+
brightGreen: '\x1b[92m',
|
|
38
|
+
brightYellow: '\x1b[93m',
|
|
39
|
+
brightBlue: '\x1b[94m',
|
|
40
|
+
brightMagenta: '\x1b[95m',
|
|
41
|
+
brightCyan: '\x1b[96m',
|
|
42
|
+
brightWhite: '\x1b[97m',
|
|
43
|
+
|
|
44
|
+
// Background
|
|
45
|
+
bgRed: '\x1b[41m',
|
|
46
|
+
bgGreen: '\x1b[42m',
|
|
47
|
+
bgYellow: '\x1b[43m',
|
|
48
|
+
bgBlue: '\x1b[44m',
|
|
49
|
+
bgMagenta: '\x1b[45m',
|
|
50
|
+
bgCyan: '\x1b[46m',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let _colorsEnabled = true;
|
|
54
|
+
|
|
55
|
+
function c(color, text) {
|
|
56
|
+
if (!_colorsEnabled) return text;
|
|
57
|
+
return `${COLORS[color]}${text}${COLORS.reset}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setColors(enabled) {
|
|
61
|
+
_colorsEnabled = enabled;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Symbols ──────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const SYMBOLS = {
|
|
67
|
+
check: '✓',
|
|
68
|
+
cross: '✗',
|
|
69
|
+
arrow: '→',
|
|
70
|
+
bullet: '•',
|
|
71
|
+
bar: '█',
|
|
72
|
+
halfBar: '▓',
|
|
73
|
+
lightBar: '░',
|
|
74
|
+
folder: '📂',
|
|
75
|
+
file: '📄',
|
|
76
|
+
trash: '🗑️',
|
|
77
|
+
sparkle: '✨',
|
|
78
|
+
warning: '⚠️',
|
|
79
|
+
error: '❌',
|
|
80
|
+
info: 'ℹ️',
|
|
81
|
+
clock: '⏱️',
|
|
82
|
+
shield: '🛡️',
|
|
83
|
+
eye: '👁️',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ─── Table Formatter ──────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Format data as a table with aligned columns
|
|
90
|
+
* @param {Object[]} data - Array of objects
|
|
91
|
+
* @param {Object} options - Column config
|
|
92
|
+
* @returns {string} Formatted table
|
|
93
|
+
*/
|
|
94
|
+
function formatTable(data, options = {}) {
|
|
95
|
+
if (!data || data.length === 0) return c('dim', ' (no data)');
|
|
96
|
+
|
|
97
|
+
const {
|
|
98
|
+
columns = null, // Column definitions [{key, label, width, align, format}]
|
|
99
|
+
maxWidth = null, // Max column width
|
|
100
|
+
indent = 2, // Left indent
|
|
101
|
+
} = options;
|
|
102
|
+
|
|
103
|
+
// Auto-detect columns from data
|
|
104
|
+
const cols = columns || Object.keys(data[0]).map(key => ({
|
|
105
|
+
key,
|
|
106
|
+
label: key.charAt(0).toUpperCase() + key.slice(1),
|
|
107
|
+
align: 'left'
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
// Calculate column widths
|
|
111
|
+
for (const col of cols) {
|
|
112
|
+
if (!col.width) {
|
|
113
|
+
const maxDataLen = Math.max(
|
|
114
|
+
col.label.length,
|
|
115
|
+
...data.map(row => {
|
|
116
|
+
const val = col.format ? col.format(row[col.key], row) : String(row[col.key] ?? '');
|
|
117
|
+
return val.length;
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
col.width = maxWidth ? Math.min(maxDataLen, maxWidth) : maxDataLen;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const lines = [];
|
|
125
|
+
const pad = ' '.repeat(indent);
|
|
126
|
+
|
|
127
|
+
// Header
|
|
128
|
+
const headerLine = cols.map(col => {
|
|
129
|
+
const label = col.label.padEnd(col.width).slice(0, col.width);
|
|
130
|
+
return c('bold', label);
|
|
131
|
+
}).join(c('dim', ' '));
|
|
132
|
+
lines.push(`${pad}${headerLine}`);
|
|
133
|
+
|
|
134
|
+
// Separator
|
|
135
|
+
const sep = cols.map(col => c('dim', '─'.repeat(col.width))).join(c('dim', '──'));
|
|
136
|
+
lines.push(`${pad}${sep}`);
|
|
137
|
+
|
|
138
|
+
// Rows
|
|
139
|
+
for (const row of data) {
|
|
140
|
+
const rowLine = cols.map(col => {
|
|
141
|
+
let val = col.format ? col.format(row[col.key], row) : String(row[col.key] ?? '');
|
|
142
|
+
const rawVal = val.replace(/\x1b\[[0-9;]*m/g, ''); // Strip ANSI for length calc
|
|
143
|
+
|
|
144
|
+
if (col.align === 'right') {
|
|
145
|
+
val = ' '.repeat(Math.max(0, col.width - rawVal.length)) + val;
|
|
146
|
+
} else {
|
|
147
|
+
val = val + ' '.repeat(Math.max(0, col.width - rawVal.length));
|
|
148
|
+
}
|
|
149
|
+
return val;
|
|
150
|
+
}).join(' ');
|
|
151
|
+
lines.push(`${pad}${rowLine}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Progress Bar ─────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Render a progress bar string
|
|
161
|
+
* @param {number} percent - 0-100
|
|
162
|
+
* @param {number} width - Bar width in chars
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
function progressBar(percent, width = 30) {
|
|
166
|
+
const filled = Math.round((percent / 100) * width);
|
|
167
|
+
const empty = width - filled;
|
|
168
|
+
const bar = c('green', SYMBOLS.bar.repeat(filled)) +
|
|
169
|
+
c('dim', SYMBOLS.lightBar.repeat(empty));
|
|
170
|
+
return `${bar} ${c('bold', `${percent}%`)}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Spinner ──────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
class Spinner {
|
|
176
|
+
constructor(message = 'Working...') {
|
|
177
|
+
this.message = message;
|
|
178
|
+
this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
179
|
+
this.frameIndex = 0;
|
|
180
|
+
this.interval = null;
|
|
181
|
+
this.stream = process.stderr;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
start() {
|
|
185
|
+
this.interval = setInterval(() => {
|
|
186
|
+
const frame = c('cyan', this.frames[this.frameIndex]);
|
|
187
|
+
this.stream.write(`\r${frame} ${this.message}`);
|
|
188
|
+
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
189
|
+
}, 80);
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
update(message) {
|
|
194
|
+
this.message = message;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
succeed(message) {
|
|
198
|
+
this.stop();
|
|
199
|
+
this.stream.write(`\r${c('green', SYMBOLS.check)} ${message || this.message}\n`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fail(message) {
|
|
203
|
+
this.stop();
|
|
204
|
+
this.stream.write(`\r${c('red', SYMBOLS.cross)} ${message || this.message}\n`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
stop() {
|
|
208
|
+
if (this.interval) {
|
|
209
|
+
clearInterval(this.interval);
|
|
210
|
+
this.interval = null;
|
|
211
|
+
this.stream.write('\r' + ' '.repeat(this.message.length + 10) + '\r');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── Scan Report ──────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Format a scan result for display
|
|
220
|
+
* @param {Object} scanResult - Result from scan()
|
|
221
|
+
* @param {string} format - Output format
|
|
222
|
+
* @returns {string}
|
|
223
|
+
*/
|
|
224
|
+
function formatScanReport(scanResult, format = 'table') {
|
|
225
|
+
switch (format) {
|
|
226
|
+
case 'json':
|
|
227
|
+
return JSON.stringify(scanResult, null, 2);
|
|
228
|
+
|
|
229
|
+
case 'csv':
|
|
230
|
+
return formatScanCSV(scanResult);
|
|
231
|
+
|
|
232
|
+
case 'minimal':
|
|
233
|
+
return scanResult.files.map(f => f.path).join('\n');
|
|
234
|
+
|
|
235
|
+
case 'table':
|
|
236
|
+
default:
|
|
237
|
+
return formatScanTable(scanResult);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function formatScanTable(result) {
|
|
242
|
+
const lines = [];
|
|
243
|
+
|
|
244
|
+
// Header
|
|
245
|
+
lines.push('');
|
|
246
|
+
lines.push(c('bold', ` ${SYMBOLS.folder} Scan Report: `) + c('cyan', result.root));
|
|
247
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
248
|
+
|
|
249
|
+
// Category summary
|
|
250
|
+
const catData = Object.entries(result.stats.categories)
|
|
251
|
+
.sort((a, b) => b[1] - a[1])
|
|
252
|
+
.map(([cat, count]) => ({
|
|
253
|
+
category: cat.charAt(0).toUpperCase() + cat.slice(1),
|
|
254
|
+
count: String(count),
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
if (catData.length > 0) {
|
|
258
|
+
lines.push('');
|
|
259
|
+
lines.push(c('bold', ' Categories'));
|
|
260
|
+
lines.push(formatTable(catData, {
|
|
261
|
+
columns: [
|
|
262
|
+
{ key: 'category', label: 'Category', width: 20 },
|
|
263
|
+
{ key: 'count', label: 'Files', width: 8, align: 'right' },
|
|
264
|
+
]
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// File list (top 25)
|
|
269
|
+
const fileData = result.files.slice(0, 25).map(f => ({
|
|
270
|
+
name: f.name.length > 40 ? f.name.slice(0, 37) + '...' : f.name,
|
|
271
|
+
category: f.category,
|
|
272
|
+
size: f.sizeHuman,
|
|
273
|
+
}));
|
|
274
|
+
|
|
275
|
+
if (fileData.length > 0) {
|
|
276
|
+
lines.push('');
|
|
277
|
+
lines.push(c('bold', ' Files'));
|
|
278
|
+
lines.push(formatTable(fileData, {
|
|
279
|
+
columns: [
|
|
280
|
+
{ key: 'name', label: 'Name', width: 42 },
|
|
281
|
+
{ key: 'category', label: 'Category', width: 14 },
|
|
282
|
+
{ key: 'size', label: 'Size', width: 10, align: 'right' },
|
|
283
|
+
]
|
|
284
|
+
}));
|
|
285
|
+
|
|
286
|
+
if (result.files.length > 25) {
|
|
287
|
+
lines.push(c('dim', ` ... and ${result.files.length - 25} more files`));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Summary
|
|
292
|
+
lines.push('');
|
|
293
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
294
|
+
lines.push(` ${c('bold', 'Total:')} ${result.stats.filesFound} files, ${formatBytes(result.stats.totalSize)}`);
|
|
295
|
+
lines.push(` ${c('bold', 'Scanned:')} ${result.stats.dirsScanned} directories in ${result.stats.durationHuman}`);
|
|
296
|
+
if (result.stats.errors.length > 0) {
|
|
297
|
+
lines.push(` ${c('yellow', SYMBOLS.warning)} ${result.stats.errors.length} errors`);
|
|
298
|
+
}
|
|
299
|
+
lines.push('');
|
|
300
|
+
|
|
301
|
+
return lines.join('\n');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function formatScanCSV(result) {
|
|
305
|
+
const header = 'name,path,category,size,modified,ext';
|
|
306
|
+
const rows = result.files.map(f =>
|
|
307
|
+
`"${f.name}","${f.path}","${f.category}",${f.size},"${f.modified.toISOString()}","${f.ext}"`
|
|
308
|
+
);
|
|
309
|
+
return [header, ...rows].join('\n');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ─── Organize Report ──────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
function formatOrganizeReport(result, format = 'table') {
|
|
315
|
+
switch (format) {
|
|
316
|
+
case 'json':
|
|
317
|
+
return JSON.stringify(result, null, 2);
|
|
318
|
+
case 'csv':
|
|
319
|
+
return formatOrganizeCSV(result);
|
|
320
|
+
case 'minimal':
|
|
321
|
+
return result.plan.map(p => `${p.source} ${SYMBOLS.arrow} ${p.destination}`).join('\n');
|
|
322
|
+
case 'table':
|
|
323
|
+
default:
|
|
324
|
+
return formatOrganizeTable(result);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function formatOrganizeTable(result) {
|
|
329
|
+
const lines = [];
|
|
330
|
+
const plan = result.plan;
|
|
331
|
+
const isDryRun = result.dryRun;
|
|
332
|
+
|
|
333
|
+
lines.push('');
|
|
334
|
+
if (isDryRun) {
|
|
335
|
+
lines.push(c('yellow', ` ${SYMBOLS.eye} DRY RUN — No files will be moved`));
|
|
336
|
+
} else {
|
|
337
|
+
lines.push(c('green', ` ${SYMBOLS.sparkle} Organization Complete`));
|
|
338
|
+
}
|
|
339
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
340
|
+
|
|
341
|
+
// Category breakdown
|
|
342
|
+
const catSummary = result.planSummary.categories;
|
|
343
|
+
const catData = Object.entries(catSummary)
|
|
344
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
345
|
+
.map(([cat, info]) => ({
|
|
346
|
+
category: cat,
|
|
347
|
+
files: String(info.count),
|
|
348
|
+
size: formatBytes(info.size)
|
|
349
|
+
}));
|
|
350
|
+
|
|
351
|
+
if (catData.length > 0) {
|
|
352
|
+
lines.push('');
|
|
353
|
+
lines.push(c('bold', ' Breakdown'));
|
|
354
|
+
lines.push(formatTable(catData, {
|
|
355
|
+
columns: [
|
|
356
|
+
{ key: 'category', label: 'Category', width: 20 },
|
|
357
|
+
{ key: 'files', label: 'Files', width: 8, align: 'right' },
|
|
358
|
+
{ key: 'size', label: 'Size', width: 10, align: 'right' },
|
|
359
|
+
]
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Move preview (top 15)
|
|
364
|
+
const preview = plan.slice(0, 15).map(p => ({
|
|
365
|
+
from: p.originalName.length > 30 ? p.originalName.slice(0, 27) + '...' : p.originalName,
|
|
366
|
+
to: p.categoryLabel,
|
|
367
|
+
action: p.action === 'move' ? c('green', 'move') :
|
|
368
|
+
p.action === 'skip' ? c('yellow', 'skip') :
|
|
369
|
+
p.action === 'rename' ? c('cyan', 'rename') :
|
|
370
|
+
c('red', p.action)
|
|
371
|
+
}));
|
|
372
|
+
|
|
373
|
+
if (preview.length > 0) {
|
|
374
|
+
lines.push('');
|
|
375
|
+
lines.push(c('bold', ' Operations'));
|
|
376
|
+
lines.push(formatTable(preview, {
|
|
377
|
+
columns: [
|
|
378
|
+
{ key: 'from', label: 'File', width: 32 },
|
|
379
|
+
{ key: 'to', label: 'Destination', width: 16 },
|
|
380
|
+
{ key: 'action', label: 'Action', width: 10 },
|
|
381
|
+
]
|
|
382
|
+
}));
|
|
383
|
+
|
|
384
|
+
if (plan.length > 15) {
|
|
385
|
+
lines.push(c('dim', ` ... and ${plan.length - 15} more operations`));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Summary
|
|
390
|
+
lines.push('');
|
|
391
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
392
|
+
lines.push(` ${c('bold', 'Total:')} ${result.planSummary.totalFiles} files, ${result.planSummary.totalSizeHuman}`);
|
|
393
|
+
|
|
394
|
+
if (!isDryRun && result.execSummary) {
|
|
395
|
+
const exec = result.execSummary;
|
|
396
|
+
lines.push(` ${c('green', SYMBOLS.check)} ${exec.succeeded} succeeded`);
|
|
397
|
+
if (exec.failed > 0) lines.push(` ${c('red', SYMBOLS.cross)} ${exec.failed} failed`);
|
|
398
|
+
if (exec.skipped > 0) lines.push(` ${c('yellow', '⊘')} ${exec.skipped} skipped`);
|
|
399
|
+
} else if (isDryRun) {
|
|
400
|
+
lines.push(c('dim', ` Run without --dry-run to execute these changes`));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
lines.push('');
|
|
404
|
+
return lines.join('\n');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function formatOrganizeCSV(result) {
|
|
408
|
+
const header = 'source,destination,category,original_name,new_name,size,action';
|
|
409
|
+
const rows = result.plan.map(p =>
|
|
410
|
+
`"${p.source}","${p.destination}","${p.category}","${p.originalName}","${p.newName}",${p.size},"${p.action}"`
|
|
411
|
+
);
|
|
412
|
+
return [header, ...rows].join('\n');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ─── Clean Report ─────────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
function formatCleanReport(result, format = 'table') {
|
|
418
|
+
switch (format) {
|
|
419
|
+
case 'json':
|
|
420
|
+
return JSON.stringify(result, null, 2);
|
|
421
|
+
case 'csv':
|
|
422
|
+
return formatCleanCSV(result);
|
|
423
|
+
case 'minimal':
|
|
424
|
+
return result.junk.map(j => `${j.sizeHuman}\t${j.path}`).join('\n');
|
|
425
|
+
case 'table':
|
|
426
|
+
default:
|
|
427
|
+
return formatCleanTable(result);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function formatCleanTable(result) {
|
|
432
|
+
const lines = [];
|
|
433
|
+
const isDryRun = result.dryRun;
|
|
434
|
+
|
|
435
|
+
lines.push('');
|
|
436
|
+
if (isDryRun) {
|
|
437
|
+
lines.push(c('yellow', ` ${SYMBOLS.eye} DRY RUN — No files will be deleted`));
|
|
438
|
+
} else if (result.deleteResult) {
|
|
439
|
+
lines.push(c('green', ` ${SYMBOLS.trash} Cleanup Complete`));
|
|
440
|
+
} else {
|
|
441
|
+
lines.push(c('cyan', ` ${SYMBOLS.trash} Junk Scan Results`));
|
|
442
|
+
}
|
|
443
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
444
|
+
|
|
445
|
+
// Category breakdown
|
|
446
|
+
const byCategory = result.stats.byCategory;
|
|
447
|
+
const catData = Object.entries(byCategory)
|
|
448
|
+
.sort((a, b) => b[1].size - a[1].size)
|
|
449
|
+
.map(([cat, info]) => ({
|
|
450
|
+
category: cat.charAt(0).toUpperCase() + cat.slice(1),
|
|
451
|
+
items: String(info.count),
|
|
452
|
+
size: formatBytes(info.size)
|
|
453
|
+
}));
|
|
454
|
+
|
|
455
|
+
if (catData.length > 0) {
|
|
456
|
+
lines.push('');
|
|
457
|
+
lines.push(c('bold', ' Junk Categories'));
|
|
458
|
+
lines.push(formatTable(catData, {
|
|
459
|
+
columns: [
|
|
460
|
+
{ key: 'category', label: 'Category', width: 20 },
|
|
461
|
+
{ key: 'items', label: 'Items', width: 8, align: 'right' },
|
|
462
|
+
{ key: 'size', label: 'Size', width: 10, align: 'right' },
|
|
463
|
+
]
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Largest items (top 10)
|
|
468
|
+
const sorted = [...result.junk].sort((a, b) => b.size - a.size).slice(0, 10);
|
|
469
|
+
const itemData = sorted.map(j => ({
|
|
470
|
+
name: j.name.length > 35 ? j.name.slice(0, 32) + '...' : j.name,
|
|
471
|
+
type: j.type === 'directory' ? c('blue', 'DIR') : c('dim', 'FILE'),
|
|
472
|
+
size: j.sizeHuman,
|
|
473
|
+
}));
|
|
474
|
+
|
|
475
|
+
if (itemData.length > 0) {
|
|
476
|
+
lines.push('');
|
|
477
|
+
lines.push(c('bold', ' Largest Junk Items'));
|
|
478
|
+
lines.push(formatTable(itemData, {
|
|
479
|
+
columns: [
|
|
480
|
+
{ key: 'name', label: 'Name', width: 37 },
|
|
481
|
+
{ key: 'type', label: 'Type', width: 6 },
|
|
482
|
+
{ key: 'size', label: 'Size', width: 10, align: 'right' },
|
|
483
|
+
]
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Summary
|
|
488
|
+
lines.push('');
|
|
489
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
490
|
+
lines.push(` ${c('bold', 'Total junk:')} ${result.stats.filesFound + result.stats.dirsFound} items, ${result.stats.totalSizeHuman}`);
|
|
491
|
+
|
|
492
|
+
if (result.deleteResult) {
|
|
493
|
+
const dr = result.deleteResult;
|
|
494
|
+
lines.push(` ${c('green', SYMBOLS.check)} Deleted ${dr.deleted} items, freed ${dr.freedHuman}`);
|
|
495
|
+
if (dr.errors.length > 0) {
|
|
496
|
+
lines.push(` ${c('red', SYMBOLS.cross)} ${dr.errors.length} errors`);
|
|
497
|
+
}
|
|
498
|
+
} else if (isDryRun) {
|
|
499
|
+
lines.push(c('dim', ` Run without --dry-run to clean these files`));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
lines.push('');
|
|
503
|
+
return lines.join('\n');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function formatCleanCSV(result) {
|
|
507
|
+
const header = 'name,path,category,type,size';
|
|
508
|
+
const rows = result.junk.map(j =>
|
|
509
|
+
`"${j.name}","${j.path}","${j.category}","${j.type}",${j.size}`
|
|
510
|
+
);
|
|
511
|
+
return [header, ...rows].join('\n');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ─── Generic Utilities ────────────────────────────────────────────
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Print a header banner
|
|
518
|
+
*/
|
|
519
|
+
function banner() {
|
|
520
|
+
return [
|
|
521
|
+
'',
|
|
522
|
+
c('bold', ` ${SYMBOLS.sparkle} FileMayor`) + c('dim', ' — Your Digital Life Organizer'),
|
|
523
|
+
c('dim', ` ${'─'.repeat(50)}`),
|
|
524
|
+
''
|
|
525
|
+
].join('\n');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Print a success message
|
|
530
|
+
*/
|
|
531
|
+
function success(msg) {
|
|
532
|
+
return `${c('green', SYMBOLS.check)} ${msg}`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Print an error message
|
|
537
|
+
*/
|
|
538
|
+
function error(msg) {
|
|
539
|
+
return `${c('red', SYMBOLS.cross)} ${msg}`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Print a warning message
|
|
544
|
+
*/
|
|
545
|
+
function warn(msg) {
|
|
546
|
+
return `${c('yellow', SYMBOLS.warning)} ${msg}`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Print an info message
|
|
551
|
+
*/
|
|
552
|
+
function info(msg) {
|
|
553
|
+
return `${c('cyan', SYMBOLS.info)} ${msg}`;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
module.exports = {
|
|
557
|
+
COLORS,
|
|
558
|
+
SYMBOLS,
|
|
559
|
+
c,
|
|
560
|
+
setColors,
|
|
561
|
+
formatTable,
|
|
562
|
+
progressBar,
|
|
563
|
+
Spinner,
|
|
564
|
+
formatScanReport,
|
|
565
|
+
formatOrganizeReport,
|
|
566
|
+
formatCleanReport,
|
|
567
|
+
banner,
|
|
568
|
+
success,
|
|
569
|
+
error,
|
|
570
|
+
warn,
|
|
571
|
+
info,
|
|
572
|
+
};
|