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.
- package/.claude/skills/changelog-social/SKILL.md +178 -0
- package/.claude/skills/changelog-social/examples/discord-example.md +53 -0
- package/.claude/skills/changelog-social/examples/linkedin-example.md +49 -0
- package/.claude/skills/changelog-social/examples/twitter-example.md +55 -0
- package/.claude/skills/changelog-social.skill +0 -0
- package/.claude/skills/draft-changelog/SKILL.md +2 -2
- package/.claude/skills/draft-changelog/prompts/instructions.md +25 -1
- package/.claude/skills/gh-triage/SKILL.md +1 -1
- package/.claude/skills/release-module/SKILL.md +1 -1
- package/.claude/skills/release-module/prompts/instructions.md +0 -4
- package/.github/workflows/quality.yaml +3 -0
- package/.vscode/settings.json +1 -2
- package/CHANGELOG.md +65 -1
- package/docs/bmgd/game-types.md +1 -0
- package/docs/bmgd/index.md +1 -1
- package/docs/bmgd/quick-flow-workflows.md +1 -0
- package/docs/reference/agents.md +22 -0
- package/docs/reference/commands.md +34 -0
- package/docs/reference/testing.md +21 -0
- package/docs/reference/workflow-map.md +1 -1
- package/package.json +5 -2
- package/src/bmm/agents/analyst.agent.yaml +11 -5
- package/src/bmm/agents/dev.agent.yaml +0 -1
- package/src/bmm/agents/pm.agent.yaml +3 -6
- package/src/bmm/agents/{quinn.agent.yaml → qa.agent.yaml} +3 -3
- package/src/bmm/module-help.csv +13 -20
- package/src/bmm/workflows/1-analysis/create-product-brief/workflow.md +0 -1
- package/src/bmm/workflows/1-analysis/research/workflow-domain-research.md +54 -0
- package/src/bmm/workflows/1-analysis/research/workflow-market-research.md +54 -0
- package/src/bmm/workflows/1-analysis/research/workflow-technical-research.md +54 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/data/domain-complexity.csv +2 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md +63 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md +65 -0
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md +65 -0
- package/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md +0 -1
- package/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md +0 -1
- package/src/bmm/workflows/3-solutioning/create-architecture/data/domain-complexity.csv +2 -0
- package/src/bmm/workflows/3-solutioning/create-architecture/workflow.md +0 -1
- package/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md +0 -1
- package/src/bmm/workflows/4-implementation/code-review/workflow.yaml +0 -3
- package/src/bmm/workflows/4-implementation/correct-course/workflow.yaml +0 -4
- package/src/bmm/workflows/4-implementation/create-story/workflow.yaml +0 -4
- package/src/bmm/workflows/4-implementation/dev-story/workflow.yaml +0 -4
- package/src/bmm/workflows/4-implementation/retrospective/workflow.yaml +0 -3
- package/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml +0 -4
- package/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml +0 -6
- package/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md +0 -1
- package/src/bmm/workflows/document-project/workflow.yaml +0 -8
- package/src/bmm/workflows/qa/automate/workflow.yaml +0 -2
- package/src/core/tasks/editorial-review-prose.xml +14 -12
- package/src/core/tasks/editorial-review-structure.xml +28 -28
- package/src/core/tasks/help.md +36 -16
- package/src/core/tasks/index-docs.xml +1 -1
- package/src/core/tasks/review-adversarial-general.xml +2 -2
- package/src/core/tasks/shard-doc.xml +1 -2
- package/src/core/tasks/workflow.xml +1 -1
- package/src/core/workflows/advanced-elicitation/workflow.xml +1 -1
- package/test/test-installation-components.js +1 -1
- package/tools/build-docs.js +21 -1
- package/tools/cli/bmad-cli.js +42 -3
- package/tools/cli/installers/lib/core/config-collector.js +5 -1
- package/tools/cli/installers/lib/core/dependency-resolver.js +1 -1
- package/tools/cli/installers/lib/core/installer.js +1 -6
- package/tools/cli/installers/lib/core/manifest-generator.js +149 -125
- package/tools/cli/installers/lib/custom/handler.js +1 -1
- package/tools/cli/installers/lib/ide/_base-ide.js +25 -15
- package/tools/cli/installers/lib/ide/_config-driven.js +79 -9
- package/tools/cli/installers/lib/ide/codex.js +7 -1
- package/tools/cli/installers/lib/ide/manager.js +8 -2
- package/tools/cli/installers/lib/ide/platform-codes.yaml +0 -3
- package/tools/cli/installers/lib/ide/shared/agent-command-generator.js +2 -2
- package/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +14 -3
- package/tools/cli/installers/lib/ide/shared/path-utils.js +9 -2
- package/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +126 -29
- package/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +2 -2
- package/tools/cli/installers/lib/ide/templates/combined/default-task.md +10 -0
- package/tools/cli/installers/lib/ide/templates/combined/default-tool.md +10 -0
- package/tools/cli/installers/lib/ide/templates/combined/gemini-task.toml +11 -0
- package/tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml +11 -0
- package/tools/cli/installers/lib/modules/manager.js +3 -6
- package/tools/cli/lib/agent/installer.js +1 -1
- package/tools/cli/lib/prompts.js +192 -6
- package/tools/cli/lib/ui.js +124 -72
- package/tools/schema/agent.js +0 -1
- package/tools/validate-file-refs.js +480 -0
- package/website/astro.config.mjs +1 -35
- package/src/bmm/workflows/1-analysis/research/workflow.md +0 -173
- package/src/bmm/workflows/2-plan-workflows/create-prd/validation-report-prd-workflow.md +0 -433
- package/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md +0 -150
- package/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-library.json +0 -90
- package/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-templates.yaml +0 -127
- package/src/bmm/workflows/excalidraw-diagrams/create-dataflow/checklist.md +0 -39
- package/src/bmm/workflows/excalidraw-diagrams/create-dataflow/instructions.md +0 -130
- package/src/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml +0 -27
- package/src/bmm/workflows/excalidraw-diagrams/create-diagram/checklist.md +0 -43
- package/src/bmm/workflows/excalidraw-diagrams/create-diagram/instructions.md +0 -141
- package/src/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml +0 -27
- package/src/bmm/workflows/excalidraw-diagrams/create-flowchart/checklist.md +0 -49
- package/src/bmm/workflows/excalidraw-diagrams/create-flowchart/instructions.md +0 -241
- package/src/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml +0 -27
- package/src/bmm/workflows/excalidraw-diagrams/create-wireframe/checklist.md +0 -38
- package/src/bmm/workflows/excalidraw-diagrams/create-wireframe/instructions.md +0 -133
- package/src/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml +0 -27
- package/src/core/resources/excalidraw/README.md +0 -160
- package/src/core/resources/excalidraw/excalidraw-helpers.md +0 -127
- package/src/core/resources/excalidraw/library-loader.md +0 -50
- package/src/core/resources/excalidraw/validate-json-instructions.md +0 -79
- package/tools/flattener/aggregate.js +0 -76
- package/tools/flattener/binary.js +0 -80
- package/tools/flattener/discovery.js +0 -71
- package/tools/flattener/files.js +0 -35
- package/tools/flattener/ignoreRules.js +0 -172
- package/tools/flattener/main.js +0 -483
- package/tools/flattener/projectRoot.js +0 -201
- package/tools/flattener/prompts.js +0 -44
- package/tools/flattener/stats.helpers.js +0 -368
- package/tools/flattener/stats.js +0 -75
- package/tools/flattener/test-matrix.js +0 -409
- package/tools/flattener/xml.js +0 -82
package/tools/flattener/main.js
DELETED
|
@@ -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 };
|