bmad-method 6.0.0-Beta.5 → 6.0.0-Beta.7

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 (119) hide show
  1. package/.claude/skills/changelog-social/SKILL.md +178 -0
  2. package/.claude/skills/changelog-social/examples/discord-example.md +53 -0
  3. package/.claude/skills/changelog-social/examples/linkedin-example.md +49 -0
  4. package/.claude/skills/changelog-social/examples/twitter-example.md +55 -0
  5. package/.claude/skills/changelog-social.skill +0 -0
  6. package/.claude/skills/draft-changelog/SKILL.md +2 -2
  7. package/.claude/skills/draft-changelog/prompts/instructions.md +25 -1
  8. package/.claude/skills/gh-triage/SKILL.md +1 -1
  9. package/.claude/skills/release-module/SKILL.md +1 -1
  10. package/.claude/skills/release-module/prompts/instructions.md +0 -4
  11. package/.github/workflows/quality.yaml +3 -0
  12. package/.vscode/settings.json +1 -2
  13. package/CHANGELOG.md +65 -1
  14. package/docs/bmgd/game-types.md +1 -0
  15. package/docs/bmgd/index.md +1 -1
  16. package/docs/bmgd/quick-flow-workflows.md +1 -0
  17. package/docs/reference/agents.md +22 -0
  18. package/docs/reference/commands.md +34 -0
  19. package/docs/reference/testing.md +21 -0
  20. package/docs/reference/workflow-map.md +1 -1
  21. package/package.json +5 -2
  22. package/src/bmm/agents/analyst.agent.yaml +11 -5
  23. package/src/bmm/agents/dev.agent.yaml +0 -1
  24. package/src/bmm/agents/pm.agent.yaml +3 -6
  25. package/src/bmm/agents/{quinn.agent.yaml → qa.agent.yaml} +3 -3
  26. package/src/bmm/module-help.csv +13 -20
  27. package/src/bmm/workflows/1-analysis/create-product-brief/workflow.md +0 -1
  28. package/src/bmm/workflows/1-analysis/research/workflow-domain-research.md +54 -0
  29. package/src/bmm/workflows/1-analysis/research/workflow-market-research.md +54 -0
  30. package/src/bmm/workflows/1-analysis/research/workflow-technical-research.md +54 -0
  31. package/src/bmm/workflows/2-plan-workflows/create-prd/data/domain-complexity.csv +2 -0
  32. package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md +63 -0
  33. package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md +65 -0
  34. package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md +65 -0
  35. package/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md +0 -1
  36. package/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md +0 -1
  37. package/src/bmm/workflows/3-solutioning/create-architecture/data/domain-complexity.csv +2 -0
  38. package/src/bmm/workflows/3-solutioning/create-architecture/workflow.md +0 -1
  39. package/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md +0 -1
  40. package/src/bmm/workflows/4-implementation/code-review/workflow.yaml +0 -3
  41. package/src/bmm/workflows/4-implementation/correct-course/workflow.yaml +0 -4
  42. package/src/bmm/workflows/4-implementation/create-story/workflow.yaml +0 -4
  43. package/src/bmm/workflows/4-implementation/dev-story/workflow.yaml +0 -4
  44. package/src/bmm/workflows/4-implementation/retrospective/workflow.yaml +0 -3
  45. package/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml +0 -4
  46. package/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml +0 -6
  47. package/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md +0 -1
  48. package/src/bmm/workflows/document-project/workflow.yaml +0 -8
  49. package/src/bmm/workflows/qa/automate/workflow.yaml +0 -2
  50. package/src/core/tasks/editorial-review-prose.xml +14 -12
  51. package/src/core/tasks/editorial-review-structure.xml +28 -28
  52. package/src/core/tasks/help.md +36 -16
  53. package/src/core/tasks/index-docs.xml +1 -1
  54. package/src/core/tasks/review-adversarial-general.xml +2 -2
  55. package/src/core/tasks/shard-doc.xml +1 -2
  56. package/src/core/tasks/workflow.xml +1 -1
  57. package/src/core/workflows/advanced-elicitation/workflow.xml +1 -1
  58. package/test/test-installation-components.js +1 -1
  59. package/tools/build-docs.js +21 -1
  60. package/tools/cli/bmad-cli.js +42 -3
  61. package/tools/cli/installers/lib/core/config-collector.js +5 -1
  62. package/tools/cli/installers/lib/core/dependency-resolver.js +1 -1
  63. package/tools/cli/installers/lib/core/installer.js +1 -6
  64. package/tools/cli/installers/lib/core/manifest-generator.js +149 -125
  65. package/tools/cli/installers/lib/custom/handler.js +1 -1
  66. package/tools/cli/installers/lib/ide/_base-ide.js +25 -15
  67. package/tools/cli/installers/lib/ide/_config-driven.js +79 -9
  68. package/tools/cli/installers/lib/ide/codex.js +7 -1
  69. package/tools/cli/installers/lib/ide/manager.js +8 -2
  70. package/tools/cli/installers/lib/ide/platform-codes.yaml +0 -3
  71. package/tools/cli/installers/lib/ide/shared/agent-command-generator.js +2 -2
  72. package/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +14 -3
  73. package/tools/cli/installers/lib/ide/shared/path-utils.js +9 -2
  74. package/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +126 -29
  75. package/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +2 -2
  76. package/tools/cli/installers/lib/ide/templates/combined/default-task.md +10 -0
  77. package/tools/cli/installers/lib/ide/templates/combined/default-tool.md +10 -0
  78. package/tools/cli/installers/lib/ide/templates/combined/gemini-task.toml +11 -0
  79. package/tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml +11 -0
  80. package/tools/cli/installers/lib/modules/manager.js +3 -6
  81. package/tools/cli/lib/agent/installer.js +1 -1
  82. package/tools/cli/lib/prompts.js +192 -6
  83. package/tools/cli/lib/ui.js +124 -72
  84. package/tools/schema/agent.js +0 -1
  85. package/tools/validate-file-refs.js +480 -0
  86. package/website/astro.config.mjs +1 -35
  87. package/src/bmm/workflows/1-analysis/research/workflow.md +0 -173
  88. package/src/bmm/workflows/2-plan-workflows/create-prd/validation-report-prd-workflow.md +0 -433
  89. package/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md +0 -150
  90. package/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-library.json +0 -90
  91. package/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-templates.yaml +0 -127
  92. package/src/bmm/workflows/excalidraw-diagrams/create-dataflow/checklist.md +0 -39
  93. package/src/bmm/workflows/excalidraw-diagrams/create-dataflow/instructions.md +0 -130
  94. package/src/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml +0 -27
  95. package/src/bmm/workflows/excalidraw-diagrams/create-diagram/checklist.md +0 -43
  96. package/src/bmm/workflows/excalidraw-diagrams/create-diagram/instructions.md +0 -141
  97. package/src/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml +0 -27
  98. package/src/bmm/workflows/excalidraw-diagrams/create-flowchart/checklist.md +0 -49
  99. package/src/bmm/workflows/excalidraw-diagrams/create-flowchart/instructions.md +0 -241
  100. package/src/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml +0 -27
  101. package/src/bmm/workflows/excalidraw-diagrams/create-wireframe/checklist.md +0 -38
  102. package/src/bmm/workflows/excalidraw-diagrams/create-wireframe/instructions.md +0 -133
  103. package/src/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml +0 -27
  104. package/src/core/resources/excalidraw/README.md +0 -160
  105. package/src/core/resources/excalidraw/excalidraw-helpers.md +0 -127
  106. package/src/core/resources/excalidraw/library-loader.md +0 -50
  107. package/src/core/resources/excalidraw/validate-json-instructions.md +0 -79
  108. package/tools/flattener/aggregate.js +0 -76
  109. package/tools/flattener/binary.js +0 -80
  110. package/tools/flattener/discovery.js +0 -71
  111. package/tools/flattener/files.js +0 -35
  112. package/tools/flattener/ignoreRules.js +0 -172
  113. package/tools/flattener/main.js +0 -483
  114. package/tools/flattener/projectRoot.js +0 -201
  115. package/tools/flattener/prompts.js +0 -44
  116. package/tools/flattener/stats.helpers.js +0 -368
  117. package/tools/flattener/stats.js +0 -75
  118. package/tools/flattener/test-matrix.js +0 -409
  119. package/tools/flattener/xml.js +0 -82
@@ -1,483 +0,0 @@
1
- const { Command } = require('commander');
2
- const fs = require('fs-extra');
3
- const path = require('node:path');
4
- const process = require('node:process');
5
-
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');
12
-
13
- /**
14
- * Recursively discover all files in a directory
15
- * @param {string} rootDir - The root directory to scan
16
- * @returns {Promise<string[]>} Array of file paths
17
- */
18
-
19
- /**
20
- * Parse .gitignore file and return ignore patterns
21
- * @param {string} gitignorePath - Path to .gitignore file
22
- * @returns {Promise<string[]>} Array of ignore patterns
23
- */
24
-
25
- /**
26
- * Check if a file is binary using file command and heuristics
27
- * @param {string} filePath - Path to the file
28
- * @returns {Promise<boolean>} True if file is binary
29
- */
30
-
31
- /**
32
- * Read and aggregate content from text files
33
- * @param {string[]} files - Array of file paths
34
- * @param {string} rootDir - The root directory
35
- * @param {Object} spinner - Optional spinner instance for progress display
36
- * @returns {Promise<Object>} Object containing file contents and metadata
37
- */
38
-
39
- /**
40
- * Generate XML output with aggregated file contents using streaming
41
- * @param {Object} aggregatedContent - The aggregated content object
42
- * @param {string} outputPath - The output file path
43
- * @returns {Promise<void>} Promise that resolves when writing is complete
44
- */
45
-
46
- /**
47
- * Calculate statistics for the processed files
48
- * @param {Object} aggregatedContent - The aggregated content object
49
- * @param {number} xmlFileSize - The size of the generated XML file in bytes
50
- * @returns {Object} Statistics object
51
- */
52
-
53
- /**
54
- * Filter files based on .gitignore patterns
55
- * @param {string[]} files - Array of file paths
56
- * @param {string} rootDir - The root directory
57
- * @returns {Promise<string[]>} Filtered array of file paths
58
- */
59
-
60
- /**
61
- * Attempt to find the project root by walking up from startDir
62
- * Looks for common project markers like .git, package.json, pyproject.toml, etc.
63
- * @param {string} startDir
64
- * @returns {Promise<string|null>} project root directory or null if not found
65
- */
66
-
67
- const program = new Command();
68
-
69
- program
70
- .name('bmad-flatten')
71
- .description('BMad-Method 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')
75
- .action(async (options) => {
76
- let inputDir = path.resolve(options.input);
77
- let outputPath = path.resolve(options.output);
78
-
79
- // Detect if user explicitly provided -i/--input or -o/--output
80
- const argv = process.argv.slice(2);
81
- const userSpecifiedInput = argv.some((a) => a === '-i' || a === '--input' || a.startsWith('--input='));
82
- const userSpecifiedOutput = argv.some((a) => a === '-o' || a === '--output' || a.startsWith('--output='));
83
- const noPathArguments = !userSpecifiedInput && !userSpecifiedOutput;
84
-
85
- if (noPathArguments) {
86
- const detectedRoot = await findProjectRoot(process.cwd());
87
- const suggestedOutput = detectedRoot ? path.join(detectedRoot, 'flattened-codebase.xml') : path.resolve('flattened-codebase.xml');
88
-
89
- if (detectedRoot) {
90
- const useDefaults = await promptYesNo(
91
- `Detected project root at "${detectedRoot}". Use it as input and write output to "${suggestedOutput}"?`,
92
- true,
93
- );
94
- if (useDefaults) {
95
- inputDir = detectedRoot;
96
- outputPath = suggestedOutput;
97
- } else {
98
- inputDir = await promptPath('Enter input directory path', process.cwd());
99
- outputPath = await promptPath('Enter output file path', path.join(inputDir, 'flattened-codebase.xml'));
100
- }
101
- } else {
102
- console.log('Could not auto-detect a project root.');
103
- inputDir = await promptPath('Enter input directory path', process.cwd());
104
- outputPath = await promptPath('Enter output file path', path.join(inputDir, 'flattened-codebase.xml'));
105
- }
106
- }
107
-
108
- // Ensure output directory exists
109
- await fs.ensureDir(path.dirname(outputPath));
110
-
111
- try {
112
- // Verify input directory exists
113
- if (!(await fs.pathExists(inputDir))) {
114
- console.error(`❌ Error: Input directory does not exist: ${inputDir}`);
115
- process.exit(1);
116
- }
117
-
118
- // Import ora dynamically
119
- const { default: ora } = await import('ora');
120
-
121
- // Start file discovery with spinner
122
- const discoverySpinner = ora('🔍 Discovering files...').start();
123
- const files = await discoverFiles(inputDir);
124
- const filteredFiles = await filterFiles(files, inputDir);
125
- discoverySpinner.succeed(`📁 Found ${filteredFiles.length} files to include`);
126
-
127
- // Process files with progress tracking
128
- console.log('Reading file contents');
129
- const processingSpinner = ora('📄 Processing files...').start();
130
- const aggregatedContent = await aggregateFileContents(filteredFiles, inputDir, processingSpinner);
131
- processingSpinner.succeed(`✅ Processed ${aggregatedContent.processedFiles}/${filteredFiles.length} files`);
132
- if (aggregatedContent.errors.length > 0) {
133
- console.log(`Errors: ${aggregatedContent.errors.length}`);
134
- }
135
-
136
- // Generate XML output using streaming
137
- const xmlSpinner = ora('🔧 Generating XML output...').start();
138
- await generateXMLOutput(aggregatedContent, outputPath);
139
- xmlSpinner.succeed('📝 XML generation completed');
140
-
141
- // Calculate and display statistics
142
- const outputStats = await fs.stat(outputPath);
143
- const stats = await calculateStatistics(aggregatedContent, outputStats.size, inputDir);
144
-
145
- // Display completion summary
146
- console.log('\n📊 Completion Summary:');
147
- console.log(`✅ Successfully processed ${filteredFiles.length} files into ${path.basename(outputPath)}`);
148
- console.log(`📁 Output file: ${outputPath}`);
149
- console.log(`📏 Total source size: ${stats.totalSize}`);
150
- console.log(`📄 Generated XML size: ${stats.xmlSize}`);
151
- console.log(`📝 Total lines of code: ${stats.totalLines.toLocaleString()}`);
152
- console.log(`🔢 Estimated tokens: ${stats.estimatedTokens}`);
153
- console.log(`📊 File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors\n`);
154
-
155
- // Ask user if they want detailed stats + markdown report
156
- const generateDetailed = await promptYesNo('Generate detailed stats (console + markdown) now?', true);
157
-
158
- if (generateDetailed) {
159
- // Additional detailed stats
160
- console.log('\n📈 Size Percentiles:');
161
- console.log(
162
- ` Avg: ${Math.round(stats.avgFileSize).toLocaleString()} B, Median: ${Math.round(
163
- stats.medianFileSize,
164
- ).toLocaleString()} B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`,
165
- );
166
-
167
- if (Array.isArray(stats.histogram) && stats.histogram.length > 0) {
168
- console.log('\n🧮 Size Histogram:');
169
- for (const b of stats.histogram.slice(0, 2)) {
170
- console.log(` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`);
171
- }
172
- if (stats.histogram.length > 2) {
173
- console.log(` … and ${stats.histogram.length - 2} more buckets`);
174
- }
175
- }
176
-
177
- if (Array.isArray(stats.byExtension) && stats.byExtension.length > 0) {
178
- const topExt = stats.byExtension.slice(0, 2);
179
- console.log('\n📦 Top Extensions:');
180
- for (const e of topExt) {
181
- const pct = stats.totalBytes ? (e.bytes / stats.totalBytes) * 100 : 0;
182
- console.log(` ${e.ext}: ${e.count} files, ${e.bytes.toLocaleString()} bytes (${pct.toFixed(2)}%)`);
183
- }
184
- if (stats.byExtension.length > 2) {
185
- console.log(` … and ${stats.byExtension.length - 2} more extensions`);
186
- }
187
- }
188
-
189
- if (Array.isArray(stats.byDirectory) && stats.byDirectory.length > 0) {
190
- const topDir = stats.byDirectory.slice(0, 2);
191
- console.log('\n📂 Top Directories:');
192
- for (const d of topDir) {
193
- const pct = stats.totalBytes ? (d.bytes / stats.totalBytes) * 100 : 0;
194
- console.log(` ${d.dir}: ${d.count} files, ${d.bytes.toLocaleString()} bytes (${pct.toFixed(2)}%)`);
195
- }
196
- if (stats.byDirectory.length > 2) {
197
- console.log(` … and ${stats.byDirectory.length - 2} more directories`);
198
- }
199
- }
200
-
201
- if (Array.isArray(stats.depthDistribution) && stats.depthDistribution.length > 0) {
202
- console.log('\n🌳 Depth Distribution:');
203
- const dd = stats.depthDistribution.slice(0, 2);
204
- let line = ' ' + dd.map((d) => `${d.depth}:${d.count}`).join(' ');
205
- if (stats.depthDistribution.length > 2) {
206
- line += ` … +${stats.depthDistribution.length - 2} more`;
207
- }
208
- console.log(line);
209
- }
210
-
211
- if (Array.isArray(stats.longestPaths) && stats.longestPaths.length > 0) {
212
- console.log('\n🧵 Longest Paths:');
213
- for (const p of stats.longestPaths.slice(0, 2)) {
214
- console.log(` ${p.path} (${p.length} chars, ${p.size.toLocaleString()} bytes)`);
215
- }
216
- if (stats.longestPaths.length > 2) {
217
- console.log(` … and ${stats.longestPaths.length - 2} more paths`);
218
- }
219
- }
220
-
221
- if (stats.temporal) {
222
- console.log('\n⏱️ Temporal:');
223
- if (stats.temporal.oldest) {
224
- console.log(` Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`);
225
- }
226
- if (stats.temporal.newest) {
227
- console.log(` Newest: ${stats.temporal.newest.path} (${stats.temporal.newest.mtime})`);
228
- }
229
- if (Array.isArray(stats.temporal.ageBuckets)) {
230
- console.log(' Age buckets:');
231
- for (const b of stats.temporal.ageBuckets.slice(0, 2)) {
232
- console.log(` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`);
233
- }
234
- if (stats.temporal.ageBuckets.length > 2) {
235
- console.log(` … and ${stats.temporal.ageBuckets.length - 2} more buckets`);
236
- }
237
- }
238
- }
239
-
240
- if (stats.quality) {
241
- console.log('\n✅ Quality Signals:');
242
- console.log(` Zero-byte files: ${stats.quality.zeroByteFiles}`);
243
- console.log(` Empty text files: ${stats.quality.emptyTextFiles}`);
244
- console.log(` Hidden files: ${stats.quality.hiddenFiles}`);
245
- console.log(` Symlinks: ${stats.quality.symlinks}`);
246
- console.log(
247
- ` Large files (>= ${(stats.quality.largeThreshold / (1024 * 1024)).toFixed(0)} MB): ${stats.quality.largeFilesCount}`,
248
- );
249
- console.log(` Suspiciously large files (>= 100 MB): ${stats.quality.suspiciousLargeFilesCount}`);
250
- }
251
-
252
- if (Array.isArray(stats.duplicateCandidates) && stats.duplicateCandidates.length > 0) {
253
- console.log('\n🧬 Duplicate Candidates:');
254
- for (const d of stats.duplicateCandidates.slice(0, 2)) {
255
- console.log(` ${d.reason}: ${d.count} files @ ${d.size.toLocaleString()} bytes`);
256
- }
257
- if (stats.duplicateCandidates.length > 2) {
258
- console.log(` … and ${stats.duplicateCandidates.length - 2} more groups`);
259
- }
260
- }
261
-
262
- if (typeof stats.compressibilityRatio === 'number') {
263
- console.log(`\n🗜️ Compressibility ratio (sampled): ${(stats.compressibilityRatio * 100).toFixed(2)}%`);
264
- }
265
-
266
- if (stats.git && stats.git.isRepo) {
267
- console.log('\n🔧 Git:');
268
- console.log(` Tracked: ${stats.git.trackedCount} files, ${stats.git.trackedBytes.toLocaleString()} bytes`);
269
- console.log(` Untracked: ${stats.git.untrackedCount} files, ${stats.git.untrackedBytes.toLocaleString()} bytes`);
270
- if (Array.isArray(stats.git.lfsCandidates) && stats.git.lfsCandidates.length > 0) {
271
- console.log(' LFS candidates (top 2):');
272
- for (const f of stats.git.lfsCandidates.slice(0, 2)) {
273
- console.log(` ${f.path} (${f.size.toLocaleString()} bytes)`);
274
- }
275
- if (stats.git.lfsCandidates.length > 2) {
276
- console.log(` … and ${stats.git.lfsCandidates.length - 2} more`);
277
- }
278
- }
279
- }
280
-
281
- if (Array.isArray(stats.largestFiles) && stats.largestFiles.length > 0) {
282
- console.log('\n📚 Largest Files (top 2):');
283
- for (const f of stats.largestFiles.slice(0, 2)) {
284
- // Show LOC for text files when available; omit ext and mtime
285
- let locStr = '';
286
- if (!f.isBinary && Array.isArray(aggregatedContent?.textFiles)) {
287
- const tf = aggregatedContent.textFiles.find((t) => t.path === f.path);
288
- if (tf && typeof tf.lines === 'number') {
289
- locStr = `, LOC: ${tf.lines.toLocaleString()}`;
290
- }
291
- }
292
- console.log(` ${f.path} – ${f.sizeFormatted} (${f.percentOfTotal.toFixed(2)}%)${locStr}`);
293
- }
294
- if (stats.largestFiles.length > 2) {
295
- console.log(` … and ${stats.largestFiles.length - 2} more files`);
296
- }
297
- }
298
-
299
- // Write a comprehensive markdown report next to the XML
300
- {
301
- const mdPath = outputPath.endsWith('.xml') ? outputPath.replace(/\.xml$/i, '.stats.md') : outputPath + '.stats.md';
302
- try {
303
- const pct = (num, den) => (den ? (num / den) * 100 : 0);
304
- const md = [];
305
- md.push(
306
- `# 🧾 Flatten Stats for ${path.basename(outputPath)}`,
307
- '',
308
- '## 📊 Summary',
309
- `- Total source size: ${stats.totalSize}`,
310
- `- Generated XML size: ${stats.xmlSize}`,
311
- `- Total lines of code: ${stats.totalLines.toLocaleString()}`,
312
- `- Estimated tokens: ${stats.estimatedTokens}`,
313
- `- File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`,
314
- '',
315
- '## 📈 Size Percentiles',
316
- `Avg: ${Math.round(stats.avgFileSize).toLocaleString()} B, Median: ${Math.round(
317
- stats.medianFileSize,
318
- ).toLocaleString()} B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`,
319
- '',
320
- );
321
-
322
- // Histogram
323
- if (Array.isArray(stats.histogram) && stats.histogram.length > 0) {
324
- md.push('## 🧮 Size Histogram', '| Bucket | Files | Bytes |', '| --- | ---: | ---: |');
325
- for (const b of stats.histogram) {
326
- md.push(`| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`);
327
- }
328
- md.push('');
329
- }
330
-
331
- // Top Extensions
332
- if (Array.isArray(stats.byExtension) && stats.byExtension.length > 0) {
333
- md.push('## 📦 Top Extensions by Bytes (Top 20)', '| Ext | Files | Bytes | % of total |', '| --- | ---: | ---: | ---: |');
334
- for (const e of stats.byExtension.slice(0, 20)) {
335
- const p = pct(e.bytes, stats.totalBytes);
336
- md.push(`| ${e.ext} | ${e.count} | ${e.bytes.toLocaleString()} | ${p.toFixed(2)}% |`);
337
- }
338
- md.push('');
339
- }
340
-
341
- // Top Directories
342
- if (Array.isArray(stats.byDirectory) && stats.byDirectory.length > 0) {
343
- md.push(
344
- '## 📂 Top Directories by Bytes (Top 20)',
345
- '| Directory | Files | Bytes | % of total |',
346
- '| --- | ---: | ---: | ---: |',
347
- );
348
- for (const d of stats.byDirectory.slice(0, 20)) {
349
- const p = pct(d.bytes, stats.totalBytes);
350
- md.push(`| ${d.dir} | ${d.count} | ${d.bytes.toLocaleString()} | ${p.toFixed(2)}% |`);
351
- }
352
- md.push('');
353
- }
354
-
355
- // Depth distribution
356
- if (Array.isArray(stats.depthDistribution) && stats.depthDistribution.length > 0) {
357
- md.push('## 🌳 Depth Distribution', '| Depth | Count |', '| ---: | ---: |');
358
- for (const d of stats.depthDistribution) {
359
- md.push(`| ${d.depth} | ${d.count} |`);
360
- }
361
- md.push('');
362
- }
363
-
364
- // Longest paths
365
- if (Array.isArray(stats.longestPaths) && stats.longestPaths.length > 0) {
366
- md.push('## 🧵 Longest Paths (Top 25)', '| Path | Length | Bytes |', '| --- | ---: | ---: |');
367
- for (const pth of stats.longestPaths) {
368
- md.push(`| ${pth.path} | ${pth.length} | ${pth.size.toLocaleString()} |`);
369
- }
370
- md.push('');
371
- }
372
-
373
- // Temporal
374
- if (stats.temporal) {
375
- md.push('## ⏱️ Temporal');
376
- if (stats.temporal.oldest) {
377
- md.push(`- Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`);
378
- }
379
- if (stats.temporal.newest) {
380
- md.push(`- Newest: ${stats.temporal.newest.path} (${stats.temporal.newest.mtime})`);
381
- }
382
- if (Array.isArray(stats.temporal.ageBuckets)) {
383
- md.push('', '| Age | Files | Bytes |', '| --- | ---: | ---: |');
384
- for (const b of stats.temporal.ageBuckets) {
385
- md.push(`| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`);
386
- }
387
- }
388
- md.push('');
389
- }
390
-
391
- // Quality signals
392
- if (stats.quality) {
393
- md.push(
394
- '## ✅ Quality Signals',
395
- `- Zero-byte files: ${stats.quality.zeroByteFiles}`,
396
- `- Empty text files: ${stats.quality.emptyTextFiles}`,
397
- `- Hidden files: ${stats.quality.hiddenFiles}`,
398
- `- Symlinks: ${stats.quality.symlinks}`,
399
- `- Large files (>= ${(stats.quality.largeThreshold / (1024 * 1024)).toFixed(0)} MB): ${stats.quality.largeFilesCount}`,
400
- `- Suspiciously large files (>= 100 MB): ${stats.quality.suspiciousLargeFilesCount}`,
401
- '',
402
- );
403
- }
404
-
405
- // Duplicates
406
- if (Array.isArray(stats.duplicateCandidates) && stats.duplicateCandidates.length > 0) {
407
- md.push('## 🧬 Duplicate Candidates', '| Reason | Files | Size (bytes) |', '| --- | ---: | ---: |');
408
- for (const d of stats.duplicateCandidates) {
409
- md.push(`| ${d.reason} | ${d.count} | ${d.size.toLocaleString()} |`);
410
- }
411
- md.push('', '### 🧬 Duplicate Groups Details');
412
- let dupIndex = 1;
413
- for (const d of stats.duplicateCandidates) {
414
- md.push(`#### Group ${dupIndex}: ${d.count} files @ ${d.size.toLocaleString()} bytes (${d.reason})`);
415
- if (Array.isArray(d.files) && d.files.length > 0) {
416
- for (const fp of d.files) {
417
- md.push(`- ${fp}`);
418
- }
419
- } else {
420
- md.push('- (file list unavailable)');
421
- }
422
- md.push('');
423
- dupIndex++;
424
- }
425
- md.push('');
426
- }
427
-
428
- // Compressibility
429
- if (typeof stats.compressibilityRatio === 'number') {
430
- md.push('## 🗜️ Compressibility', `Sampled compressibility ratio: ${(stats.compressibilityRatio * 100).toFixed(2)}%`, '');
431
- }
432
-
433
- // Git
434
- if (stats.git && stats.git.isRepo) {
435
- md.push(
436
- '## 🔧 Git',
437
- `- Tracked: ${stats.git.trackedCount} files, ${stats.git.trackedBytes.toLocaleString()} bytes`,
438
- `- Untracked: ${stats.git.untrackedCount} files, ${stats.git.untrackedBytes.toLocaleString()} bytes`,
439
- );
440
- if (Array.isArray(stats.git.lfsCandidates) && stats.git.lfsCandidates.length > 0) {
441
- md.push('', '### 📦 LFS Candidates (Top 20)', '| Path | Bytes |', '| --- | ---: |');
442
- for (const f of stats.git.lfsCandidates.slice(0, 20)) {
443
- md.push(`| ${f.path} | ${f.size.toLocaleString()} |`);
444
- }
445
- }
446
- md.push('');
447
- }
448
-
449
- // Largest Files
450
- if (Array.isArray(stats.largestFiles) && stats.largestFiles.length > 0) {
451
- md.push('## 📚 Largest Files (Top 50)', '| Path | Size | % of total | LOC |', '| --- | ---: | ---: | ---: |');
452
- for (const f of stats.largestFiles) {
453
- let loc = '';
454
- if (!f.isBinary && Array.isArray(aggregatedContent?.textFiles)) {
455
- const tf = aggregatedContent.textFiles.find((t) => t.path === f.path);
456
- if (tf && typeof tf.lines === 'number') {
457
- loc = tf.lines.toLocaleString();
458
- }
459
- }
460
- md.push(`| ${f.path} | ${f.sizeFormatted} | ${f.percentOfTotal.toFixed(2)}% | ${loc} |`);
461
- }
462
- md.push('');
463
- }
464
-
465
- await fs.writeFile(mdPath, md.join('\n'));
466
- console.log(`\n🧾 Detailed stats report written to: ${mdPath}`);
467
- } catch (error) {
468
- console.warn(`⚠️ Failed to write stats markdown: ${error.message}`);
469
- }
470
- }
471
- }
472
- } catch (error) {
473
- console.error('❌ Critical error:', error.message);
474
- console.error('An unexpected error occurred.');
475
- process.exit(1);
476
- }
477
- });
478
-
479
- if (require.main === module) {
480
- program.parse();
481
- }
482
-
483
- module.exports = program;
@@ -1,201 +0,0 @@
1
- const fs = require('fs-extra');
2
- const path = require('node:path');
3
-
4
- // Deno/Node compatibility: explicitly import process
5
- const process = require('node:process');
6
- const { execFile } = require('node:child_process');
7
- const { promisify } = require('node:util');
8
- const execFileAsync = promisify(execFile);
9
-
10
- // Simple memoization across calls (keyed by realpath of startDir)
11
- const _cache = new Map();
12
-
13
- async function _tryRun(cmd, args, cwd, timeoutMs = 500) {
14
- try {
15
- const { stdout } = await execFileAsync(cmd, args, {
16
- cwd,
17
- timeout: timeoutMs,
18
- windowsHide: true,
19
- maxBuffer: 1024 * 1024,
20
- });
21
- const out = String(stdout || '').trim();
22
- return out || null;
23
- } catch {
24
- return null;
25
- }
26
- }
27
-
28
- async function _detectVcsTopLevel(startDir) {
29
- // Run common VCS root queries in parallel; ignore failures
30
- const gitP = _tryRun('git', ['rev-parse', '--show-toplevel'], startDir);
31
- const hgP = _tryRun('hg', ['root'], startDir);
32
- const svnP = (async () => {
33
- const show = await _tryRun('svn', ['info', '--show-item', 'wc-root'], startDir);
34
- if (show) return show;
35
- const info = await _tryRun('svn', ['info'], startDir);
36
- if (info) {
37
- const line = info.split(/\r?\n/).find((l) => l.toLowerCase().startsWith('working copy root path:'));
38
- if (line) return line.split(':').slice(1).join(':').trim();
39
- }
40
- return null;
41
- })();
42
- const [git, hg, svn] = await Promise.all([gitP, hgP, svnP]);
43
- return git || hg || svn || null;
44
- }
45
-
46
- /**
47
- * Attempt to find the project root by walking up from startDir.
48
- * Uses a robust, prioritized set of ecosystem markers (VCS > workspaces/monorepo > lock/build > language config).
49
- * Also recognizes package.json with "workspaces" as a workspace root.
50
- * You can augment markers via env PROJECT_ROOT_MARKERS as a comma-separated list of file/dir names.
51
- * @param {string} startDir
52
- * @returns {Promise<string|null>} project root directory or null if not found
53
- */
54
- async function findProjectRoot(startDir) {
55
- try {
56
- // Resolve symlinks for robustness (e.g., when invoked from a symlinked path)
57
- let dir = path.resolve(startDir);
58
- try {
59
- dir = await fs.realpath(dir);
60
- } catch {
61
- // ignore if realpath fails; continue with resolved path
62
- }
63
- const startKey = dir; // preserve starting point for caching
64
- if (_cache.has(startKey)) return _cache.get(startKey);
65
- const fsRoot = path.parse(dir).root;
66
-
67
- // Helper to safely check for existence
68
- const exists = (p) => fs.pathExists(p);
69
-
70
- // Build checks: an array of { makePath: (dir) => string, weight }
71
- const checks = [];
72
-
73
- const add = (rel, weight) => {
74
- const makePath = (d) => (Array.isArray(rel) ? path.join(d, ...rel) : path.join(d, rel));
75
- checks.push({ makePath, weight });
76
- };
77
-
78
- // Highest priority: explicit sentinel markers
79
- add('.project-root', 110);
80
- add('.workspace-root', 110);
81
- add('.repo-root', 110);
82
-
83
- // Highest priority: VCS roots
84
- add('.git', 100);
85
- add('.hg', 95);
86
- add('.svn', 95);
87
-
88
- // Monorepo/workspace indicators
89
- add('pnpm-workspace.yaml', 90);
90
- add('lerna.json', 90);
91
- add('turbo.json', 90);
92
- add('nx.json', 90);
93
- add('rush.json', 90);
94
- add('go.work', 90);
95
- add('WORKSPACE', 90);
96
- add('WORKSPACE.bazel', 90);
97
- add('MODULE.bazel', 90);
98
- add('pants.toml', 90);
99
-
100
- // Lockfiles and package-manager/top-level locks
101
- add('yarn.lock', 85);
102
- add('pnpm-lock.yaml', 85);
103
- add('package-lock.json', 85);
104
- add('bun.lockb', 85);
105
- add('Cargo.lock', 85);
106
- add('composer.lock', 85);
107
- add('poetry.lock', 85);
108
- add('Pipfile.lock', 85);
109
- add('Gemfile.lock', 85);
110
-
111
- // Build-system root indicators
112
- add('settings.gradle', 80);
113
- add('settings.gradle.kts', 80);
114
- add('gradlew', 80);
115
- add('pom.xml', 80);
116
- add('build.sbt', 80);
117
- add(['project', 'build.properties'], 80);
118
-
119
- // Language/project config markers
120
- add('deno.json', 75);
121
- add('deno.jsonc', 75);
122
- add('pyproject.toml', 75);
123
- add('Pipfile', 75);
124
- add('requirements.txt', 75);
125
- add('go.mod', 75);
126
- add('Cargo.toml', 75);
127
- add('composer.json', 75);
128
- add('mix.exs', 75);
129
- add('Gemfile', 75);
130
- add('CMakeLists.txt', 75);
131
- add('stack.yaml', 75);
132
- add('cabal.project', 75);
133
- add('rebar.config', 75);
134
- add('pubspec.yaml', 75);
135
- add('flake.nix', 75);
136
- add('shell.nix', 75);
137
- add('default.nix', 75);
138
- add('.tool-versions', 75);
139
- add('package.json', 74); // generic Node project (lower than lockfiles/workspaces)
140
-
141
- // Changesets
142
- add(['.changeset', 'config.json'], 70);
143
- add('.changeset', 70);
144
-
145
- // Custom markers via env (comma-separated names)
146
- if (process.env.PROJECT_ROOT_MARKERS) {
147
- for (const name of process.env.PROJECT_ROOT_MARKERS.split(',')
148
- .map((s) => s.trim())
149
- .filter(Boolean)) {
150
- add(name, 72);
151
- }
152
- }
153
-
154
- /** Check for package.json with "workspaces" */
155
- const hasWorkspacePackageJson = async (d) => {
156
- const pkgPath = path.join(d, 'package.json');
157
- if (!(await exists(pkgPath))) return false;
158
- try {
159
- const raw = await fs.readFile(pkgPath, 'utf8');
160
- const pkg = JSON.parse(raw);
161
- return Boolean(pkg && pkg.workspaces);
162
- } catch {
163
- return false;
164
- }
165
- };
166
-
167
- let best = null; // { dir, weight }
168
-
169
- // Try to detect VCS toplevel once up-front; treat as authoritative slightly above .git marker
170
- const vcsTop = await _detectVcsTopLevel(dir);
171
- if (vcsTop) {
172
- best = { dir: vcsTop, weight: 101 };
173
- }
174
-
175
- while (true) {
176
- // Special check: package.json with "workspaces"
177
- if ((await hasWorkspacePackageJson(dir)) && (!best || 90 >= best.weight)) best = { dir, weight: 90 };
178
-
179
- // Evaluate all other checks in parallel
180
- const results = await Promise.all(checks.map(async (c) => ({ c, ok: await exists(c.makePath(dir)) })));
181
-
182
- for (const { c, ok } of results) {
183
- if (!ok) continue;
184
- if (!best || c.weight >= best.weight) {
185
- best = { dir, weight: c.weight };
186
- }
187
- }
188
-
189
- if (dir === fsRoot) break;
190
- dir = path.dirname(dir);
191
- }
192
-
193
- const out = best ? best.dir : null;
194
- _cache.set(startKey, out);
195
- return out;
196
- } catch {
197
- return null;
198
- }
199
- }
200
-
201
- module.exports = { findProjectRoot };