filemayor 2.1.0 → 4.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/LICENSE +90 -90
- package/README.md +96 -96
- package/core/ai/planner.js +42 -0
- package/core/ai/sentry.js +92 -0
- package/core/ai/strategist.js +186 -0
- package/core/ai/validator.js +116 -0
- package/core/analyzer.js +163 -152
- package/core/categories.js +235 -235
- package/core/cleaner.js +527 -527
- package/core/config.js +562 -562
- package/core/emergency-halt.js +104 -0
- package/core/engine/apply-engine.js +69 -0
- package/core/engine/cure-engine.js +70 -0
- package/core/engine/dedupe-engine.js +77 -0
- package/core/engine/explain-engine.js +114 -0
- package/core/engine/preview-engine.js +49 -0
- package/core/fs-abstraction.js +271 -0
- package/core/guardrail.js +115 -0
- package/core/index.js +135 -79
- package/core/intent-interpreter.js +301 -0
- package/core/jailer.js +151 -0
- package/core/license.js +403 -337
- package/core/metadata-store.js +104 -0
- package/core/organizer.js +414 -528
- package/core/reporter.js +783 -653
- package/core/scanner.js +466 -436
- package/core/security.js +370 -317
- package/core/sop-parser.js +564 -564
- package/core/telemetry.js +74 -0
- package/core/vault.js +179 -0
- package/core/watcher.js +478 -478
- package/index.js +895 -685
- package/package.json +3 -3
package/core/reporter.js
CHANGED
|
@@ -1,653 +1,783 @@
|
|
|
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
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.
|
|
179
|
-
this.
|
|
180
|
-
this.
|
|
181
|
-
this.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
this.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
this.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
this.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
this.interval
|
|
211
|
-
this.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
*
|
|
221
|
-
* @param {
|
|
222
|
-
* @
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
lines.push(
|
|
247
|
-
lines.push(c('
|
|
248
|
-
lines.push(c('dim', ` ${
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
lines.push(
|
|
261
|
-
lines.push(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
{ key: '
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
lines.push(
|
|
279
|
-
lines.push(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
{ key: '
|
|
283
|
-
{ key: '
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
lines.push(
|
|
295
|
-
lines.push(
|
|
296
|
-
lines.push(` ${c('bold', '
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
);
|
|
414
|
-
return
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
lines.push(
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
);
|
|
514
|
-
return
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
lines
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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 clamped = Math.max(0, Math.min(100, percent || 0));
|
|
167
|
+
const filled = Math.round((clamped / 100) * width);
|
|
168
|
+
const empty = width - filled;
|
|
169
|
+
const bar = c('green', SYMBOLS.bar.repeat(filled)) +
|
|
170
|
+
c('dim', SYMBOLS.lightBar.repeat(empty));
|
|
171
|
+
return `${bar} ${c('bold', `${clamped}%`)}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Spinner ──────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
class Spinner {
|
|
177
|
+
constructor(message = 'Working...') {
|
|
178
|
+
this.message = message;
|
|
179
|
+
this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
180
|
+
this.frameIndex = 0;
|
|
181
|
+
this.interval = null;
|
|
182
|
+
this.stream = process.stderr;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
start() {
|
|
186
|
+
this.interval = setInterval(() => {
|
|
187
|
+
const frame = c('cyan', this.frames[this.frameIndex]);
|
|
188
|
+
this.stream.write(`\r${frame} ${this.message}`);
|
|
189
|
+
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
190
|
+
}, 80);
|
|
191
|
+
return this;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
update(message) {
|
|
195
|
+
this.message = message;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
succeed(message) {
|
|
199
|
+
this.stop();
|
|
200
|
+
this.stream.write(`\r${c('green', SYMBOLS.check)} ${message || this.message}\n`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fail(message) {
|
|
204
|
+
this.stop();
|
|
205
|
+
this.stream.write(`\r${c('red', SYMBOLS.cross)} ${message || this.message}\n`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
stop() {
|
|
209
|
+
if (this.interval) {
|
|
210
|
+
clearInterval(this.interval);
|
|
211
|
+
this.interval = null;
|
|
212
|
+
this.stream.write('\r' + ' '.repeat(this.message.length + 10) + '\r');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Scan Report ──────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Format a scan result for display
|
|
221
|
+
* @param {Object} scanResult - Result from scan()
|
|
222
|
+
* @param {string} format - Output format
|
|
223
|
+
* @returns {string}
|
|
224
|
+
*/
|
|
225
|
+
function formatScanReport(scanResult, format = 'table') {
|
|
226
|
+
switch (format) {
|
|
227
|
+
case 'json':
|
|
228
|
+
return JSON.stringify(scanResult, null, 2);
|
|
229
|
+
|
|
230
|
+
case 'csv':
|
|
231
|
+
return formatScanCSV(scanResult);
|
|
232
|
+
|
|
233
|
+
case 'minimal':
|
|
234
|
+
return scanResult.files.map(f => f.path).join('\n');
|
|
235
|
+
|
|
236
|
+
case 'table':
|
|
237
|
+
default:
|
|
238
|
+
return formatScanTable(scanResult);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatScanTable(result) {
|
|
243
|
+
const lines = [];
|
|
244
|
+
|
|
245
|
+
// Header
|
|
246
|
+
lines.push('');
|
|
247
|
+
lines.push(c('bold', ` ${SYMBOLS.folder} Scan Report: `) + c('cyan', result.root));
|
|
248
|
+
lines.push(c('dim', ` ${SYMBOLS.shield} Integrity Check: `) + c('green', 'PASSED'));
|
|
249
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
250
|
+
|
|
251
|
+
// Category summary
|
|
252
|
+
const catData = Object.entries(result.stats.categories)
|
|
253
|
+
.sort((a, b) => b[1] - a[1])
|
|
254
|
+
.map(([cat, count]) => ({
|
|
255
|
+
category: cat.charAt(0).toUpperCase() + cat.slice(1),
|
|
256
|
+
count: String(count),
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
if (catData.length > 0) {
|
|
260
|
+
lines.push('');
|
|
261
|
+
lines.push(c('bold', ' Categories'));
|
|
262
|
+
lines.push(formatTable(catData, {
|
|
263
|
+
columns: [
|
|
264
|
+
{ key: 'category', label: 'Category', width: 20 },
|
|
265
|
+
{ key: 'count', label: 'Files', width: 8, align: 'right' },
|
|
266
|
+
]
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// File list (top 25)
|
|
271
|
+
const fileData = result.files.slice(0, 25).map(f => ({
|
|
272
|
+
name: f.name.length > 40 ? f.name.slice(0, 37) + '...' : f.name,
|
|
273
|
+
category: f.category,
|
|
274
|
+
size: f.sizeHuman,
|
|
275
|
+
}));
|
|
276
|
+
|
|
277
|
+
if (fileData.length > 0) {
|
|
278
|
+
lines.push('');
|
|
279
|
+
lines.push(c('bold', ' Files'));
|
|
280
|
+
lines.push(formatTable(fileData, {
|
|
281
|
+
columns: [
|
|
282
|
+
{ key: 'name', label: 'Name', width: 42 },
|
|
283
|
+
{ key: 'category', label: 'Category', width: 14 },
|
|
284
|
+
{ key: 'size', label: 'Size', width: 10, align: 'right' },
|
|
285
|
+
]
|
|
286
|
+
}));
|
|
287
|
+
|
|
288
|
+
if (result.files.length > 25) {
|
|
289
|
+
lines.push(c('dim', ` ... and ${result.files.length - 25} more files`));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Summary
|
|
294
|
+
lines.push('');
|
|
295
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
296
|
+
lines.push(` ${c('bold', 'Total:')} ${result.stats.filesFound} files, ${formatBytes(result.stats.totalSize)}`);
|
|
297
|
+
lines.push(` ${c('bold', 'Scanned:')} ${result.stats.dirsScanned} directories in ${result.stats.durationHuman}`);
|
|
298
|
+
if (result.stats.errors.length > 0) {
|
|
299
|
+
lines.push(` ${c('yellow', SYMBOLS.warning)} ${result.stats.errors.length} errors`);
|
|
300
|
+
}
|
|
301
|
+
lines.push('');
|
|
302
|
+
|
|
303
|
+
return lines.join('\n');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function formatScanCSV(result) {
|
|
307
|
+
const header = 'name,path,category,size,modified,ext';
|
|
308
|
+
const rows = result.files.map(f =>
|
|
309
|
+
`"${f.name}","${f.path}","${f.category}",${f.size},"${f.modified.toISOString()}","${f.ext}"`
|
|
310
|
+
);
|
|
311
|
+
return [header, ...rows].join('\n');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─── Organize Report ──────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
function formatOrganizeReport(result, format = 'table') {
|
|
317
|
+
// Defensive: handle empty/incomplete results
|
|
318
|
+
if (!result || (!result.plan && !result.planSummary)) {
|
|
319
|
+
return `\n ${c('green', SYMBOLS.check)} Nothing to organize — folder is already clean.\n`;
|
|
320
|
+
}
|
|
321
|
+
if (!result.plan) result.plan = [];
|
|
322
|
+
if (!result.planSummary) result.planSummary = { categories: {}, totalFiles: 0, totalSizeHuman: '0 B' };
|
|
323
|
+
|
|
324
|
+
switch (format) {
|
|
325
|
+
case 'json':
|
|
326
|
+
return JSON.stringify(result, null, 2);
|
|
327
|
+
case 'csv':
|
|
328
|
+
return formatOrganizeCSV(result);
|
|
329
|
+
case 'minimal':
|
|
330
|
+
return result.plan.map(p => `${p.source} ${SYMBOLS.arrow} ${p.destination}`).join('\n');
|
|
331
|
+
case 'table':
|
|
332
|
+
default:
|
|
333
|
+
return formatOrganizeTable(result);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function formatOrganizeTable(result) {
|
|
338
|
+
const lines = [];
|
|
339
|
+
const plan = result.plan;
|
|
340
|
+
const isDryRun = result.dryRun;
|
|
341
|
+
|
|
342
|
+
lines.push('');
|
|
343
|
+
if (isDryRun) {
|
|
344
|
+
lines.push(c('yellow', ` ${SYMBOLS.eye} DRY RUN — No files will be moved`));
|
|
345
|
+
} else {
|
|
346
|
+
lines.push(c('green', ` ${SYMBOLS.sparkle} Organization Complete`));
|
|
347
|
+
}
|
|
348
|
+
lines.push(c('dim', ` ${SYMBOLS.shield} Integrity Check: `) + c('green', 'PASSED'));
|
|
349
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
350
|
+
|
|
351
|
+
// Category breakdown
|
|
352
|
+
const catSummary = result.planSummary.categories;
|
|
353
|
+
const catData = Object.entries(catSummary)
|
|
354
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
355
|
+
.map(([cat, info]) => ({
|
|
356
|
+
category: cat,
|
|
357
|
+
files: String(info.count),
|
|
358
|
+
size: formatBytes(info.size)
|
|
359
|
+
}));
|
|
360
|
+
|
|
361
|
+
if (catData.length > 0) {
|
|
362
|
+
lines.push('');
|
|
363
|
+
lines.push(c('bold', ' Breakdown'));
|
|
364
|
+
lines.push(formatTable(catData, {
|
|
365
|
+
columns: [
|
|
366
|
+
{ key: 'category', label: 'Category', width: 20 },
|
|
367
|
+
{ key: 'files', label: 'Files', width: 8, align: 'right' },
|
|
368
|
+
{ key: 'size', label: 'Size', width: 10, align: 'right' },
|
|
369
|
+
]
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Move preview (top 15)
|
|
374
|
+
const preview = plan.slice(0, 15).map(p => ({
|
|
375
|
+
from: p.originalName.length > 30 ? p.originalName.slice(0, 27) + '...' : p.originalName,
|
|
376
|
+
to: p.categoryLabel,
|
|
377
|
+
action: p.action === 'move' ? c('green', 'move') :
|
|
378
|
+
p.action === 'skip' ? c('yellow', 'skip') :
|
|
379
|
+
p.action === 'rename' ? c('cyan', 'rename') :
|
|
380
|
+
c('red', p.action)
|
|
381
|
+
}));
|
|
382
|
+
|
|
383
|
+
if (preview.length > 0) {
|
|
384
|
+
lines.push('');
|
|
385
|
+
lines.push(c('bold', ' Operations'));
|
|
386
|
+
lines.push(formatTable(preview, {
|
|
387
|
+
columns: [
|
|
388
|
+
{ key: 'from', label: 'File', width: 32 },
|
|
389
|
+
{ key: 'to', label: 'Destination', width: 16 },
|
|
390
|
+
{ key: 'action', label: 'Action', width: 10 },
|
|
391
|
+
]
|
|
392
|
+
}));
|
|
393
|
+
|
|
394
|
+
if (plan.length > 15) {
|
|
395
|
+
lines.push(c('dim', ` ... and ${plan.length - 15} more operations`));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Summary
|
|
400
|
+
lines.push('');
|
|
401
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
402
|
+
lines.push(` ${c('bold', 'Total:')} ${result.planSummary.totalFiles} files, ${result.planSummary.totalSizeHuman}`);
|
|
403
|
+
|
|
404
|
+
if (!isDryRun && result.execSummary) {
|
|
405
|
+
const exec = result.execSummary;
|
|
406
|
+
lines.push(` ${c('green', SYMBOLS.check)} ${exec.succeeded} succeeded`);
|
|
407
|
+
if (exec.failed > 0) lines.push(` ${c('red', SYMBOLS.cross)} ${exec.failed} failed`);
|
|
408
|
+
if (exec.skipped > 0) lines.push(` ${c('yellow', '⊘')} ${exec.skipped} skipped`);
|
|
409
|
+
} else if (isDryRun) {
|
|
410
|
+
lines.push(c('dim', ` Run without --dry-run to execute these changes`));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
lines.push('');
|
|
414
|
+
return lines.join('\n');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function formatOrganizeCSV(result) {
|
|
418
|
+
const header = 'source,destination,category,original_name,new_name,size,action';
|
|
419
|
+
const rows = result.plan.map(p =>
|
|
420
|
+
`"${p.source}","${p.destination}","${p.category}","${p.originalName}","${p.newName}",${p.size},"${p.action}"`
|
|
421
|
+
);
|
|
422
|
+
return [header, ...rows].join('\n');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ─── Clean Report ─────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
function formatCleanReport(result, format = 'table') {
|
|
428
|
+
switch (format) {
|
|
429
|
+
case 'json':
|
|
430
|
+
return JSON.stringify(result, null, 2);
|
|
431
|
+
case 'csv':
|
|
432
|
+
return formatCleanCSV(result);
|
|
433
|
+
case 'minimal':
|
|
434
|
+
return result.junk.map(j => `${j.sizeHuman}\t${j.path}`).join('\n');
|
|
435
|
+
case 'table':
|
|
436
|
+
default:
|
|
437
|
+
return formatCleanTable(result);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function formatCleanTable(result) {
|
|
442
|
+
const lines = [];
|
|
443
|
+
const isDryRun = result.dryRun;
|
|
444
|
+
|
|
445
|
+
lines.push('');
|
|
446
|
+
if (isDryRun) {
|
|
447
|
+
lines.push(c('yellow', ` ${SYMBOLS.eye} DRY RUN — No files will be deleted`));
|
|
448
|
+
} else if (result.deleteResult) {
|
|
449
|
+
lines.push(c('green', ` ${SYMBOLS.trash} Cleanup Complete`));
|
|
450
|
+
} else {
|
|
451
|
+
lines.push(c('cyan', ` ${SYMBOLS.trash} Junk Scan Results`));
|
|
452
|
+
}
|
|
453
|
+
lines.push(c('dim', ` ${SYMBOLS.shield} Integrity Check: `) + c('green', 'PASSED'));
|
|
454
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
455
|
+
|
|
456
|
+
// Category breakdown
|
|
457
|
+
const byCategory = result.stats.byCategory;
|
|
458
|
+
const catData = Object.entries(byCategory)
|
|
459
|
+
.sort((a, b) => b[1].size - a[1].size)
|
|
460
|
+
.map(([cat, info]) => ({
|
|
461
|
+
category: cat.charAt(0).toUpperCase() + cat.slice(1),
|
|
462
|
+
items: String(info.count),
|
|
463
|
+
size: formatBytes(info.size)
|
|
464
|
+
}));
|
|
465
|
+
|
|
466
|
+
if (catData.length > 0) {
|
|
467
|
+
lines.push('');
|
|
468
|
+
lines.push(c('bold', ' Junk Categories'));
|
|
469
|
+
lines.push(formatTable(catData, {
|
|
470
|
+
columns: [
|
|
471
|
+
{ key: 'category', label: 'Category', width: 20 },
|
|
472
|
+
{ key: 'items', label: 'Items', width: 8, align: 'right' },
|
|
473
|
+
{ key: 'size', label: 'Size', width: 10, align: 'right' },
|
|
474
|
+
]
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Largest items (top 10)
|
|
479
|
+
const sorted = [...result.junk].sort((a, b) => b.size - a.size).slice(0, 10);
|
|
480
|
+
const itemData = sorted.map(j => ({
|
|
481
|
+
name: j.name.length > 35 ? j.name.slice(0, 32) + '...' : j.name,
|
|
482
|
+
type: j.type === 'directory' ? c('blue', 'DIR') : c('dim', 'FILE'),
|
|
483
|
+
size: j.sizeHuman,
|
|
484
|
+
}));
|
|
485
|
+
|
|
486
|
+
if (itemData.length > 0) {
|
|
487
|
+
lines.push('');
|
|
488
|
+
lines.push(c('bold', ' Largest Junk Items'));
|
|
489
|
+
lines.push(formatTable(itemData, {
|
|
490
|
+
columns: [
|
|
491
|
+
{ key: 'name', label: 'Name', width: 37 },
|
|
492
|
+
{ key: 'type', label: 'Type', width: 6 },
|
|
493
|
+
{ key: 'size', label: 'Size', width: 10, align: 'right' },
|
|
494
|
+
]
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Summary
|
|
499
|
+
lines.push('');
|
|
500
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
501
|
+
lines.push(` ${c('bold', 'Total junk:')} ${result.stats.filesFound + result.stats.dirsFound} items, ${result.stats.totalSizeHuman}`);
|
|
502
|
+
|
|
503
|
+
if (result.deleteResult) {
|
|
504
|
+
const dr = result.deleteResult;
|
|
505
|
+
lines.push(` ${c('green', SYMBOLS.check)} Deleted ${dr.deleted} items, freed ${dr.freedHuman}`);
|
|
506
|
+
if (dr.errors.length > 0) {
|
|
507
|
+
lines.push(` ${c('red', SYMBOLS.cross)} ${dr.errors.length} errors`);
|
|
508
|
+
}
|
|
509
|
+
} else if (isDryRun) {
|
|
510
|
+
lines.push(c('dim', ` Run without --dry-run to clean these files`));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
lines.push('');
|
|
514
|
+
return lines.join('\n');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function formatCleanCSV(result) {
|
|
518
|
+
const header = 'name,path,category,type,size';
|
|
519
|
+
const rows = result.junk.map(j =>
|
|
520
|
+
`"${j.name}","${j.path}","${j.category}","${j.type}",${j.size}`
|
|
521
|
+
);
|
|
522
|
+
return [header, ...rows].join('\n');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ─── Generic Utilities ────────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Print a header banner
|
|
529
|
+
*/
|
|
530
|
+
function banner() {
|
|
531
|
+
return [
|
|
532
|
+
'',
|
|
533
|
+
c('bold', ` ${SYMBOLS.sparkle} FileMayor`) + c('dim', ' — Your Digital Life Organizer'),
|
|
534
|
+
c('dim', ' by Lehlohonolo Goodwill Nchefu (Chevza)'),
|
|
535
|
+
c('dim', ` ${'─'.repeat(50)}`),
|
|
536
|
+
''
|
|
537
|
+
].join('\n');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Print a success message
|
|
542
|
+
*/
|
|
543
|
+
function success(msg) {
|
|
544
|
+
return `${c('green', SYMBOLS.check)} ${msg}`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Print an error message
|
|
549
|
+
*/
|
|
550
|
+
function error(msg) {
|
|
551
|
+
return `${c('red', SYMBOLS.cross)} ${msg}`;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Print a warning message
|
|
556
|
+
*/
|
|
557
|
+
function warn(msg) {
|
|
558
|
+
return `${c('yellow', SYMBOLS.warning)} ${msg}`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Print an info message
|
|
563
|
+
*/
|
|
564
|
+
function info(msg) {
|
|
565
|
+
return `${c('cyan', SYMBOLS.info)} ${msg}`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ─── Analyze Report ────────────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
function formatAnalyzeReport(result, format = 'table') {
|
|
571
|
+
if (format === 'json') return JSON.stringify(result, null, 2);
|
|
572
|
+
|
|
573
|
+
const lines = [];
|
|
574
|
+
lines.push('');
|
|
575
|
+
lines.push(c('bold', ` ${SYMBOLS.eye} Deep Intelligence Report: `) + c('cyan', result.root));
|
|
576
|
+
lines.push(c('dim', ` ${'─'.repeat(60)}`));
|
|
577
|
+
|
|
578
|
+
// Summary Insights
|
|
579
|
+
lines.push('');
|
|
580
|
+
lines.push(` ${c('bold', 'Summary:')} ${result.summary.totalFiles} files detected. Total size: ${result.summary.totalSizeHuman}`);
|
|
581
|
+
lines.push(` ${c('green', '⚡ Insight:')} You can reclaim ${c('bold', result.summary.potentialSavingsHuman)} today.`);
|
|
582
|
+
|
|
583
|
+
// Duplicates Section
|
|
584
|
+
if (result.duplicates.sets > 0) {
|
|
585
|
+
lines.push('');
|
|
586
|
+
lines.push(c('bold', ' Duplicate Bloat'));
|
|
587
|
+
const dupeData = result.duplicates.details.map(d => ({
|
|
588
|
+
name: d.files[0].name.length > 35 ? d.files[0].name.slice(0, 32) + '...' : d.files[0].name,
|
|
589
|
+
count: String(d.files.length),
|
|
590
|
+
wasted: d.wastedSpaceHuman
|
|
591
|
+
}));
|
|
592
|
+
lines.push(formatTable(dupeData, {
|
|
593
|
+
columns: [
|
|
594
|
+
{ key: 'name', label: 'File Name', width: 37 },
|
|
595
|
+
{ key: 'count', label: 'Copies', width: 8, align: 'right' },
|
|
596
|
+
{ key: 'wasted', label: 'Wasted', width: 10, align: 'right' }
|
|
597
|
+
]
|
|
598
|
+
}));
|
|
599
|
+
lines.push(` ${c('dim', ` Found ${result.duplicates.sets} duplicate sets causing ${result.duplicates.totalWastedHuman} bloat.`)}`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Largest Folders Section
|
|
603
|
+
if (result.largestDirs.length > 0) {
|
|
604
|
+
lines.push('');
|
|
605
|
+
lines.push(c('bold', ' Top Space Consumers (Bloat Map)'));
|
|
606
|
+
const dirData = result.largestDirs.map(d => ({
|
|
607
|
+
name: d.name.length > 37 ? d.name.slice(0, 34) + '...' : d.name,
|
|
608
|
+
files: String(d.count),
|
|
609
|
+
size: d.sizeHuman
|
|
610
|
+
}));
|
|
611
|
+
lines.push(formatTable(dirData, {
|
|
612
|
+
columns: [
|
|
613
|
+
{ key: 'name', label: 'Directory', width: 40 },
|
|
614
|
+
{ key: 'files', label: 'Files', width: 8, align: 'right' },
|
|
615
|
+
{ key: 'size', label: 'Size', width: 10, align: 'right' }
|
|
616
|
+
]
|
|
617
|
+
}));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Junk Section
|
|
621
|
+
if (result.junk.count > 0) {
|
|
622
|
+
lines.push('');
|
|
623
|
+
lines.push(c('bold', ' Recoverable Junk'));
|
|
624
|
+
const junkData = Object.entries(result.junk.categories).map(([cat, info]) => ({
|
|
625
|
+
category: cat.charAt(0).toUpperCase() + cat.slice(1),
|
|
626
|
+
size: formatBytes(info.size)
|
|
627
|
+
}));
|
|
628
|
+
lines.push(formatTable(junkData, {
|
|
629
|
+
columns: [
|
|
630
|
+
{ key: 'category', label: 'Category', width: 40 },
|
|
631
|
+
{ key: 'size', label: 'Savings', width: 18, align: 'right' }
|
|
632
|
+
]
|
|
633
|
+
}));
|
|
634
|
+
lines.push(` ${c('dim', ` Total Junk: ${result.junk.count} items / ${result.junk.totalSizeHuman}`)}`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
lines.push('');
|
|
638
|
+
lines.push(c('yellow', ' Run \'filemayor organize\' or \'filemayor clean\' to restore order and reclaim space.'));
|
|
639
|
+
lines.push('');
|
|
640
|
+
|
|
641
|
+
return lines.join('\n');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ─── Diagnostic (Explain) Report ───────────────────────────────────
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Format an explain/diagnostic result
|
|
648
|
+
* @param {Object} result - Result from ExplainEngine
|
|
649
|
+
* @returns {string}
|
|
650
|
+
*/
|
|
651
|
+
function formatExplainReport(result) {
|
|
652
|
+
const lines = [];
|
|
653
|
+
const health = result.health;
|
|
654
|
+
|
|
655
|
+
lines.push('');
|
|
656
|
+
lines.push(c('bold', ` ${health.score > 70 ? SYMBOLS.sparkle : SYMBOLS.warning} ${result.title}`));
|
|
657
|
+
lines.push(c('dim', ` ${'─'.repeat(40)}`));
|
|
658
|
+
|
|
659
|
+
lines.push(` ${c('bold', 'Target:')} ${c('cyan', result.path)}`);
|
|
660
|
+
|
|
661
|
+
// Health Gauge
|
|
662
|
+
const gauge = progressBar(health.score, 20);
|
|
663
|
+
lines.push(` ${c('bold', 'Health:')} ${c('bold', health.label)} (${gauge})`);
|
|
664
|
+
|
|
665
|
+
lines.push('');
|
|
666
|
+
lines.push(c('bold', ' Issues Detected:'));
|
|
667
|
+
if (result.insights.length === 0) {
|
|
668
|
+
lines.push(` ${c('green', SYMBOLS.check)} No major issues identified. Clean as a whistle.`);
|
|
669
|
+
} else {
|
|
670
|
+
for (const insight of result.insights) {
|
|
671
|
+
lines.push(` ${c('yellow', SYMBOLS.bullet)} ${insight}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
lines.push('');
|
|
676
|
+
lines.push(c('bold', ' Suggested Treatment:'));
|
|
677
|
+
lines.push(` ${c('dim', 'Run')} ${c('yellow', 'filemayor cure')} ${c('dim', 'to generate an AI-powered curative plan.')}`);
|
|
678
|
+
lines.push('');
|
|
679
|
+
|
|
680
|
+
return lines.join('\n');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Format a curative plan for terminal display
|
|
685
|
+
*/
|
|
686
|
+
function formatCureReport(plan) {
|
|
687
|
+
const lines = [];
|
|
688
|
+
lines.push('');
|
|
689
|
+
lines.push(c('bold', ` ${SYMBOLS.shield} CURATIVE PLAN GENERATED`));
|
|
690
|
+
lines.push(c('dim', ` ${'─'.repeat(40)}`));
|
|
691
|
+
|
|
692
|
+
lines.push(` ${c('bold', 'Strategy:')} ${plan.narrative}`);
|
|
693
|
+
lines.push(` ${c('bold', 'Confidence:')} ${plan.confidence}%`);
|
|
694
|
+
|
|
695
|
+
lines.push('');
|
|
696
|
+
lines.push(c('bold', ' Proposed Actions:'));
|
|
697
|
+
const preview = plan.plan.slice(0, 10).map(p => ({
|
|
698
|
+
file: p.source.split(/[\\/]/).pop(),
|
|
699
|
+
to: p.destination.split(/[\\/]/).pop(),
|
|
700
|
+
}));
|
|
701
|
+
|
|
702
|
+
lines.push(formatTable(preview, {
|
|
703
|
+
columns: [
|
|
704
|
+
{ key: 'file', label: 'File', width: 25 },
|
|
705
|
+
{ key: 'to', label: 'Move to', width: 25 },
|
|
706
|
+
]
|
|
707
|
+
}));
|
|
708
|
+
|
|
709
|
+
if (plan.plan.length > 10) {
|
|
710
|
+
lines.push(` ${c('dim', `... and ${plan.plan.length - 10} more actions.`)}`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
lines.push('');
|
|
714
|
+
lines.push(c('yellow', ` Run 'filemayor apply' to execute this plan.`));
|
|
715
|
+
lines.push('');
|
|
716
|
+
|
|
717
|
+
return lines.join('\n');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Format a duplicate detection report
|
|
722
|
+
*/
|
|
723
|
+
function formatDedupeReport(result) {
|
|
724
|
+
const lines = [];
|
|
725
|
+
lines.push('');
|
|
726
|
+
lines.push(c('bold', ` ${SYMBOLS.trash} DUPLICATE ANALYSIS: `) + c('cyan', result.path));
|
|
727
|
+
lines.push(c('dim', ` ${'─'.repeat(50)}`));
|
|
728
|
+
|
|
729
|
+
if (result.sets === 0) {
|
|
730
|
+
lines.push(` ${c('green', SYMBOLS.check)} No duplicates detected. Zero bloat.`);
|
|
731
|
+
lines.push('');
|
|
732
|
+
return lines.join('\n');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
lines.push(` ${c('red', 'BLOAT DETECTED:')} ${result.sets} duplicate files found.`);
|
|
736
|
+
lines.push(` ${c('yellow', 'SAVINGS POTENTIAL:')} ${result.totalWastedHuman}`);
|
|
737
|
+
lines.push('');
|
|
738
|
+
|
|
739
|
+
lines.push(c('bold', ' Duplicate Items:'));
|
|
740
|
+
const preview = result.duplicates.slice(0, 10).map(d => ({
|
|
741
|
+
name: d.duplicate.name.length > 35 ? d.duplicate.name.slice(0, 32) + '...' : d.duplicate.name,
|
|
742
|
+
size: d.duplicate.sizeHuman,
|
|
743
|
+
}));
|
|
744
|
+
|
|
745
|
+
lines.push(formatTable(preview, {
|
|
746
|
+
columns: [
|
|
747
|
+
{ key: 'name', label: 'File', width: 37 },
|
|
748
|
+
{ key: 'size', label: 'Wasted', width: 10, align: 'right' }
|
|
749
|
+
]
|
|
750
|
+
}));
|
|
751
|
+
|
|
752
|
+
if (result.duplicates.length > 10) {
|
|
753
|
+
lines.push(` ${c('dim', `... and ${result.duplicates.length - 10} more duplicates.`)}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
lines.push('');
|
|
757
|
+
lines.push(c('yellow', ` Run 'filemayor dedupe' to remove these and reclaim space.`));
|
|
758
|
+
lines.push('');
|
|
759
|
+
|
|
760
|
+
return lines.join('\n');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
module.exports = {
|
|
764
|
+
COLORS,
|
|
765
|
+
SYMBOLS,
|
|
766
|
+
c,
|
|
767
|
+
setColors,
|
|
768
|
+
formatTable,
|
|
769
|
+
progressBar,
|
|
770
|
+
Spinner,
|
|
771
|
+
formatScanReport,
|
|
772
|
+
formatOrganizeReport,
|
|
773
|
+
formatCleanReport,
|
|
774
|
+
formatAnalyzeReport,
|
|
775
|
+
formatExplainReport,
|
|
776
|
+
formatCureReport,
|
|
777
|
+
formatDedupeReport,
|
|
778
|
+
banner,
|
|
779
|
+
success,
|
|
780
|
+
error,
|
|
781
|
+
warn,
|
|
782
|
+
info,
|
|
783
|
+
};
|