@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.
- package/.idea/workspace.xml +27 -26
- package/JAVA-BACKEND-COMMANDS-REFERENCE.md +62 -52
- package/JAVA-BACKEND-ITERATION-GUIDE.md +125 -18
- package/README.md +1 -1
- package/common/utils/bmad-doc-template.md +5 -5
- package/dist/agents/analyst.txt +35 -5
- package/dist/agents/architect.txt +217 -31
- package/dist/agents/automation-orchestrator.txt +4 -4
- package/dist/agents/dev.txt +3 -3
- package/dist/agents/full-requirement-orchestrator.txt +11 -11
- package/dist/agents/qa.txt +102 -102
- package/dist/agents/sm.txt +6 -6
- package/dist/agents/ux-expert.txt +6 -1
- package/dist/agents/workflow-executor.txt +879 -0
- package/dist/agents/xiaoma-master.txt +258 -37
- package/dist/teams/team-all.txt +1223 -445
- package/dist/teams/team-fullstack-with-database.txt +384 -446
- package/dist/teams/team-fullstack.txt +258 -37
- package/dist/teams/team-ide-minimal.txt +111 -111
- package/dist/teams/team-no-ui.txt +252 -36
- package/docs/architecture-sharding-modification.md +623 -0
- package/docs/automated-requirements-analysis-outputs.md +896 -0
- package/package.json +1 -1
- package/tools/builders/web-builder.js +292 -142
- package/tools/bump-all-versions.js +50 -32
- package/tools/cli.js +52 -47
- package/tools/flattener/aggregate.js +30 -12
- package/tools/flattener/binary.js +46 -43
- package/tools/flattener/discovery.js +23 -15
- package/tools/flattener/files.js +6 -6
- package/tools/flattener/ignoreRules.js +122 -121
- package/tools/flattener/main.js +249 -144
- package/tools/flattener/projectRoot.js +74 -69
- package/tools/flattener/prompts.js +12 -10
- package/tools/flattener/stats.helpers.js +90 -61
- package/tools/flattener/stats.js +1 -1
- package/tools/flattener/test-matrix.js +225 -170
- package/tools/flattener/xml.js +31 -23
- package/tools/installer/bin/xiaoma.js +199 -153
- package/tools/installer/lib/config-loader.js +76 -47
- package/tools/installer/lib/file-manager.js +101 -44
- package/tools/installer/lib/ide-base-setup.js +49 -39
- package/tools/installer/lib/ide-setup.js +694 -380
- package/tools/installer/lib/installer.js +802 -469
- package/tools/installer/lib/memory-profiler.js +22 -12
- package/tools/installer/lib/module-manager.js +16 -14
- package/tools/installer/lib/resource-locator.js +61 -35
- package/tools/lib/dependency-resolver.js +34 -23
- package/tools/lib/yaml-utils.js +7 -2
- package/tools/preview-release-notes.js +33 -25
- package/tools/shared/bannerArt.js +3 -3
- package/tools/sync-installer-version.js +16 -7
- package/tools/upgraders/v3-to-v4-upgrader.js +244 -163
- package/tools/version-bump.js +24 -18
- package/tools/xiaoma-npx-wrapper.js +15 -10
- package/tools/yaml-format.js +60 -36
- package/xiaoma-core/agent-teams/team-fullstack-with-database.yaml +0 -1
- package/xiaoma-core/agents/automated-fix-validator.yaml +2 -1
- package/xiaoma-core/agents/automated-quality-validator.yaml +10 -5
- package/xiaoma-core/agents/automation-orchestrator.md +4 -4
- package/xiaoma-core/agents/dev.md +4 -4
- package/xiaoma-core/agents/enhanced-workflow-orchestrator.yaml +2 -1
- package/xiaoma-core/agents/full-requirement-orchestrator.md +11 -11
- package/xiaoma-core/agents/global-requirements-auditor.yaml +11 -3
- package/xiaoma-core/agents/intelligent-template-adapter.yaml +19 -5
- package/xiaoma-core/agents/master-execution-engine.yaml +19 -5
- package/xiaoma-core/agents/workflow-executor.md +126 -18
- package/xiaoma-core/agents/xiaoma-master.md +1 -1
- package/xiaoma-core/data/test-levels-framework.md +12 -12
- package/xiaoma-core/tasks/analyze-existing-database.md +1 -1
- package/xiaoma-core/tasks/apply-qa-fixes.md +3 -3
- package/xiaoma-core/tasks/batch-story-generation.md +22 -22
- package/xiaoma-core/tasks/create-enhanced-story-with-database.md +6 -6
- package/xiaoma-core/tasks/nfr-assess.md +6 -6
- package/xiaoma-core/tasks/project-integration-testing.md +42 -42
- package/xiaoma-core/tasks/qa-gate.md +23 -23
- package/xiaoma-core/tasks/review-story.md +18 -18
- package/xiaoma-core/tasks/risk-profile.md +25 -25
- package/xiaoma-core/tasks/serial-development-orchestration.md +51 -51
- package/xiaoma-core/tasks/test-design.md +9 -9
- package/xiaoma-core/tasks/trace-requirements.md +21 -21
- package/xiaoma-core/templates/competitor-analysis-tmpl.yaml +35 -5
- package/xiaoma-core/templates/front-end-architecture-tmpl.yaml +77 -11
- package/xiaoma-core/templates/front-end-spec-tmpl.yaml +6 -1
- package/xiaoma-core/templates/fullstack-architecture-tmpl.yaml +140 -20
- package/xiaoma-core/templates/global-qa-monitoring-tmpl.yaml +2 -1
- package/xiaoma-core/templates/requirements-coverage-audit.yaml +2 -1
- package/xiaoma-core/workflows/automated-requirements-analysis.yaml +4 -4
- package/dist/agents/database-architect.txt +0 -322
package/tools/flattener/main.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
const { Command } = require(
|
|
2
|
-
const fs = require(
|
|
3
|
-
const path = require(
|
|
4
|
-
const process = require(
|
|
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(
|
|
8
|
-
const { promptYesNo, promptPath } = require(
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
|
|
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(
|
|
71
|
-
.description(
|
|
72
|
-
.version(
|
|
73
|
-
.option(
|
|
74
|
-
.option(
|
|
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 ===
|
|
86
|
+
(a) => a === "-i" || a === "--input" || a.startsWith("--input="),
|
|
83
87
|
);
|
|
84
88
|
const userSpecifiedOutput = argv.some(
|
|
85
|
-
(a) => a ===
|
|
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,
|
|
93
|
-
: path.resolve(
|
|
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(
|
|
108
|
+
inputDir = await promptPath(
|
|
109
|
+
"Enter input directory path",
|
|
110
|
+
process.cwd(),
|
|
111
|
+
);
|
|
105
112
|
outputPath = await promptPath(
|
|
106
|
-
|
|
107
|
-
path.join(inputDir,
|
|
113
|
+
"Enter output file path",
|
|
114
|
+
path.join(inputDir, "flattened-codebase.xml"),
|
|
108
115
|
);
|
|
109
116
|
}
|
|
110
117
|
} else {
|
|
111
|
-
console.log(
|
|
112
|
-
inputDir = await promptPath(
|
|
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
|
-
|
|
115
|
-
path.join(inputDir,
|
|
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(
|
|
141
|
+
const { default: ora } = await import("ora");
|
|
132
142
|
|
|
133
143
|
// Start file discovery with spinner
|
|
134
|
-
const discoverySpinner = ora(
|
|
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(
|
|
147
|
+
discoverySpinner.succeed(
|
|
148
|
+
`📁 Found ${filteredFiles.length} files to include`,
|
|
149
|
+
);
|
|
138
150
|
|
|
139
151
|
// Process files with progress tracking
|
|
140
|
-
console.log(
|
|
141
|
-
const processingSpinner = ora(
|
|
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(
|
|
167
|
+
const xmlSpinner = ora("🔧 Generating XML output...").start();
|
|
156
168
|
await generateXMLOutput(aggregatedContent, outputPath);
|
|
157
|
-
xmlSpinner.succeed(
|
|
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(
|
|
173
|
+
const stats = await calculateStatistics(
|
|
174
|
+
aggregatedContent,
|
|
175
|
+
outputStats.size,
|
|
176
|
+
inputDir,
|
|
177
|
+
);
|
|
162
178
|
|
|
163
179
|
// Display completion summary
|
|
164
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
211
|
+
console.log("\n🧮 Size Histogram:");
|
|
194
212
|
for (const b of stats.histogram.slice(0, 2)) {
|
|
195
|
-
console.log(
|
|
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(
|
|
224
|
+
console.log("\n📦 Top Extensions:");
|
|
205
225
|
for (const e of topExt) {
|
|
206
|
-
const pct = stats.totalBytes
|
|
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(
|
|
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(
|
|
244
|
+
console.log("\n📂 Top Directories:");
|
|
221
245
|
for (const d of topDir) {
|
|
222
|
-
const pct = stats.totalBytes
|
|
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(
|
|
256
|
+
console.log(
|
|
257
|
+
` … and ${stats.byDirectory.length - 2} more directories`,
|
|
258
|
+
);
|
|
231
259
|
}
|
|
232
260
|
}
|
|
233
261
|
|
|
234
|
-
if (
|
|
235
|
-
|
|
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 =
|
|
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 (
|
|
245
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
303
|
+
console.log(" Age buckets:");
|
|
268
304
|
for (const b of stats.temporal.ageBuckets.slice(0, 2)) {
|
|
269
|
-
console.log(
|
|
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(
|
|
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(
|
|
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 (>= ${(
|
|
285
|
-
|
|
286
|
-
|
|
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 (
|
|
294
|
-
|
|
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(
|
|
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(
|
|
345
|
+
console.log(
|
|
346
|
+
` … and ${stats.duplicateCandidates.length - 2} more groups`,
|
|
347
|
+
);
|
|
300
348
|
}
|
|
301
349
|
}
|
|
302
350
|
|
|
303
|
-
if (typeof stats.compressibilityRatio ===
|
|
351
|
+
if (typeof stats.compressibilityRatio === "number") {
|
|
304
352
|
console.log(
|
|
305
|
-
`\n🗜️ Compressibility ratio (sampled): ${(
|
|
306
|
-
|
|
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(
|
|
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 (
|
|
320
|
-
|
|
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(
|
|
376
|
+
console.log(
|
|
377
|
+
` … and ${stats.git.lfsCandidates.length - 2} more`,
|
|
378
|
+
);
|
|
326
379
|
}
|
|
327
380
|
}
|
|
328
381
|
}
|
|
329
382
|
|
|
330
|
-
if (
|
|
331
|
-
|
|
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(
|
|
337
|
-
|
|
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(
|
|
353
|
-
? outputPath.replace(/\.xml$/i,
|
|
354
|
-
: outputPath +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
436
|
+
"## 🧮 Size Histogram",
|
|
437
|
+
"| Bucket | Files | Bytes |",
|
|
438
|
+
"| --- | ---: | ---: |",
|
|
381
439
|
);
|
|
382
440
|
for (const b of stats.histogram) {
|
|
383
|
-
md.push(
|
|
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 (
|
|
449
|
+
if (
|
|
450
|
+
Array.isArray(stats.byExtension) &&
|
|
451
|
+
stats.byExtension.length > 0
|
|
452
|
+
) {
|
|
390
453
|
md.push(
|
|
391
|
-
|
|
392
|
-
|
|
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 (
|
|
468
|
+
if (
|
|
469
|
+
Array.isArray(stats.byDirectory) &&
|
|
470
|
+
stats.byDirectory.length > 0
|
|
471
|
+
) {
|
|
406
472
|
md.push(
|
|
407
|
-
|
|
408
|
-
|
|
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 (
|
|
422
|
-
|
|
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 (
|
|
503
|
+
if (
|
|
504
|
+
Array.isArray(stats.longestPaths) &&
|
|
505
|
+
stats.longestPaths.length > 0
|
|
506
|
+
) {
|
|
431
507
|
md.push(
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
508
|
+
"## 🧵 Longest Paths (Top 25)",
|
|
509
|
+
"| Path | Length | Bytes |",
|
|
510
|
+
"| --- | ---: | ---: |",
|
|
435
511
|
);
|
|
436
512
|
for (const pth of stats.longestPaths) {
|
|
437
|
-
md.push(
|
|
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(
|
|
522
|
+
md.push("## ⏱️ Temporal");
|
|
445
523
|
if (stats.temporal.oldest) {
|
|
446
|
-
md.push(
|
|
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(
|
|
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(
|
|
534
|
+
md.push("", "| Age | Files | Bytes |", "| --- | ---: | ---: |");
|
|
453
535
|
for (const b of stats.temporal.ageBuckets) {
|
|
454
|
-
md.push(
|
|
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
|
-
|
|
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 (
|
|
559
|
+
if (
|
|
560
|
+
Array.isArray(stats.duplicateCandidates) &&
|
|
561
|
+
stats.duplicateCandidates.length > 0
|
|
562
|
+
) {
|
|
476
563
|
md.push(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
564
|
+
"## 🧬 Duplicate Candidates",
|
|
565
|
+
"| Reason | Files | Size (bytes) |",
|
|
566
|
+
"| --- | ---: | ---: |",
|
|
480
567
|
);
|
|
481
568
|
for (const d of stats.duplicateCandidates) {
|
|
482
|
-
md.push(
|
|
569
|
+
md.push(
|
|
570
|
+
`| ${d.reason} | ${d.count} | ${d.size.toLocaleString()} |`,
|
|
571
|
+
);
|
|
483
572
|
}
|
|
484
|
-
md.push(
|
|
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(
|
|
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 ===
|
|
593
|
+
if (typeof stats.compressibilityRatio === "number") {
|
|
505
594
|
md.push(
|
|
506
|
-
|
|
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
|
-
|
|
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 (
|
|
520
|
-
|
|
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 (
|
|
626
|
+
if (
|
|
627
|
+
Array.isArray(stats.largestFiles) &&
|
|
628
|
+
stats.largestFiles.length > 0
|
|
629
|
+
) {
|
|
530
630
|
md.push(
|
|
531
|
-
|
|
532
|
-
|
|
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 (
|
|
538
|
-
|
|
539
|
-
|
|
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(
|
|
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(
|
|
559
|
-
console.error(
|
|
663
|
+
console.error("❌ Critical error:", error.message);
|
|
664
|
+
console.error("An unexpected error occurred.");
|
|
560
665
|
process.exit(1);
|
|
561
666
|
}
|
|
562
667
|
});
|