@zeyue0329/xiaoma-cli 1.0.36 → 1.0.38

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.
Files changed (89) hide show
  1. package/.idea/workspace.xml +27 -26
  2. package/JAVA-BACKEND-COMMANDS-REFERENCE.md +62 -52
  3. package/JAVA-BACKEND-ITERATION-GUIDE.md +125 -18
  4. package/README.md +1 -1
  5. package/common/utils/bmad-doc-template.md +5 -5
  6. package/dist/agents/analyst.txt +35 -5
  7. package/dist/agents/architect.txt +217 -31
  8. package/dist/agents/automation-orchestrator.txt +4 -4
  9. package/dist/agents/dev.txt +3 -3
  10. package/dist/agents/full-requirement-orchestrator.txt +11 -11
  11. package/dist/agents/qa.txt +102 -102
  12. package/dist/agents/sm.txt +6 -6
  13. package/dist/agents/ux-expert.txt +6 -1
  14. package/dist/agents/workflow-executor.txt +879 -0
  15. package/dist/agents/xiaoma-master.txt +258 -37
  16. package/dist/teams/team-all.txt +1223 -445
  17. package/dist/teams/team-fullstack-with-database.txt +384 -446
  18. package/dist/teams/team-fullstack.txt +258 -37
  19. package/dist/teams/team-ide-minimal.txt +111 -111
  20. package/dist/teams/team-no-ui.txt +252 -36
  21. package/docs/architecture-sharding-modification.md +623 -0
  22. package/docs/automated-requirements-analysis-outputs.md +896 -0
  23. package/package.json +1 -1
  24. package/tools/builders/web-builder.js +292 -142
  25. package/tools/bump-all-versions.js +50 -32
  26. package/tools/cli.js +52 -47
  27. package/tools/flattener/aggregate.js +30 -12
  28. package/tools/flattener/binary.js +46 -43
  29. package/tools/flattener/discovery.js +23 -15
  30. package/tools/flattener/files.js +6 -6
  31. package/tools/flattener/ignoreRules.js +122 -121
  32. package/tools/flattener/main.js +249 -144
  33. package/tools/flattener/projectRoot.js +74 -69
  34. package/tools/flattener/prompts.js +12 -10
  35. package/tools/flattener/stats.helpers.js +90 -61
  36. package/tools/flattener/stats.js +1 -1
  37. package/tools/flattener/test-matrix.js +225 -170
  38. package/tools/flattener/xml.js +31 -23
  39. package/tools/installer/bin/xiaoma.js +199 -153
  40. package/tools/installer/lib/config-loader.js +76 -47
  41. package/tools/installer/lib/file-manager.js +101 -44
  42. package/tools/installer/lib/ide-base-setup.js +49 -39
  43. package/tools/installer/lib/ide-setup.js +694 -380
  44. package/tools/installer/lib/installer.js +802 -469
  45. package/tools/installer/lib/memory-profiler.js +22 -12
  46. package/tools/installer/lib/module-manager.js +16 -14
  47. package/tools/installer/lib/resource-locator.js +61 -35
  48. package/tools/lib/dependency-resolver.js +34 -23
  49. package/tools/lib/yaml-utils.js +7 -2
  50. package/tools/preview-release-notes.js +33 -25
  51. package/tools/shared/bannerArt.js +3 -3
  52. package/tools/sync-installer-version.js +16 -7
  53. package/tools/upgraders/v3-to-v4-upgrader.js +244 -163
  54. package/tools/version-bump.js +24 -18
  55. package/tools/xiaoma-npx-wrapper.js +15 -10
  56. package/tools/yaml-format.js +60 -36
  57. package/xiaoma-core/agent-teams/team-fullstack-with-database.yaml +0 -1
  58. package/xiaoma-core/agents/automated-fix-validator.yaml +2 -1
  59. package/xiaoma-core/agents/automated-quality-validator.yaml +10 -5
  60. package/xiaoma-core/agents/automation-orchestrator.md +4 -4
  61. package/xiaoma-core/agents/dev.md +4 -4
  62. package/xiaoma-core/agents/enhanced-workflow-orchestrator.yaml +2 -1
  63. package/xiaoma-core/agents/full-requirement-orchestrator.md +11 -11
  64. package/xiaoma-core/agents/global-requirements-auditor.yaml +11 -3
  65. package/xiaoma-core/agents/intelligent-template-adapter.yaml +19 -5
  66. package/xiaoma-core/agents/master-execution-engine.yaml +19 -5
  67. package/xiaoma-core/agents/workflow-executor.md +126 -18
  68. package/xiaoma-core/agents/xiaoma-master.md +1 -1
  69. package/xiaoma-core/data/test-levels-framework.md +12 -12
  70. package/xiaoma-core/tasks/analyze-existing-database.md +1 -1
  71. package/xiaoma-core/tasks/apply-qa-fixes.md +3 -3
  72. package/xiaoma-core/tasks/batch-story-generation.md +22 -22
  73. package/xiaoma-core/tasks/create-enhanced-story-with-database.md +6 -6
  74. package/xiaoma-core/tasks/nfr-assess.md +6 -6
  75. package/xiaoma-core/tasks/project-integration-testing.md +42 -42
  76. package/xiaoma-core/tasks/qa-gate.md +23 -23
  77. package/xiaoma-core/tasks/review-story.md +18 -18
  78. package/xiaoma-core/tasks/risk-profile.md +25 -25
  79. package/xiaoma-core/tasks/serial-development-orchestration.md +51 -51
  80. package/xiaoma-core/tasks/test-design.md +9 -9
  81. package/xiaoma-core/tasks/trace-requirements.md +21 -21
  82. package/xiaoma-core/templates/competitor-analysis-tmpl.yaml +35 -5
  83. package/xiaoma-core/templates/front-end-architecture-tmpl.yaml +77 -11
  84. package/xiaoma-core/templates/front-end-spec-tmpl.yaml +6 -1
  85. package/xiaoma-core/templates/fullstack-architecture-tmpl.yaml +140 -20
  86. package/xiaoma-core/templates/global-qa-monitoring-tmpl.yaml +2 -1
  87. package/xiaoma-core/templates/requirements-coverage-audit.yaml +2 -1
  88. package/xiaoma-core/workflows/automated-requirements-analysis.yaml +4 -4
  89. package/dist/agents/database-architect.txt +0 -322
@@ -1,14 +1,18 @@
1
- const { Command } = require('commander');
2
- const fs = require('fs-extra');
3
- const path = require('node:path');
4
- const process = require('node:process');
1
+ const { Command } = require("commander");
2
+ const fs = require("fs-extra");
3
+ const path = require("node:path");
4
+ const process = require("node:process");
5
5
 
6
6
  // Modularized components
7
- const { findProjectRoot } = require('./projectRoot.js');
8
- const { promptYesNo, promptPath } = require('./prompts.js');
9
- const { discoverFiles, filterFiles, aggregateFileContents } = require('./files.js');
10
- const { generateXMLOutput } = require('./xml.js');
11
- const { calculateStatistics } = require('./stats.js');
7
+ const { findProjectRoot } = require("./projectRoot.js");
8
+ const { promptYesNo, promptPath } = require("./prompts.js");
9
+ const {
10
+ discoverFiles,
11
+ filterFiles,
12
+ aggregateFileContents,
13
+ } = require("./files.js");
14
+ const { generateXMLOutput } = require("./xml.js");
15
+ const { calculateStatistics } = require("./stats.js");
12
16
 
13
17
  /**
14
18
  * Recursively discover all files in a directory
@@ -67,11 +71,11 @@ const { calculateStatistics } = require('./stats.js');
67
71
  const program = new Command();
68
72
 
69
73
  program
70
- .name('bmad-flatten')
71
- .description('XIAOMA-CLI™ codebase flattener tool')
72
- .version('1.0.0')
73
- .option('-i, --input <path>', 'Input directory to flatten', process.cwd())
74
- .option('-o, --output <path>', 'Output file path', 'flattened-codebase.xml')
74
+ .name("bmad-flatten")
75
+ .description("XIAOMA-CLI™ codebase flattener tool")
76
+ .version("1.0.0")
77
+ .option("-i, --input <path>", "Input directory to flatten", process.cwd())
78
+ .option("-o, --output <path>", "Output file path", "flattened-codebase.xml")
75
79
  .action(async (options) => {
76
80
  let inputDir = path.resolve(options.input);
77
81
  let outputPath = path.resolve(options.output);
@@ -79,18 +83,18 @@ program
79
83
  // Detect if user explicitly provided -i/--input or -o/--output
80
84
  const argv = process.argv.slice(2);
81
85
  const userSpecifiedInput = argv.some(
82
- (a) => a === '-i' || a === '--input' || a.startsWith('--input='),
86
+ (a) => a === "-i" || a === "--input" || a.startsWith("--input="),
83
87
  );
84
88
  const userSpecifiedOutput = argv.some(
85
- (a) => a === '-o' || a === '--output' || a.startsWith('--output='),
89
+ (a) => a === "-o" || a === "--output" || a.startsWith("--output="),
86
90
  );
87
91
  const noPathArguments = !userSpecifiedInput && !userSpecifiedOutput;
88
92
 
89
93
  if (noPathArguments) {
90
94
  const detectedRoot = await findProjectRoot(process.cwd());
91
95
  const suggestedOutput = detectedRoot
92
- ? path.join(detectedRoot, 'flattened-codebase.xml')
93
- : path.resolve('flattened-codebase.xml');
96
+ ? path.join(detectedRoot, "flattened-codebase.xml")
97
+ : path.resolve("flattened-codebase.xml");
94
98
 
95
99
  if (detectedRoot) {
96
100
  const useDefaults = await promptYesNo(
@@ -101,18 +105,24 @@ program
101
105
  inputDir = detectedRoot;
102
106
  outputPath = suggestedOutput;
103
107
  } else {
104
- inputDir = await promptPath('Enter input directory path', process.cwd());
108
+ inputDir = await promptPath(
109
+ "Enter input directory path",
110
+ process.cwd(),
111
+ );
105
112
  outputPath = await promptPath(
106
- 'Enter output file path',
107
- path.join(inputDir, 'flattened-codebase.xml'),
113
+ "Enter output file path",
114
+ path.join(inputDir, "flattened-codebase.xml"),
108
115
  );
109
116
  }
110
117
  } else {
111
- console.log('Could not auto-detect a project root.');
112
- inputDir = await promptPath('Enter input directory path', process.cwd());
118
+ console.log("Could not auto-detect a project root.");
119
+ inputDir = await promptPath(
120
+ "Enter input directory path",
121
+ process.cwd(),
122
+ );
113
123
  outputPath = await promptPath(
114
- 'Enter output file path',
115
- path.join(inputDir, 'flattened-codebase.xml'),
124
+ "Enter output file path",
125
+ path.join(inputDir, "flattened-codebase.xml"),
116
126
  );
117
127
  }
118
128
  }
@@ -128,17 +138,19 @@ program
128
138
  }
129
139
 
130
140
  // Import ora dynamically
131
- const { default: ora } = await import('ora');
141
+ const { default: ora } = await import("ora");
132
142
 
133
143
  // Start file discovery with spinner
134
- const discoverySpinner = ora('🔍 Discovering files...').start();
144
+ const discoverySpinner = ora("🔍 Discovering files...").start();
135
145
  const files = await discoverFiles(inputDir);
136
146
  const filteredFiles = await filterFiles(files, inputDir);
137
- discoverySpinner.succeed(`📁 Found ${filteredFiles.length} files to include`);
147
+ discoverySpinner.succeed(
148
+ `📁 Found ${filteredFiles.length} files to include`,
149
+ );
138
150
 
139
151
  // Process files with progress tracking
140
- console.log('Reading file contents');
141
- const processingSpinner = ora('📄 Processing files...').start();
152
+ console.log("Reading file contents");
153
+ const processingSpinner = ora("📄 Processing files...").start();
142
154
  const aggregatedContent = await aggregateFileContents(
143
155
  filteredFiles,
144
156
  inputDir,
@@ -152,23 +164,29 @@ program
152
164
  }
153
165
 
154
166
  // Generate XML output using streaming
155
- const xmlSpinner = ora('🔧 Generating XML output...').start();
167
+ const xmlSpinner = ora("🔧 Generating XML output...").start();
156
168
  await generateXMLOutput(aggregatedContent, outputPath);
157
- xmlSpinner.succeed('📝 XML generation completed');
169
+ xmlSpinner.succeed("📝 XML generation completed");
158
170
 
159
171
  // Calculate and display statistics
160
172
  const outputStats = await fs.stat(outputPath);
161
- const stats = await calculateStatistics(aggregatedContent, outputStats.size, inputDir);
173
+ const stats = await calculateStatistics(
174
+ aggregatedContent,
175
+ outputStats.size,
176
+ inputDir,
177
+ );
162
178
 
163
179
  // Display completion summary
164
- console.log('\n📊 Completion Summary:');
180
+ console.log("\n📊 Completion Summary:");
165
181
  console.log(
166
182
  `✅ Successfully processed ${filteredFiles.length} files into ${path.basename(outputPath)}`,
167
183
  );
168
184
  console.log(`📁 Output file: ${outputPath}`);
169
185
  console.log(`📏 Total source size: ${stats.totalSize}`);
170
186
  console.log(`📄 Generated XML size: ${stats.xmlSize}`);
171
- console.log(`📝 Total lines of code: ${stats.totalLines.toLocaleString()}`);
187
+ console.log(
188
+ `📝 Total lines of code: ${stats.totalLines.toLocaleString()}`,
189
+ );
172
190
  console.log(`🔢 Estimated tokens: ${stats.estimatedTokens}`);
173
191
  console.log(
174
192
  `📊 File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors\n`,
@@ -176,13 +194,13 @@ program
176
194
 
177
195
  // Ask user if they want detailed stats + markdown report
178
196
  const generateDetailed = await promptYesNo(
179
- 'Generate detailed stats (console + markdown) now?',
197
+ "Generate detailed stats (console + markdown) now?",
180
198
  true,
181
199
  );
182
200
 
183
201
  if (generateDetailed) {
184
202
  // Additional detailed stats
185
- console.log('\n📈 Size Percentiles:');
203
+ console.log("\n📈 Size Percentiles:");
186
204
  console.log(
187
205
  ` Avg: ${Math.round(stats.avgFileSize).toLocaleString()} B, Median: ${Math.round(
188
206
  stats.medianFileSize,
@@ -190,9 +208,11 @@ program
190
208
  );
191
209
 
192
210
  if (Array.isArray(stats.histogram) && stats.histogram.length > 0) {
193
- console.log('\n🧮 Size Histogram:');
211
+ console.log("\n🧮 Size Histogram:");
194
212
  for (const b of stats.histogram.slice(0, 2)) {
195
- console.log(` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`);
213
+ console.log(
214
+ ` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`,
215
+ );
196
216
  }
197
217
  if (stats.histogram.length > 2) {
198
218
  console.log(` … and ${stats.histogram.length - 2} more buckets`);
@@ -201,9 +221,11 @@ program
201
221
 
202
222
  if (Array.isArray(stats.byExtension) && stats.byExtension.length > 0) {
203
223
  const topExt = stats.byExtension.slice(0, 2);
204
- console.log('\n📦 Top Extensions:');
224
+ console.log("\n📦 Top Extensions:");
205
225
  for (const e of topExt) {
206
- const pct = stats.totalBytes ? (e.bytes / stats.totalBytes) * 100 : 0;
226
+ const pct = stats.totalBytes
227
+ ? (e.bytes / stats.totalBytes) * 100
228
+ : 0;
207
229
  console.log(
208
230
  ` ${e.ext}: ${e.count} files, ${e.bytes.toLocaleString()} bytes (${pct.toFixed(
209
231
  2,
@@ -211,15 +233,19 @@ program
211
233
  );
212
234
  }
213
235
  if (stats.byExtension.length > 2) {
214
- console.log(` … and ${stats.byExtension.length - 2} more extensions`);
236
+ console.log(
237
+ ` … and ${stats.byExtension.length - 2} more extensions`,
238
+ );
215
239
  }
216
240
  }
217
241
 
218
242
  if (Array.isArray(stats.byDirectory) && stats.byDirectory.length > 0) {
219
243
  const topDir = stats.byDirectory.slice(0, 2);
220
- console.log('\n📂 Top Directories:');
244
+ console.log("\n📂 Top Directories:");
221
245
  for (const d of topDir) {
222
- const pct = stats.totalBytes ? (d.bytes / stats.totalBytes) * 100 : 0;
246
+ const pct = stats.totalBytes
247
+ ? (d.bytes / stats.totalBytes) * 100
248
+ : 0;
223
249
  console.log(
224
250
  ` ${d.dir}: ${d.count} files, ${d.bytes.toLocaleString()} bytes (${pct.toFixed(
225
251
  2,
@@ -227,24 +253,34 @@ program
227
253
  );
228
254
  }
229
255
  if (stats.byDirectory.length > 2) {
230
- console.log(` … and ${stats.byDirectory.length - 2} more directories`);
256
+ console.log(
257
+ ` … and ${stats.byDirectory.length - 2} more directories`,
258
+ );
231
259
  }
232
260
  }
233
261
 
234
- if (Array.isArray(stats.depthDistribution) && stats.depthDistribution.length > 0) {
235
- console.log('\n🌳 Depth Distribution:');
262
+ if (
263
+ Array.isArray(stats.depthDistribution) &&
264
+ stats.depthDistribution.length > 0
265
+ ) {
266
+ console.log("\n🌳 Depth Distribution:");
236
267
  const dd = stats.depthDistribution.slice(0, 2);
237
- let line = ' ' + dd.map((d) => `${d.depth}:${d.count}`).join(' ');
268
+ let line = " " + dd.map((d) => `${d.depth}:${d.count}`).join(" ");
238
269
  if (stats.depthDistribution.length > 2) {
239
270
  line += ` … +${stats.depthDistribution.length - 2} more`;
240
271
  }
241
272
  console.log(line);
242
273
  }
243
274
 
244
- if (Array.isArray(stats.longestPaths) && stats.longestPaths.length > 0) {
245
- console.log('\n🧵 Longest Paths:');
275
+ if (
276
+ Array.isArray(stats.longestPaths) &&
277
+ stats.longestPaths.length > 0
278
+ ) {
279
+ console.log("\n🧵 Longest Paths:");
246
280
  for (const p of stats.longestPaths.slice(0, 2)) {
247
- console.log(` ${p.path} (${p.length} chars, ${p.size.toLocaleString()} bytes)`);
281
+ console.log(
282
+ ` ${p.path} (${p.length} chars, ${p.size.toLocaleString()} bytes)`,
283
+ );
248
284
  }
249
285
  if (stats.longestPaths.length > 2) {
250
286
  console.log(` … and ${stats.longestPaths.length - 2} more paths`);
@@ -252,7 +288,7 @@ program
252
288
  }
253
289
 
254
290
  if (stats.temporal) {
255
- console.log('\n⏱️ Temporal:');
291
+ console.log("\n⏱️ Temporal:");
256
292
  if (stats.temporal.oldest) {
257
293
  console.log(
258
294
  ` Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`,
@@ -264,77 +300,99 @@ program
264
300
  );
265
301
  }
266
302
  if (Array.isArray(stats.temporal.ageBuckets)) {
267
- console.log(' Age buckets:');
303
+ console.log(" Age buckets:");
268
304
  for (const b of stats.temporal.ageBuckets.slice(0, 2)) {
269
- console.log(` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`);
305
+ console.log(
306
+ ` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`,
307
+ );
270
308
  }
271
309
  if (stats.temporal.ageBuckets.length > 2) {
272
- console.log(` … and ${stats.temporal.ageBuckets.length - 2} more buckets`);
310
+ console.log(
311
+ ` … and ${stats.temporal.ageBuckets.length - 2} more buckets`,
312
+ );
273
313
  }
274
314
  }
275
315
  }
276
316
 
277
317
  if (stats.quality) {
278
- console.log('\n✅ Quality Signals:');
318
+ console.log("\n✅ Quality Signals:");
279
319
  console.log(` Zero-byte files: ${stats.quality.zeroByteFiles}`);
280
320
  console.log(` Empty text files: ${stats.quality.emptyTextFiles}`);
281
321
  console.log(` Hidden files: ${stats.quality.hiddenFiles}`);
282
322
  console.log(` Symlinks: ${stats.quality.symlinks}`);
283
323
  console.log(
284
- ` Large files (>= ${(stats.quality.largeThreshold / (1024 * 1024)).toFixed(
285
- 0,
286
- )} MB): ${stats.quality.largeFilesCount}`,
324
+ ` Large files (>= ${(
325
+ stats.quality.largeThreshold /
326
+ (1024 * 1024)
327
+ ).toFixed(0)} MB): ${stats.quality.largeFilesCount}`,
287
328
  );
288
329
  console.log(
289
330
  ` Suspiciously large files (>= 100 MB): ${stats.quality.suspiciousLargeFilesCount}`,
290
331
  );
291
332
  }
292
333
 
293
- if (Array.isArray(stats.duplicateCandidates) && stats.duplicateCandidates.length > 0) {
294
- console.log('\n🧬 Duplicate Candidates:');
334
+ if (
335
+ Array.isArray(stats.duplicateCandidates) &&
336
+ stats.duplicateCandidates.length > 0
337
+ ) {
338
+ console.log("\n🧬 Duplicate Candidates:");
295
339
  for (const d of stats.duplicateCandidates.slice(0, 2)) {
296
- console.log(` ${d.reason}: ${d.count} files @ ${d.size.toLocaleString()} bytes`);
340
+ console.log(
341
+ ` ${d.reason}: ${d.count} files @ ${d.size.toLocaleString()} bytes`,
342
+ );
297
343
  }
298
344
  if (stats.duplicateCandidates.length > 2) {
299
- console.log(` … and ${stats.duplicateCandidates.length - 2} more groups`);
345
+ console.log(
346
+ ` … and ${stats.duplicateCandidates.length - 2} more groups`,
347
+ );
300
348
  }
301
349
  }
302
350
 
303
- if (typeof stats.compressibilityRatio === 'number') {
351
+ if (typeof stats.compressibilityRatio === "number") {
304
352
  console.log(
305
- `\n🗜️ Compressibility ratio (sampled): ${(stats.compressibilityRatio * 100).toFixed(
306
- 2,
307
- )}%`,
353
+ `\n🗜️ Compressibility ratio (sampled): ${(
354
+ stats.compressibilityRatio * 100
355
+ ).toFixed(2)}%`,
308
356
  );
309
357
  }
310
358
 
311
359
  if (stats.git && stats.git.isRepo) {
312
- console.log('\n🔧 Git:');
360
+ console.log("\n🔧 Git:");
313
361
  console.log(
314
362
  ` Tracked: ${stats.git.trackedCount} files, ${stats.git.trackedBytes.toLocaleString()} bytes`,
315
363
  );
316
364
  console.log(
317
365
  ` Untracked: ${stats.git.untrackedCount} files, ${stats.git.untrackedBytes.toLocaleString()} bytes`,
318
366
  );
319
- if (Array.isArray(stats.git.lfsCandidates) && stats.git.lfsCandidates.length > 0) {
320
- console.log(' LFS candidates (top 2):');
367
+ if (
368
+ Array.isArray(stats.git.lfsCandidates) &&
369
+ stats.git.lfsCandidates.length > 0
370
+ ) {
371
+ console.log(" LFS candidates (top 2):");
321
372
  for (const f of stats.git.lfsCandidates.slice(0, 2)) {
322
373
  console.log(` ${f.path} (${f.size.toLocaleString()} bytes)`);
323
374
  }
324
375
  if (stats.git.lfsCandidates.length > 2) {
325
- console.log(` … and ${stats.git.lfsCandidates.length - 2} more`);
376
+ console.log(
377
+ ` … and ${stats.git.lfsCandidates.length - 2} more`,
378
+ );
326
379
  }
327
380
  }
328
381
  }
329
382
 
330
- if (Array.isArray(stats.largestFiles) && stats.largestFiles.length > 0) {
331
- console.log('\n📚 Largest Files (top 2):');
383
+ if (
384
+ Array.isArray(stats.largestFiles) &&
385
+ stats.largestFiles.length > 0
386
+ ) {
387
+ console.log("\n📚 Largest Files (top 2):");
332
388
  for (const f of stats.largestFiles.slice(0, 2)) {
333
389
  // Show LOC for text files when available; omit ext and mtime
334
- let locStr = '';
390
+ let locStr = "";
335
391
  if (!f.isBinary && Array.isArray(aggregatedContent?.textFiles)) {
336
- const tf = aggregatedContent.textFiles.find((t) => t.path === f.path);
337
- if (tf && typeof tf.lines === 'number') {
392
+ const tf = aggregatedContent.textFiles.find(
393
+ (t) => t.path === f.path,
394
+ );
395
+ if (tf && typeof tf.lines === "number") {
338
396
  locStr = `, LOC: ${tf.lines.toLocaleString()}`;
339
397
  }
340
398
  }
@@ -349,48 +407,53 @@ program
349
407
 
350
408
  // Write a comprehensive markdown report next to the XML
351
409
  {
352
- const mdPath = outputPath.endsWith('.xml')
353
- ? outputPath.replace(/\.xml$/i, '.stats.md')
354
- : outputPath + '.stats.md';
410
+ const mdPath = outputPath.endsWith(".xml")
411
+ ? outputPath.replace(/\.xml$/i, ".stats.md")
412
+ : outputPath + ".stats.md";
355
413
  try {
356
414
  const pct = (num, den) => (den ? (num / den) * 100 : 0);
357
415
  const md = [];
358
416
  md.push(
359
417
  `# 🧾 Flatten Stats for ${path.basename(outputPath)}`,
360
- '',
361
- '## 📊 Summary',
418
+ "",
419
+ "## 📊 Summary",
362
420
  `- Total source size: ${stats.totalSize}`,
363
421
  `- Generated XML size: ${stats.xmlSize}`,
364
422
  `- Total lines of code: ${stats.totalLines.toLocaleString()}`,
365
423
  `- Estimated tokens: ${stats.estimatedTokens}`,
366
424
  `- File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`,
367
- '',
368
- '## 📈 Size Percentiles',
425
+ "",
426
+ "## 📈 Size Percentiles",
369
427
  `Avg: ${Math.round(stats.avgFileSize).toLocaleString()} B, Median: ${Math.round(
370
428
  stats.medianFileSize,
371
429
  ).toLocaleString()} B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`,
372
- '',
430
+ "",
373
431
  );
374
432
 
375
433
  // Histogram
376
434
  if (Array.isArray(stats.histogram) && stats.histogram.length > 0) {
377
435
  md.push(
378
- '## 🧮 Size Histogram',
379
- '| Bucket | Files | Bytes |',
380
- '| --- | ---: | ---: |',
436
+ "## 🧮 Size Histogram",
437
+ "| Bucket | Files | Bytes |",
438
+ "| --- | ---: | ---: |",
381
439
  );
382
440
  for (const b of stats.histogram) {
383
- md.push(`| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`);
441
+ md.push(
442
+ `| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`,
443
+ );
384
444
  }
385
- md.push('');
445
+ md.push("");
386
446
  }
387
447
 
388
448
  // Top Extensions
389
- if (Array.isArray(stats.byExtension) && stats.byExtension.length > 0) {
449
+ if (
450
+ Array.isArray(stats.byExtension) &&
451
+ stats.byExtension.length > 0
452
+ ) {
390
453
  md.push(
391
- '## 📦 Top Extensions by Bytes (Top 20)',
392
- '| Ext | Files | Bytes | % of total |',
393
- '| --- | ---: | ---: | ---: |',
454
+ "## 📦 Top Extensions by Bytes (Top 20)",
455
+ "| Ext | Files | Bytes | % of total |",
456
+ "| --- | ---: | ---: | ---: |",
394
457
  );
395
458
  for (const e of stats.byExtension.slice(0, 20)) {
396
459
  const p = pct(e.bytes, stats.totalBytes);
@@ -398,15 +461,18 @@ program
398
461
  `| ${e.ext} | ${e.count} | ${e.bytes.toLocaleString()} | ${p.toFixed(2)}% |`,
399
462
  );
400
463
  }
401
- md.push('');
464
+ md.push("");
402
465
  }
403
466
 
404
467
  // Top Directories
405
- if (Array.isArray(stats.byDirectory) && stats.byDirectory.length > 0) {
468
+ if (
469
+ Array.isArray(stats.byDirectory) &&
470
+ stats.byDirectory.length > 0
471
+ ) {
406
472
  md.push(
407
- '## 📂 Top Directories by Bytes (Top 20)',
408
- '| Directory | Files | Bytes | % of total |',
409
- '| --- | ---: | ---: | ---: |',
473
+ "## 📂 Top Directories by Bytes (Top 20)",
474
+ "| Directory | Files | Bytes | % of total |",
475
+ "| --- | ---: | ---: | ---: |",
410
476
  );
411
477
  for (const d of stats.byDirectory.slice(0, 20)) {
412
478
  const p = pct(d.bytes, stats.totalBytes);
@@ -414,74 +480,97 @@ program
414
480
  `| ${d.dir} | ${d.count} | ${d.bytes.toLocaleString()} | ${p.toFixed(2)}% |`,
415
481
  );
416
482
  }
417
- md.push('');
483
+ md.push("");
418
484
  }
419
485
 
420
486
  // Depth distribution
421
- if (Array.isArray(stats.depthDistribution) && stats.depthDistribution.length > 0) {
422
- md.push('## 🌳 Depth Distribution', '| Depth | Count |', '| ---: | ---: |');
487
+ if (
488
+ Array.isArray(stats.depthDistribution) &&
489
+ stats.depthDistribution.length > 0
490
+ ) {
491
+ md.push(
492
+ "## 🌳 Depth Distribution",
493
+ "| Depth | Count |",
494
+ "| ---: | ---: |",
495
+ );
423
496
  for (const d of stats.depthDistribution) {
424
497
  md.push(`| ${d.depth} | ${d.count} |`);
425
498
  }
426
- md.push('');
499
+ md.push("");
427
500
  }
428
501
 
429
502
  // Longest paths
430
- if (Array.isArray(stats.longestPaths) && stats.longestPaths.length > 0) {
503
+ if (
504
+ Array.isArray(stats.longestPaths) &&
505
+ stats.longestPaths.length > 0
506
+ ) {
431
507
  md.push(
432
- '## 🧵 Longest Paths (Top 25)',
433
- '| Path | Length | Bytes |',
434
- '| --- | ---: | ---: |',
508
+ "## 🧵 Longest Paths (Top 25)",
509
+ "| Path | Length | Bytes |",
510
+ "| --- | ---: | ---: |",
435
511
  );
436
512
  for (const pth of stats.longestPaths) {
437
- md.push(`| ${pth.path} | ${pth.length} | ${pth.size.toLocaleString()} |`);
513
+ md.push(
514
+ `| ${pth.path} | ${pth.length} | ${pth.size.toLocaleString()} |`,
515
+ );
438
516
  }
439
- md.push('');
517
+ md.push("");
440
518
  }
441
519
 
442
520
  // Temporal
443
521
  if (stats.temporal) {
444
- md.push('## ⏱️ Temporal');
522
+ md.push("## ⏱️ Temporal");
445
523
  if (stats.temporal.oldest) {
446
- md.push(`- Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`);
524
+ md.push(
525
+ `- Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`,
526
+ );
447
527
  }
448
528
  if (stats.temporal.newest) {
449
- md.push(`- Newest: ${stats.temporal.newest.path} (${stats.temporal.newest.mtime})`);
529
+ md.push(
530
+ `- Newest: ${stats.temporal.newest.path} (${stats.temporal.newest.mtime})`,
531
+ );
450
532
  }
451
533
  if (Array.isArray(stats.temporal.ageBuckets)) {
452
- md.push('', '| Age | Files | Bytes |', '| --- | ---: | ---: |');
534
+ md.push("", "| Age | Files | Bytes |", "| --- | ---: | ---: |");
453
535
  for (const b of stats.temporal.ageBuckets) {
454
- md.push(`| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`);
536
+ md.push(
537
+ `| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`,
538
+ );
455
539
  }
456
540
  }
457
- md.push('');
541
+ md.push("");
458
542
  }
459
543
 
460
544
  // Quality signals
461
545
  if (stats.quality) {
462
546
  md.push(
463
- '## ✅ Quality Signals',
547
+ "## ✅ Quality Signals",
464
548
  `- Zero-byte files: ${stats.quality.zeroByteFiles}`,
465
549
  `- Empty text files: ${stats.quality.emptyTextFiles}`,
466
550
  `- Hidden files: ${stats.quality.hiddenFiles}`,
467
551
  `- Symlinks: ${stats.quality.symlinks}`,
468
552
  `- Large files (>= ${(stats.quality.largeThreshold / (1024 * 1024)).toFixed(0)} MB): ${stats.quality.largeFilesCount}`,
469
553
  `- Suspiciously large files (>= 100 MB): ${stats.quality.suspiciousLargeFilesCount}`,
470
- '',
554
+ "",
471
555
  );
472
556
  }
473
557
 
474
558
  // Duplicates
475
- if (Array.isArray(stats.duplicateCandidates) && stats.duplicateCandidates.length > 0) {
559
+ if (
560
+ Array.isArray(stats.duplicateCandidates) &&
561
+ stats.duplicateCandidates.length > 0
562
+ ) {
476
563
  md.push(
477
- '## 🧬 Duplicate Candidates',
478
- '| Reason | Files | Size (bytes) |',
479
- '| --- | ---: | ---: |',
564
+ "## 🧬 Duplicate Candidates",
565
+ "| Reason | Files | Size (bytes) |",
566
+ "| --- | ---: | ---: |",
480
567
  );
481
568
  for (const d of stats.duplicateCandidates) {
482
- md.push(`| ${d.reason} | ${d.count} | ${d.size.toLocaleString()} |`);
569
+ md.push(
570
+ `| ${d.reason} | ${d.count} | ${d.size.toLocaleString()} |`,
571
+ );
483
572
  }
484
- md.push('', '### 🧬 Duplicate Groups Details');
573
+ md.push("", "### 🧬 Duplicate Groups Details");
485
574
  let dupIndex = 1;
486
575
  for (const d of stats.duplicateCandidates) {
487
576
  md.push(
@@ -492,51 +581,67 @@ program
492
581
  md.push(`- ${fp}`);
493
582
  }
494
583
  } else {
495
- md.push('- (file list unavailable)');
584
+ md.push("- (file list unavailable)");
496
585
  }
497
- md.push('');
586
+ md.push("");
498
587
  dupIndex++;
499
588
  }
500
- md.push('');
589
+ md.push("");
501
590
  }
502
591
 
503
592
  // Compressibility
504
- if (typeof stats.compressibilityRatio === 'number') {
593
+ if (typeof stats.compressibilityRatio === "number") {
505
594
  md.push(
506
- '## 🗜️ Compressibility',
595
+ "## 🗜️ Compressibility",
507
596
  `Sampled compressibility ratio: ${(stats.compressibilityRatio * 100).toFixed(2)}%`,
508
- '',
597
+ "",
509
598
  );
510
599
  }
511
600
 
512
601
  // Git
513
602
  if (stats.git && stats.git.isRepo) {
514
603
  md.push(
515
- '## 🔧 Git',
604
+ "## 🔧 Git",
516
605
  `- Tracked: ${stats.git.trackedCount} files, ${stats.git.trackedBytes.toLocaleString()} bytes`,
517
606
  `- Untracked: ${stats.git.untrackedCount} files, ${stats.git.untrackedBytes.toLocaleString()} bytes`,
518
607
  );
519
- if (Array.isArray(stats.git.lfsCandidates) && stats.git.lfsCandidates.length > 0) {
520
- md.push('', '### 📦 LFS Candidates (Top 20)', '| Path | Bytes |', '| --- | ---: |');
608
+ if (
609
+ Array.isArray(stats.git.lfsCandidates) &&
610
+ stats.git.lfsCandidates.length > 0
611
+ ) {
612
+ md.push(
613
+ "",
614
+ "### 📦 LFS Candidates (Top 20)",
615
+ "| Path | Bytes |",
616
+ "| --- | ---: |",
617
+ );
521
618
  for (const f of stats.git.lfsCandidates.slice(0, 20)) {
522
619
  md.push(`| ${f.path} | ${f.size.toLocaleString()} |`);
523
620
  }
524
621
  }
525
- md.push('');
622
+ md.push("");
526
623
  }
527
624
 
528
625
  // Largest Files
529
- if (Array.isArray(stats.largestFiles) && stats.largestFiles.length > 0) {
626
+ if (
627
+ Array.isArray(stats.largestFiles) &&
628
+ stats.largestFiles.length > 0
629
+ ) {
530
630
  md.push(
531
- '## 📚 Largest Files (Top 50)',
532
- '| Path | Size | % of total | LOC |',
533
- '| --- | ---: | ---: | ---: |',
631
+ "## 📚 Largest Files (Top 50)",
632
+ "| Path | Size | % of total | LOC |",
633
+ "| --- | ---: | ---: | ---: |",
534
634
  );
535
635
  for (const f of stats.largestFiles) {
536
- let loc = '';
537
- if (!f.isBinary && Array.isArray(aggregatedContent?.textFiles)) {
538
- const tf = aggregatedContent.textFiles.find((t) => t.path === f.path);
539
- if (tf && typeof tf.lines === 'number') {
636
+ let loc = "";
637
+ if (
638
+ !f.isBinary &&
639
+ Array.isArray(aggregatedContent?.textFiles)
640
+ ) {
641
+ const tf = aggregatedContent.textFiles.find(
642
+ (t) => t.path === f.path,
643
+ );
644
+ if (tf && typeof tf.lines === "number") {
540
645
  loc = tf.lines.toLocaleString();
541
646
  }
542
647
  }
@@ -544,10 +649,10 @@ program
544
649
  `| ${f.path} | ${f.sizeFormatted} | ${f.percentOfTotal.toFixed(2)}% | ${loc} |`,
545
650
  );
546
651
  }
547
- md.push('');
652
+ md.push("");
548
653
  }
549
654
 
550
- await fs.writeFile(mdPath, md.join('\n'));
655
+ await fs.writeFile(mdPath, md.join("\n"));
551
656
  console.log(`\n🧾 Detailed stats report written to: ${mdPath}`);
552
657
  } catch (error) {
553
658
  console.warn(`⚠️ Failed to write stats markdown: ${error.message}`);
@@ -555,8 +660,8 @@ program
555
660
  }
556
661
  }
557
662
  } catch (error) {
558
- console.error('❌ Critical error:', error.message);
559
- console.error('An unexpected error occurred.');
663
+ console.error("❌ Critical error:", error.message);
664
+ console.error("An unexpected error occurred.");
560
665
  process.exit(1);
561
666
  }
562
667
  });