bmad-method 5.0.0-beta.1 โ†’ 5.0.0-beta.2

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/CHANGELOG.md CHANGED
@@ -1,26 +1,9 @@
1
- # [5.0.0-beta.1](https://github.com/bmadcode/BMAD-METHOD/compare/v4.37.0-beta.1...v5.0.0-beta.1) (2025-08-16)
2
-
3
-
4
- ### Bug Fixes
5
-
6
- * add permissions and authentication for promotion workflow ([7f016d0](https://github.com/bmadcode/BMAD-METHOD/commit/7f016d0020705c2a048b656eeaaf9bd1762e4914))
7
- * resolve CommonJS import compatibility for chalk, inquirer, and ora ([#442](https://github.com/bmadcode/BMAD-METHOD/issues/442)) ([33269c8](https://github.com/bmadcode/BMAD-METHOD/commit/33269c888d930d197ab47a3ec1d8a66c5469c43b))
8
- * update package-lock.json for semver dependency ([6cb2fa6](https://github.com/bmadcode/BMAD-METHOD/commit/6cb2fa68b305dfe7eac052cd32d84839c57fb321))
9
- * update versions for dual publishing beta releases ([e0dcbcf](https://github.com/bmadcode/BMAD-METHOD/commit/e0dcbcf5277ac33a824b445060177fd3e71f13d4))
1
+ # [5.0.0-beta.2](https://github.com/bmadcode/BMAD-METHOD/compare/v5.0.0-beta.1...v5.0.0-beta.2) (2025-08-16)
10
2
 
11
3
 
12
4
  ### Features
13
5
 
14
- * add automated promotion workflow for stable releases ([fb02234](https://github.com/bmadcode/BMAD-METHOD/commit/fb02234b592f2345d8c42275e666cf01e5e92869))
15
- * publish stable release 5.0.0 ([93426c2](https://github.com/bmadcode/BMAD-METHOD/commit/93426c2d2f046ce37a9c491d1f55fe9f7a2566d8))
16
- * transform QA agent into Test Architect with advanced quality caโ€ฆ ([#433](https://github.com/bmadcode/BMAD-METHOD/issues/433)) ([0b61175](https://github.com/bmadcode/BMAD-METHOD/commit/0b61175d98e6def508cc82bb4539e7f37f8f6e1a))
17
-
18
-
19
- ### BREAKING CHANGES
20
-
21
- * Promote beta features to stable release for v5.0.0
22
-
23
- This commit ensures the stable release gets properly published to NPM and GitHub releases.
6
+ * **flattener:** prompt for detailed stats; polish .stats.md with emojis ([#422](https://github.com/bmadcode/BMAD-METHOD/issues/422)) ([fab9d5e](https://github.com/bmadcode/BMAD-METHOD/commit/fab9d5e1f55d7876b6909002415af89508cc41a7))
24
7
 
25
8
  ## [4.36.2](https://github.com/bmadcode/BMAD-METHOD/compare/v4.36.1...v4.36.2) (2025-08-10)
26
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bmad-method",
3
- "version": "5.0.0-beta.1",
3
+ "version": "5.0.0-beta.2",
4
4
  "description": "Breakthrough Method of Agile AI-driven Development",
5
5
  "main": "tools/cli.js",
6
6
  "bin": {
@@ -127,19 +127,11 @@ program
127
127
  path.join(inputDir, "flattened-codebase.xml"),
128
128
  );
129
129
  }
130
- } else {
131
- console.error(
132
- "Could not auto-detect a project root and no arguments were provided. Please specify -i/--input and -o/--output.",
133
- );
134
- process.exit(1);
135
130
  }
136
131
 
137
132
  // Ensure output directory exists
138
133
  await fs.ensureDir(path.dirname(outputPath));
139
134
 
140
- console.log(`Flattening codebase from: ${inputDir}`);
141
- console.log(`Output file: ${outputPath}`);
142
-
143
135
  try {
144
136
  // Verify input directory exists
145
137
  if (!await fs.pathExists(inputDir)) {
@@ -159,7 +151,6 @@ program
159
151
  );
160
152
 
161
153
  // Process files with progress tracking
162
- console.log("Reading file contents");
163
154
  const processingSpinner = ora("๐Ÿ“„ Processing files...").start();
164
155
  const aggregatedContent = await aggregateFileContents(
165
156
  filteredFiles,
@@ -172,10 +163,6 @@ program
172
163
  if (aggregatedContent.errors.length > 0) {
173
164
  console.log(`Errors: ${aggregatedContent.errors.length}`);
174
165
  }
175
- console.log(`Text files: ${aggregatedContent.textFiles.length}`);
176
- if (aggregatedContent.binaryFiles.length > 0) {
177
- console.log(`Binary files: ${aggregatedContent.binaryFiles.length}`);
178
- }
179
166
 
180
167
  // Generate XML output using streaming
181
168
  const xmlSpinner = ora("๐Ÿ”ง Generating XML output...").start();
@@ -184,7 +171,11 @@ program
184
171
 
185
172
  // Calculate and display statistics
186
173
  const outputStats = await fs.stat(outputPath);
187
- const stats = calculateStatistics(aggregatedContent, outputStats.size);
174
+ const stats = await calculateStatistics(
175
+ aggregatedContent,
176
+ outputStats.size,
177
+ inputDir,
178
+ );
188
179
 
189
180
  // Display completion summary
190
181
  console.log("\n๐Ÿ“Š Completion Summary:");
@@ -201,8 +192,476 @@ program
201
192
  );
202
193
  console.log(`๐Ÿ”ข Estimated tokens: ${stats.estimatedTokens}`);
203
194
  console.log(
204
- `๐Ÿ“Š File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`,
195
+ `๐Ÿ“Š File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors\n`,
196
+ );
197
+
198
+ // Ask user if they want detailed stats + markdown report
199
+ const generateDetailed = await promptYesNo(
200
+ "Generate detailed stats (console + markdown) now?",
201
+ true,
205
202
  );
203
+
204
+ if (generateDetailed) {
205
+ // Additional detailed stats
206
+ console.log("\n๐Ÿ“ˆ Size Percentiles:");
207
+ console.log(
208
+ ` Avg: ${
209
+ Math.round(stats.avgFileSize).toLocaleString()
210
+ } B, Median: ${
211
+ Math.round(stats.medianFileSize).toLocaleString()
212
+ } B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`,
213
+ );
214
+
215
+ if (Array.isArray(stats.histogram) && stats.histogram.length) {
216
+ console.log("\n๐Ÿงฎ Size Histogram:");
217
+ for (const b of stats.histogram.slice(0, 2)) {
218
+ console.log(
219
+ ` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`,
220
+ );
221
+ }
222
+ if (stats.histogram.length > 2) {
223
+ console.log(` โ€ฆ and ${stats.histogram.length - 2} more buckets`);
224
+ }
225
+ }
226
+
227
+ if (Array.isArray(stats.byExtension) && stats.byExtension.length) {
228
+ const topExt = stats.byExtension.slice(0, 2);
229
+ console.log("\n๐Ÿ“ฆ Top Extensions:");
230
+ for (const e of topExt) {
231
+ const pct = stats.totalBytes
232
+ ? ((e.bytes / stats.totalBytes) * 100)
233
+ : 0;
234
+ console.log(
235
+ ` ${e.ext}: ${e.count} files, ${e.bytes.toLocaleString()} bytes (${
236
+ pct.toFixed(2)
237
+ }%)`,
238
+ );
239
+ }
240
+ if (stats.byExtension.length > 2) {
241
+ console.log(
242
+ ` โ€ฆ and ${stats.byExtension.length - 2} more extensions`,
243
+ );
244
+ }
245
+ }
246
+
247
+ if (Array.isArray(stats.byDirectory) && stats.byDirectory.length) {
248
+ const topDir = stats.byDirectory.slice(0, 2);
249
+ console.log("\n๐Ÿ“‚ Top Directories:");
250
+ for (const d of topDir) {
251
+ const pct = stats.totalBytes
252
+ ? ((d.bytes / stats.totalBytes) * 100)
253
+ : 0;
254
+ console.log(
255
+ ` ${d.dir}: ${d.count} files, ${d.bytes.toLocaleString()} bytes (${
256
+ pct.toFixed(2)
257
+ }%)`,
258
+ );
259
+ }
260
+ if (stats.byDirectory.length > 2) {
261
+ console.log(
262
+ ` โ€ฆ and ${stats.byDirectory.length - 2} more directories`,
263
+ );
264
+ }
265
+ }
266
+
267
+ if (
268
+ Array.isArray(stats.depthDistribution) &&
269
+ stats.depthDistribution.length
270
+ ) {
271
+ console.log("\n๐ŸŒณ Depth Distribution:");
272
+ const dd = stats.depthDistribution.slice(0, 2);
273
+ let line = " " + dd.map((d) => `${d.depth}:${d.count}`).join(" ");
274
+ if (stats.depthDistribution.length > 2) {
275
+ line += ` โ€ฆ +${stats.depthDistribution.length - 2} more`;
276
+ }
277
+ console.log(line);
278
+ }
279
+
280
+ if (Array.isArray(stats.longestPaths) && stats.longestPaths.length) {
281
+ console.log("\n๐Ÿงต Longest Paths:");
282
+ for (const p of stats.longestPaths.slice(0, 2)) {
283
+ console.log(
284
+ ` ${p.path} (${p.length} chars, ${p.size.toLocaleString()} bytes)`,
285
+ );
286
+ }
287
+ if (stats.longestPaths.length > 2) {
288
+ console.log(` โ€ฆ and ${stats.longestPaths.length - 2} more paths`);
289
+ }
290
+ }
291
+
292
+ if (stats.temporal) {
293
+ console.log("\nโฑ๏ธ Temporal:");
294
+ if (stats.temporal.oldest) {
295
+ console.log(
296
+ ` Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`,
297
+ );
298
+ }
299
+ if (stats.temporal.newest) {
300
+ console.log(
301
+ ` Newest: ${stats.temporal.newest.path} (${stats.temporal.newest.mtime})`,
302
+ );
303
+ }
304
+ if (Array.isArray(stats.temporal.ageBuckets)) {
305
+ console.log(" Age buckets:");
306
+ for (const b of stats.temporal.ageBuckets.slice(0, 2)) {
307
+ console.log(
308
+ ` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`,
309
+ );
310
+ }
311
+ if (stats.temporal.ageBuckets.length > 2) {
312
+ console.log(
313
+ ` โ€ฆ and ${
314
+ stats.temporal.ageBuckets.length - 2
315
+ } more buckets`,
316
+ );
317
+ }
318
+ }
319
+ }
320
+
321
+ if (stats.quality) {
322
+ console.log("\nโœ… Quality Signals:");
323
+ console.log(` Zero-byte files: ${stats.quality.zeroByteFiles}`);
324
+ console.log(` Empty text files: ${stats.quality.emptyTextFiles}`);
325
+ console.log(` Hidden files: ${stats.quality.hiddenFiles}`);
326
+ console.log(` Symlinks: ${stats.quality.symlinks}`);
327
+ console.log(
328
+ ` Large files (>= ${
329
+ (stats.quality.largeThreshold / (1024 * 1024)).toFixed(0)
330
+ } MB): ${stats.quality.largeFilesCount}`,
331
+ );
332
+ console.log(
333
+ ` Suspiciously large files (>= 100 MB): ${stats.quality.suspiciousLargeFilesCount}`,
334
+ );
335
+ }
336
+
337
+ if (
338
+ Array.isArray(stats.duplicateCandidates) &&
339
+ stats.duplicateCandidates.length
340
+ ) {
341
+ console.log("\n๐Ÿงฌ Duplicate Candidates:");
342
+ for (const d of stats.duplicateCandidates.slice(0, 2)) {
343
+ console.log(
344
+ ` ${d.reason}: ${d.count} files @ ${d.size.toLocaleString()} bytes`,
345
+ );
346
+ }
347
+ if (stats.duplicateCandidates.length > 2) {
348
+ console.log(
349
+ ` โ€ฆ and ${stats.duplicateCandidates.length - 2} more groups`,
350
+ );
351
+ }
352
+ }
353
+
354
+ if (typeof stats.compressibilityRatio === "number") {
355
+ console.log(
356
+ `\n๐Ÿ—œ๏ธ Compressibility ratio (sampled): ${
357
+ (stats.compressibilityRatio * 100).toFixed(2)
358
+ }%`,
359
+ );
360
+ }
361
+
362
+ if (stats.git && stats.git.isRepo) {
363
+ console.log("\n๐Ÿ”ง Git:");
364
+ console.log(
365
+ ` Tracked: ${stats.git.trackedCount} files, ${stats.git.trackedBytes.toLocaleString()} bytes`,
366
+ );
367
+ console.log(
368
+ ` Untracked: ${stats.git.untrackedCount} files, ${stats.git.untrackedBytes.toLocaleString()} bytes`,
369
+ );
370
+ if (
371
+ Array.isArray(stats.git.lfsCandidates) &&
372
+ stats.git.lfsCandidates.length
373
+ ) {
374
+ console.log(" LFS candidates (top 2):");
375
+ for (const f of stats.git.lfsCandidates.slice(0, 2)) {
376
+ console.log(` ${f.path} (${f.size.toLocaleString()} bytes)`);
377
+ }
378
+ if (stats.git.lfsCandidates.length > 2) {
379
+ console.log(
380
+ ` โ€ฆ and ${stats.git.lfsCandidates.length - 2} more`,
381
+ );
382
+ }
383
+ }
384
+ }
385
+
386
+ if (Array.isArray(stats.largestFiles) && stats.largestFiles.length) {
387
+ console.log("\n๐Ÿ“š Largest Files (top 2):");
388
+ for (const f of stats.largestFiles.slice(0, 2)) {
389
+ // Show LOC for text files when available; omit ext and mtime
390
+ let locStr = "";
391
+ if (!f.isBinary && Array.isArray(aggregatedContent?.textFiles)) {
392
+ const tf = aggregatedContent.textFiles.find((t) =>
393
+ t.path === f.path
394
+ );
395
+ if (tf && typeof tf.lines === "number") {
396
+ locStr = `, LOC: ${tf.lines.toLocaleString()}`;
397
+ }
398
+ }
399
+ console.log(
400
+ ` ${f.path} โ€“ ${f.sizeFormatted} (${
401
+ f.percentOfTotal.toFixed(2)
402
+ }%)${locStr}`,
403
+ );
404
+ }
405
+ if (stats.largestFiles.length > 2) {
406
+ console.log(` โ€ฆ and ${stats.largestFiles.length - 2} more files`);
407
+ }
408
+ }
409
+
410
+ // Write a comprehensive markdown report next to the XML
411
+ {
412
+ const mdPath = outputPath.endsWith(".xml")
413
+ ? outputPath.replace(/\.xml$/i, ".stats.md")
414
+ : outputPath + ".stats.md";
415
+ try {
416
+ const pct = (num, den) => (den ? ((num / den) * 100) : 0);
417
+ const md = [];
418
+ md.push(`# ๐Ÿงพ Flatten Stats for ${path.basename(outputPath)}`);
419
+ md.push("");
420
+ md.push("## ๐Ÿ“Š Summary");
421
+ md.push(`- Total source size: ${stats.totalSize}`);
422
+ md.push(`- Generated XML size: ${stats.xmlSize}`);
423
+ md.push(
424
+ `- Total lines of code: ${stats.totalLines.toLocaleString()}`,
425
+ );
426
+ md.push(`- Estimated tokens: ${stats.estimatedTokens}`);
427
+ md.push(
428
+ `- File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`,
429
+ );
430
+ md.push("");
431
+
432
+ // Percentiles
433
+ md.push("## ๐Ÿ“ˆ Size Percentiles");
434
+ md.push(
435
+ `Avg: ${
436
+ Math.round(stats.avgFileSize).toLocaleString()
437
+ } B, Median: ${
438
+ Math.round(stats.medianFileSize).toLocaleString()
439
+ } B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`,
440
+ );
441
+ md.push("");
442
+
443
+ // Histogram
444
+ if (Array.isArray(stats.histogram) && stats.histogram.length) {
445
+ md.push("## ๐Ÿงฎ Size Histogram");
446
+ md.push("| Bucket | Files | Bytes |");
447
+ md.push("| --- | ---: | ---: |");
448
+ for (const b of stats.histogram) {
449
+ md.push(
450
+ `| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`,
451
+ );
452
+ }
453
+ md.push("");
454
+ }
455
+
456
+ // Top Extensions
457
+ if (Array.isArray(stats.byExtension) && stats.byExtension.length) {
458
+ md.push("## ๐Ÿ“ฆ Top Extensions by Bytes (Top 20)");
459
+ md.push("| Ext | Files | Bytes | % of total |");
460
+ md.push("| --- | ---: | ---: | ---: |");
461
+ for (const e of stats.byExtension.slice(0, 20)) {
462
+ const p = pct(e.bytes, stats.totalBytes);
463
+ md.push(
464
+ `| ${e.ext} | ${e.count} | ${e.bytes.toLocaleString()} | ${
465
+ p.toFixed(2)
466
+ }% |`,
467
+ );
468
+ }
469
+ md.push("");
470
+ }
471
+
472
+ // Top Directories
473
+ if (Array.isArray(stats.byDirectory) && stats.byDirectory.length) {
474
+ md.push("## ๐Ÿ“‚ Top Directories by Bytes (Top 20)");
475
+ md.push("| Directory | Files | Bytes | % of total |");
476
+ md.push("| --- | ---: | ---: | ---: |");
477
+ for (const d of stats.byDirectory.slice(0, 20)) {
478
+ const p = pct(d.bytes, stats.totalBytes);
479
+ md.push(
480
+ `| ${d.dir} | ${d.count} | ${d.bytes.toLocaleString()} | ${
481
+ p.toFixed(2)
482
+ }% |`,
483
+ );
484
+ }
485
+ md.push("");
486
+ }
487
+
488
+ // Depth distribution
489
+ if (
490
+ Array.isArray(stats.depthDistribution) &&
491
+ stats.depthDistribution.length
492
+ ) {
493
+ md.push("## ๐ŸŒณ Depth Distribution");
494
+ md.push("| Depth | Count |");
495
+ md.push("| ---: | ---: |");
496
+ for (const d of stats.depthDistribution) {
497
+ md.push(`| ${d.depth} | ${d.count} |`);
498
+ }
499
+ md.push("");
500
+ }
501
+
502
+ // Longest paths
503
+ if (
504
+ Array.isArray(stats.longestPaths) && stats.longestPaths.length
505
+ ) {
506
+ md.push("## ๐Ÿงต Longest Paths (Top 25)");
507
+ md.push("| Path | Length | Bytes |");
508
+ md.push("| --- | ---: | ---: |");
509
+ for (const pth of stats.longestPaths) {
510
+ md.push(
511
+ `| ${pth.path} | ${pth.length} | ${pth.size.toLocaleString()} |`,
512
+ );
513
+ }
514
+ md.push("");
515
+ }
516
+
517
+ // Temporal
518
+ if (stats.temporal) {
519
+ md.push("## โฑ๏ธ Temporal");
520
+ if (stats.temporal.oldest) {
521
+ md.push(
522
+ `- Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`,
523
+ );
524
+ }
525
+ if (stats.temporal.newest) {
526
+ md.push(
527
+ `- Newest: ${stats.temporal.newest.path} (${stats.temporal.newest.mtime})`,
528
+ );
529
+ }
530
+ if (Array.isArray(stats.temporal.ageBuckets)) {
531
+ md.push("");
532
+ md.push("| Age | Files | Bytes |");
533
+ md.push("| --- | ---: | ---: |");
534
+ for (const b of stats.temporal.ageBuckets) {
535
+ md.push(
536
+ `| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`,
537
+ );
538
+ }
539
+ }
540
+ md.push("");
541
+ }
542
+
543
+ // Quality signals
544
+ if (stats.quality) {
545
+ md.push("## โœ… Quality Signals");
546
+ md.push(`- Zero-byte files: ${stats.quality.zeroByteFiles}`);
547
+ md.push(`- Empty text files: ${stats.quality.emptyTextFiles}`);
548
+ md.push(`- Hidden files: ${stats.quality.hiddenFiles}`);
549
+ md.push(`- Symlinks: ${stats.quality.symlinks}`);
550
+ md.push(
551
+ `- Large files (>= ${
552
+ (stats.quality.largeThreshold / (1024 * 1024)).toFixed(0)
553
+ } MB): ${stats.quality.largeFilesCount}`,
554
+ );
555
+ md.push(
556
+ `- Suspiciously large files (>= 100 MB): ${stats.quality.suspiciousLargeFilesCount}`,
557
+ );
558
+ md.push("");
559
+ }
560
+
561
+ // Duplicates
562
+ if (
563
+ Array.isArray(stats.duplicateCandidates) &&
564
+ stats.duplicateCandidates.length
565
+ ) {
566
+ md.push("## ๐Ÿงฌ Duplicate Candidates");
567
+ md.push("| Reason | Files | Size (bytes) |");
568
+ md.push("| --- | ---: | ---: |");
569
+ for (const d of stats.duplicateCandidates) {
570
+ md.push(
571
+ `| ${d.reason} | ${d.count} | ${d.size.toLocaleString()} |`,
572
+ );
573
+ }
574
+ md.push("");
575
+ // Detailed listing of duplicate file names and locations
576
+ md.push("### ๐Ÿงฌ Duplicate Groups Details");
577
+ let dupIndex = 1;
578
+ for (const d of stats.duplicateCandidates) {
579
+ md.push(
580
+ `#### Group ${dupIndex}: ${d.count} files @ ${d.size.toLocaleString()} bytes (${d.reason})`,
581
+ );
582
+ if (Array.isArray(d.files) && d.files.length) {
583
+ for (const fp of d.files) {
584
+ md.push(`- ${fp}`);
585
+ }
586
+ } else {
587
+ md.push("- (file list unavailable)");
588
+ }
589
+ md.push("");
590
+ dupIndex++;
591
+ }
592
+ md.push("");
593
+ }
594
+
595
+ // Compressibility
596
+ if (typeof stats.compressibilityRatio === "number") {
597
+ md.push("## ๐Ÿ—œ๏ธ Compressibility");
598
+ md.push(
599
+ `Sampled compressibility ratio: ${
600
+ (stats.compressibilityRatio * 100).toFixed(2)
601
+ }%`,
602
+ );
603
+ md.push("");
604
+ }
605
+
606
+ // Git
607
+ if (stats.git && stats.git.isRepo) {
608
+ md.push("## ๐Ÿ”ง Git");
609
+ md.push(
610
+ `- Tracked: ${stats.git.trackedCount} files, ${stats.git.trackedBytes.toLocaleString()} bytes`,
611
+ );
612
+ md.push(
613
+ `- Untracked: ${stats.git.untrackedCount} files, ${stats.git.untrackedBytes.toLocaleString()} bytes`,
614
+ );
615
+ if (
616
+ Array.isArray(stats.git.lfsCandidates) &&
617
+ stats.git.lfsCandidates.length
618
+ ) {
619
+ md.push("");
620
+ md.push("### ๐Ÿ“ฆ LFS Candidates (Top 20)");
621
+ md.push("| Path | Bytes |");
622
+ md.push("| --- | ---: |");
623
+ for (const f of stats.git.lfsCandidates.slice(0, 20)) {
624
+ md.push(`| ${f.path} | ${f.size.toLocaleString()} |`);
625
+ }
626
+ }
627
+ md.push("");
628
+ }
629
+
630
+ // Largest Files
631
+ if (
632
+ Array.isArray(stats.largestFiles) && stats.largestFiles.length
633
+ ) {
634
+ md.push("## ๐Ÿ“š Largest Files (Top 50)");
635
+ md.push("| Path | Size | % of total | LOC |");
636
+ md.push("| --- | ---: | ---: | ---: |");
637
+ for (const f of stats.largestFiles) {
638
+ let loc = "";
639
+ if (
640
+ !f.isBinary && Array.isArray(aggregatedContent?.textFiles)
641
+ ) {
642
+ const tf = aggregatedContent.textFiles.find((t) =>
643
+ t.path === f.path
644
+ );
645
+ if (tf && typeof tf.lines === "number") {
646
+ loc = tf.lines.toLocaleString();
647
+ }
648
+ }
649
+ md.push(
650
+ `| ${f.path} | ${f.sizeFormatted} | ${
651
+ f.percentOfTotal.toFixed(2)
652
+ }% | ${loc} |`,
653
+ );
654
+ }
655
+ md.push("");
656
+ }
657
+
658
+ await fs.writeFile(mdPath, md.join("\n"));
659
+ console.log(`\n๐Ÿงพ Detailed stats report written to: ${mdPath}`);
660
+ } catch (e) {
661
+ console.warn(`โš ๏ธ Failed to write stats markdown: ${e.message}`);
662
+ }
663
+ }
664
+ }
206
665
  } catch (error) {
207
666
  console.error("โŒ Critical error:", error.message);
208
667
  console.error("An unexpected error occurred.");