bmad-method 4.35.3 ā 4.36.0
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/.github/workflows/discord.yaml +16 -0
- package/CHANGELOG.md +8 -2
- package/README.md +36 -3
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/Complete AI Agent System - Flowchart.svg +102 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.1 Google Cloud Project Setup/1.1.1 - Initial Project Configuration - bash copy.txt +13 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.1 Google Cloud Project Setup/1.1.1 - Initial Project Configuration - bash.txt +13 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.2 Agent Development Kit Installation/1.2.2 - Basic Project Structure - txt.txt +25 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.3 Core Configuration Files/1.3.1 - settings.py +34 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.3 Core Configuration Files/1.3.2 - main.py - Base Application.py +70 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.4 Deployment Configuration/1.4.2 - cloudbuild.yaml +26 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/README.md +109 -0
- package/package.json +2 -2
- package/tools/flattener/aggregate.js +76 -0
- package/tools/flattener/binary.js +53 -0
- package/tools/flattener/discovery.js +70 -0
- package/tools/flattener/files.js +35 -0
- package/tools/flattener/ignoreRules.js +176 -0
- package/tools/flattener/main.js +113 -466
- package/tools/flattener/projectRoot.js +45 -0
- package/tools/flattener/prompts.js +44 -0
- package/tools/flattener/stats.js +30 -0
- package/tools/flattener/xml.js +86 -0
- package/tools/installer/package.json +1 -1
- package/tools/shared/bannerArt.js +105 -0
- package/tools/installer/package-lock.json +0 -906
package/tools/flattener/main.js
CHANGED
|
@@ -1,219 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { Command } = require(
|
|
4
|
-
const fs = require(
|
|
5
|
-
const path = require(
|
|
6
|
-
const
|
|
7
|
-
|
|
3
|
+
const { Command } = require("commander");
|
|
4
|
+
const fs = require("fs-extra");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const process = require("node:process");
|
|
7
|
+
|
|
8
|
+
// Modularized components
|
|
9
|
+
const { findProjectRoot } = require("./projectRoot.js");
|
|
10
|
+
const { promptYesNo, promptPath } = require("./prompts.js");
|
|
11
|
+
const {
|
|
12
|
+
discoverFiles,
|
|
13
|
+
filterFiles,
|
|
14
|
+
aggregateFileContents,
|
|
15
|
+
} = require("./files.js");
|
|
16
|
+
const { generateXMLOutput } = require("./xml.js");
|
|
17
|
+
const { calculateStatistics } = require("./stats.js");
|
|
8
18
|
|
|
9
19
|
/**
|
|
10
20
|
* Recursively discover all files in a directory
|
|
11
21
|
* @param {string} rootDir - The root directory to scan
|
|
12
22
|
* @returns {Promise<string[]>} Array of file paths
|
|
13
23
|
*/
|
|
14
|
-
async function discoverFiles(rootDir) {
|
|
15
|
-
try {
|
|
16
|
-
const gitignorePath = path.join(rootDir, '.gitignore');
|
|
17
|
-
const gitignorePatterns = await parseGitignore(gitignorePath);
|
|
18
|
-
|
|
19
|
-
// Common gitignore patterns that should always be ignored
|
|
20
|
-
const commonIgnorePatterns = [
|
|
21
|
-
// Version control
|
|
22
|
-
'.git/**',
|
|
23
|
-
'.svn/**',
|
|
24
|
-
'.hg/**',
|
|
25
|
-
'.bzr/**',
|
|
26
|
-
|
|
27
|
-
// Dependencies
|
|
28
|
-
'node_modules/**',
|
|
29
|
-
'bower_components/**',
|
|
30
|
-
'vendor/**',
|
|
31
|
-
'packages/**',
|
|
32
|
-
|
|
33
|
-
// Build outputs
|
|
34
|
-
'build/**',
|
|
35
|
-
'dist/**',
|
|
36
|
-
'out/**',
|
|
37
|
-
'target/**',
|
|
38
|
-
'bin/**',
|
|
39
|
-
'obj/**',
|
|
40
|
-
'release/**',
|
|
41
|
-
'debug/**',
|
|
42
|
-
|
|
43
|
-
// Environment and config
|
|
44
|
-
'.env',
|
|
45
|
-
'.env.*',
|
|
46
|
-
'*.env',
|
|
47
|
-
'.config',
|
|
48
|
-
|
|
49
|
-
// Logs
|
|
50
|
-
'logs/**',
|
|
51
|
-
'*.log',
|
|
52
|
-
'npm-debug.log*',
|
|
53
|
-
'yarn-debug.log*',
|
|
54
|
-
'yarn-error.log*',
|
|
55
|
-
'lerna-debug.log*',
|
|
56
|
-
|
|
57
|
-
// Coverage and testing
|
|
58
|
-
'coverage/**',
|
|
59
|
-
'.nyc_output/**',
|
|
60
|
-
'.coverage/**',
|
|
61
|
-
'test-results/**',
|
|
62
|
-
'junit.xml',
|
|
63
|
-
|
|
64
|
-
// Cache directories
|
|
65
|
-
'.cache/**',
|
|
66
|
-
'.tmp/**',
|
|
67
|
-
'.temp/**',
|
|
68
|
-
'tmp/**',
|
|
69
|
-
'temp/**',
|
|
70
|
-
'.sass-cache/**',
|
|
71
|
-
'.eslintcache',
|
|
72
|
-
'.stylelintcache',
|
|
73
|
-
|
|
74
|
-
// OS generated files
|
|
75
|
-
'.DS_Store',
|
|
76
|
-
'.DS_Store?',
|
|
77
|
-
'._*',
|
|
78
|
-
'.Spotlight-V100',
|
|
79
|
-
'.Trashes',
|
|
80
|
-
'ehthumbs.db',
|
|
81
|
-
'Thumbs.db',
|
|
82
|
-
'desktop.ini',
|
|
83
|
-
|
|
84
|
-
// IDE and editor files
|
|
85
|
-
'.vscode/**',
|
|
86
|
-
'.idea/**',
|
|
87
|
-
'*.swp',
|
|
88
|
-
'*.swo',
|
|
89
|
-
'*~',
|
|
90
|
-
'.project',
|
|
91
|
-
'.classpath',
|
|
92
|
-
'.settings/**',
|
|
93
|
-
'*.sublime-project',
|
|
94
|
-
'*.sublime-workspace',
|
|
95
|
-
|
|
96
|
-
// Package manager files
|
|
97
|
-
'package-lock.json',
|
|
98
|
-
'yarn.lock',
|
|
99
|
-
'pnpm-lock.yaml',
|
|
100
|
-
'composer.lock',
|
|
101
|
-
'Pipfile.lock',
|
|
102
|
-
|
|
103
|
-
// Runtime and compiled files
|
|
104
|
-
'*.pyc',
|
|
105
|
-
'*.pyo',
|
|
106
|
-
'*.pyd',
|
|
107
|
-
'__pycache__/**',
|
|
108
|
-
'*.class',
|
|
109
|
-
'*.jar',
|
|
110
|
-
'*.war',
|
|
111
|
-
'*.ear',
|
|
112
|
-
'*.o',
|
|
113
|
-
'*.so',
|
|
114
|
-
'*.dll',
|
|
115
|
-
'*.exe',
|
|
116
|
-
|
|
117
|
-
// Documentation build
|
|
118
|
-
'_site/**',
|
|
119
|
-
'.jekyll-cache/**',
|
|
120
|
-
'.jekyll-metadata',
|
|
121
|
-
|
|
122
|
-
// Flattener specific outputs
|
|
123
|
-
'flattened-codebase.xml',
|
|
124
|
-
'repomix-output.xml'
|
|
125
|
-
];
|
|
126
|
-
|
|
127
|
-
const combinedIgnores = [
|
|
128
|
-
...gitignorePatterns,
|
|
129
|
-
...commonIgnorePatterns
|
|
130
|
-
];
|
|
131
|
-
|
|
132
|
-
// Use glob to recursively find all files, excluding common ignore patterns
|
|
133
|
-
const files = await glob('**/*', {
|
|
134
|
-
cwd: rootDir,
|
|
135
|
-
nodir: true, // Only files, not directories
|
|
136
|
-
dot: true, // Include hidden files
|
|
137
|
-
follow: false, // Don't follow symbolic links
|
|
138
|
-
ignore: combinedIgnores
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
return files.map(file => path.resolve(rootDir, file));
|
|
142
|
-
} catch (error) {
|
|
143
|
-
console.error('Error discovering files:', error.message);
|
|
144
|
-
return [];
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
24
|
|
|
148
25
|
/**
|
|
149
26
|
* Parse .gitignore file and return ignore patterns
|
|
150
27
|
* @param {string} gitignorePath - Path to .gitignore file
|
|
151
28
|
* @returns {Promise<string[]>} Array of ignore patterns
|
|
152
29
|
*/
|
|
153
|
-
async function parseGitignore(gitignorePath) {
|
|
154
|
-
try {
|
|
155
|
-
if (!await fs.pathExists(gitignorePath)) {
|
|
156
|
-
return [];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const content = await fs.readFile(gitignorePath, 'utf8');
|
|
160
|
-
return content
|
|
161
|
-
.split('\n')
|
|
162
|
-
.map(line => line.trim())
|
|
163
|
-
.filter(line => line && !line.startsWith('#')) // Remove empty lines and comments
|
|
164
|
-
.map(pattern => {
|
|
165
|
-
// Convert gitignore patterns to glob patterns
|
|
166
|
-
if (pattern.endsWith('/')) {
|
|
167
|
-
return pattern + '**';
|
|
168
|
-
}
|
|
169
|
-
return pattern;
|
|
170
|
-
});
|
|
171
|
-
} catch (error) {
|
|
172
|
-
console.error('Error parsing .gitignore:', error.message);
|
|
173
|
-
return [];
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
30
|
|
|
177
31
|
/**
|
|
178
32
|
* Check if a file is binary using file command and heuristics
|
|
179
33
|
* @param {string} filePath - Path to the file
|
|
180
34
|
* @returns {Promise<boolean>} True if file is binary
|
|
181
35
|
*/
|
|
182
|
-
async function isBinaryFile(filePath) {
|
|
183
|
-
try {
|
|
184
|
-
// First check by file extension
|
|
185
|
-
const binaryExtensions = [
|
|
186
|
-
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.svg',
|
|
187
|
-
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
188
|
-
'.zip', '.tar', '.gz', '.rar', '.7z',
|
|
189
|
-
'.exe', '.dll', '.so', '.dylib',
|
|
190
|
-
'.mp3', '.mp4', '.avi', '.mov', '.wav',
|
|
191
|
-
'.ttf', '.otf', '.woff', '.woff2',
|
|
192
|
-
'.bin', '.dat', '.db', '.sqlite'
|
|
193
|
-
];
|
|
194
|
-
|
|
195
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
196
|
-
if (binaryExtensions.includes(ext)) {
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// For files without clear extensions, try to read a small sample
|
|
201
|
-
const stats = await fs.stat(filePath);
|
|
202
|
-
if (stats.size === 0) {
|
|
203
|
-
return false; // Empty files are considered text
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Read first 1024 bytes to check for null bytes
|
|
207
|
-
const sampleSize = Math.min(1024, stats.size);
|
|
208
|
-
const buffer = await fs.readFile(filePath, { encoding: null, flag: 'r' });
|
|
209
|
-
const sample = buffer.slice(0, sampleSize);
|
|
210
|
-
// If we find null bytes, it's likely binary
|
|
211
|
-
return sample.includes(0);
|
|
212
|
-
} catch (error) {
|
|
213
|
-
console.warn(`Warning: Could not determine if file is binary: ${filePath} - ${error.message}`);
|
|
214
|
-
return false; // Default to text if we can't determine
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
36
|
|
|
218
37
|
/**
|
|
219
38
|
* Read and aggregate content from text files
|
|
@@ -222,68 +41,6 @@ async function isBinaryFile(filePath) {
|
|
|
222
41
|
* @param {Object} spinner - Optional spinner instance for progress display
|
|
223
42
|
* @returns {Promise<Object>} Object containing file contents and metadata
|
|
224
43
|
*/
|
|
225
|
-
async function aggregateFileContents(files, rootDir, spinner = null) {
|
|
226
|
-
const results = {
|
|
227
|
-
textFiles: [],
|
|
228
|
-
binaryFiles: [],
|
|
229
|
-
errors: [],
|
|
230
|
-
totalFiles: files.length,
|
|
231
|
-
processedFiles: 0
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
for (const filePath of files) {
|
|
235
|
-
try {
|
|
236
|
-
const relativePath = path.relative(rootDir, filePath);
|
|
237
|
-
|
|
238
|
-
// Update progress indicator
|
|
239
|
-
if (spinner) {
|
|
240
|
-
spinner.text = `Processing file ${results.processedFiles + 1}/${results.totalFiles}: ${relativePath}`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const isBinary = await isBinaryFile(filePath);
|
|
244
|
-
|
|
245
|
-
if (isBinary) {
|
|
246
|
-
results.binaryFiles.push({
|
|
247
|
-
path: relativePath,
|
|
248
|
-
absolutePath: filePath,
|
|
249
|
-
size: (await fs.stat(filePath)).size
|
|
250
|
-
});
|
|
251
|
-
} else {
|
|
252
|
-
// Read text file content
|
|
253
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
254
|
-
results.textFiles.push({
|
|
255
|
-
path: relativePath,
|
|
256
|
-
absolutePath: filePath,
|
|
257
|
-
content: content,
|
|
258
|
-
size: content.length,
|
|
259
|
-
lines: content.split('\n').length
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
results.processedFiles++;
|
|
264
|
-
} catch (error) {
|
|
265
|
-
const relativePath = path.relative(rootDir, filePath);
|
|
266
|
-
const errorInfo = {
|
|
267
|
-
path: relativePath,
|
|
268
|
-
absolutePath: filePath,
|
|
269
|
-
error: error.message
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
results.errors.push(errorInfo);
|
|
273
|
-
|
|
274
|
-
// Log warning without interfering with spinner
|
|
275
|
-
if (spinner) {
|
|
276
|
-
spinner.warn(`Warning: Could not read file ${relativePath}: ${error.message}`);
|
|
277
|
-
} else {
|
|
278
|
-
console.warn(`Warning: Could not read file ${relativePath}: ${error.message}`);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
results.processedFiles++;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return results;
|
|
286
|
-
}
|
|
287
44
|
|
|
288
45
|
/**
|
|
289
46
|
* Generate XML output with aggregated file contents using streaming
|
|
@@ -291,111 +48,6 @@ async function aggregateFileContents(files, rootDir, spinner = null) {
|
|
|
291
48
|
* @param {string} outputPath - The output file path
|
|
292
49
|
* @returns {Promise<void>} Promise that resolves when writing is complete
|
|
293
50
|
*/
|
|
294
|
-
async function generateXMLOutput(aggregatedContent, outputPath) {
|
|
295
|
-
const { textFiles } = aggregatedContent;
|
|
296
|
-
|
|
297
|
-
// Create write stream for efficient memory usage
|
|
298
|
-
const writeStream = fs.createWriteStream(outputPath, { encoding: 'utf8' });
|
|
299
|
-
|
|
300
|
-
return new Promise((resolve, reject) => {
|
|
301
|
-
writeStream.on('error', reject);
|
|
302
|
-
writeStream.on('finish', resolve);
|
|
303
|
-
|
|
304
|
-
// Write XML header
|
|
305
|
-
writeStream.write('<?xml version="1.0" encoding="UTF-8"?>\n');
|
|
306
|
-
writeStream.write('<files>\n');
|
|
307
|
-
|
|
308
|
-
// Process files one by one to minimize memory usage
|
|
309
|
-
let fileIndex = 0;
|
|
310
|
-
|
|
311
|
-
const writeNextFile = () => {
|
|
312
|
-
if (fileIndex >= textFiles.length) {
|
|
313
|
-
// All files processed, close XML and stream
|
|
314
|
-
writeStream.write('</files>\n');
|
|
315
|
-
writeStream.end();
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const file = textFiles[fileIndex];
|
|
320
|
-
fileIndex++;
|
|
321
|
-
|
|
322
|
-
// Write file opening tag
|
|
323
|
-
writeStream.write(` <file path="${escapeXml(file.path)}">`);
|
|
324
|
-
|
|
325
|
-
// Use CDATA for code content, handling CDATA end sequences properly
|
|
326
|
-
if (file.content?.trim()) {
|
|
327
|
-
const indentedContent = indentFileContent(file.content);
|
|
328
|
-
if (file.content.includes(']]>')) {
|
|
329
|
-
// If content contains ]]>, split it and wrap each part in CDATA
|
|
330
|
-
writeStream.write(splitAndWrapCDATA(indentedContent));
|
|
331
|
-
} else {
|
|
332
|
-
writeStream.write(`<![CDATA[\n${indentedContent}\n ]]>`);
|
|
333
|
-
}
|
|
334
|
-
} else if (file.content) {
|
|
335
|
-
// Handle empty or whitespace-only content
|
|
336
|
-
const indentedContent = indentFileContent(file.content);
|
|
337
|
-
writeStream.write(`<![CDATA[\n${indentedContent}\n ]]>`);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Write file closing tag
|
|
341
|
-
writeStream.write('</file>\n');
|
|
342
|
-
|
|
343
|
-
// Continue with next file on next tick to avoid stack overflow
|
|
344
|
-
setImmediate(writeNextFile);
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
// Start processing files
|
|
348
|
-
writeNextFile();
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Escape XML special characters for attributes
|
|
354
|
-
* @param {string} str - String to escape
|
|
355
|
-
* @returns {string} Escaped string
|
|
356
|
-
*/
|
|
357
|
-
function escapeXml(str) {
|
|
358
|
-
if (typeof str !== 'string') {
|
|
359
|
-
return String(str);
|
|
360
|
-
}
|
|
361
|
-
return str
|
|
362
|
-
.replace(/&/g, '&')
|
|
363
|
-
.replace(/</g, '<')
|
|
364
|
-
.replace(/>/g, '>')
|
|
365
|
-
.replace(/"/g, '"')
|
|
366
|
-
.replace(/'/g, ''');
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Indent file content with 4 spaces for each line
|
|
371
|
-
* @param {string} content - Content to indent
|
|
372
|
-
* @returns {string} Indented content
|
|
373
|
-
*/
|
|
374
|
-
function indentFileContent(content) {
|
|
375
|
-
if (typeof content !== 'string') {
|
|
376
|
-
return String(content);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Split content into lines and add 4 spaces of indentation to each line
|
|
380
|
-
return content.split('\n').map(line => ` ${line}`).join('\n');
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Split content containing ]]> and wrap each part in CDATA
|
|
385
|
-
* @param {string} content - Content to process
|
|
386
|
-
* @returns {string} Content with properly wrapped CDATA sections
|
|
387
|
-
*/
|
|
388
|
-
function splitAndWrapCDATA(content) {
|
|
389
|
-
if (typeof content !== 'string') {
|
|
390
|
-
return String(content);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Replace ]]> with ]]]]><![CDATA[> to escape it within CDATA
|
|
394
|
-
const escapedContent = content.replace(/]]>/g, ']]]]><![CDATA[>');
|
|
395
|
-
return `<![CDATA[
|
|
396
|
-
${escapedContent}
|
|
397
|
-
]]>`;
|
|
398
|
-
}
|
|
399
51
|
|
|
400
52
|
/**
|
|
401
53
|
* Calculate statistics for the processed files
|
|
@@ -403,38 +55,6 @@ ${escapedContent}
|
|
|
403
55
|
* @param {number} xmlFileSize - The size of the generated XML file in bytes
|
|
404
56
|
* @returns {Object} Statistics object
|
|
405
57
|
*/
|
|
406
|
-
function calculateStatistics(aggregatedContent, xmlFileSize) {
|
|
407
|
-
const { textFiles, binaryFiles, errors } = aggregatedContent;
|
|
408
|
-
|
|
409
|
-
// Calculate total file size in bytes
|
|
410
|
-
const totalTextSize = textFiles.reduce((sum, file) => sum + file.size, 0);
|
|
411
|
-
const totalBinarySize = binaryFiles.reduce((sum, file) => sum + file.size, 0);
|
|
412
|
-
const totalSize = totalTextSize + totalBinarySize;
|
|
413
|
-
|
|
414
|
-
// Calculate total lines of code
|
|
415
|
-
const totalLines = textFiles.reduce((sum, file) => sum + file.lines, 0);
|
|
416
|
-
|
|
417
|
-
// Estimate token count (rough approximation: 1 token ā 4 characters)
|
|
418
|
-
const estimatedTokens = Math.ceil(xmlFileSize / 4);
|
|
419
|
-
|
|
420
|
-
// Format file size
|
|
421
|
-
const formatSize = (bytes) => {
|
|
422
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
423
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
424
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
return {
|
|
428
|
-
totalFiles: textFiles.length + binaryFiles.length,
|
|
429
|
-
textFiles: textFiles.length,
|
|
430
|
-
binaryFiles: binaryFiles.length,
|
|
431
|
-
errorFiles: errors.length,
|
|
432
|
-
totalSize: formatSize(totalSize),
|
|
433
|
-
xmlSize: formatSize(xmlFileSize),
|
|
434
|
-
totalLines,
|
|
435
|
-
estimatedTokens: estimatedTokens.toLocaleString()
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
58
|
|
|
439
59
|
/**
|
|
440
60
|
* Filter files based on .gitignore patterns
|
|
@@ -442,66 +62,81 @@ function calculateStatistics(aggregatedContent, xmlFileSize) {
|
|
|
442
62
|
* @param {string} rootDir - The root directory
|
|
443
63
|
* @returns {Promise<string[]>} Filtered array of file paths
|
|
444
64
|
*/
|
|
445
|
-
async function filterFiles(files, rootDir) {
|
|
446
|
-
const gitignorePath = path.join(rootDir, '.gitignore');
|
|
447
|
-
const ignorePatterns = await parseGitignore(gitignorePath);
|
|
448
|
-
|
|
449
|
-
if (ignorePatterns.length === 0) {
|
|
450
|
-
return files;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Convert absolute paths to relative for pattern matching
|
|
454
|
-
const relativeFiles = files.map(file => path.relative(rootDir, file));
|
|
455
|
-
|
|
456
|
-
// Separate positive and negative patterns
|
|
457
|
-
const positivePatterns = ignorePatterns.filter(p => !p.startsWith('!'));
|
|
458
|
-
const negativePatterns = ignorePatterns.filter(p => p.startsWith('!')).map(p => p.slice(1));
|
|
459
|
-
|
|
460
|
-
// Filter out files that match ignore patterns
|
|
461
|
-
const filteredRelative = [];
|
|
462
65
|
|
|
463
|
-
|
|
464
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Attempt to find the project root by walking up from startDir
|
|
68
|
+
* Looks for common project markers like .git, package.json, pyproject.toml, etc.
|
|
69
|
+
* @param {string} startDir
|
|
70
|
+
* @returns {Promise<string|null>} project root directory or null if not found
|
|
71
|
+
*/
|
|
465
72
|
|
|
466
|
-
|
|
467
|
-
for (const pattern of positivePatterns) {
|
|
468
|
-
if (minimatch(file, pattern)) {
|
|
469
|
-
shouldIgnore = true;
|
|
470
|
-
break;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
73
|
+
const program = new Command();
|
|
473
74
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
75
|
+
program
|
|
76
|
+
.name("bmad-flatten")
|
|
77
|
+
.description("BMad-Method codebase flattener tool")
|
|
78
|
+
.version("1.0.0")
|
|
79
|
+
.option("-i, --input <path>", "Input directory to flatten", process.cwd())
|
|
80
|
+
.option("-o, --output <path>", "Output file path", "flattened-codebase.xml")
|
|
81
|
+
.action(async (options) => {
|
|
82
|
+
let inputDir = path.resolve(options.input);
|
|
83
|
+
let outputPath = path.resolve(options.output);
|
|
84
|
+
|
|
85
|
+
// Detect if user explicitly provided -i/--input or -o/--output
|
|
86
|
+
const argv = process.argv.slice(2);
|
|
87
|
+
const userSpecifiedInput = argv.some((a) =>
|
|
88
|
+
a === "-i" || a === "--input" || a.startsWith("--input=")
|
|
89
|
+
);
|
|
90
|
+
const userSpecifiedOutput = argv.some((a) =>
|
|
91
|
+
a === "-o" || a === "--output" || a.startsWith("--output=")
|
|
92
|
+
);
|
|
93
|
+
const noPathArgs = !userSpecifiedInput && !userSpecifiedOutput;
|
|
94
|
+
|
|
95
|
+
if (noPathArgs) {
|
|
96
|
+
const detectedRoot = await findProjectRoot(process.cwd());
|
|
97
|
+
const suggestedOutput = detectedRoot
|
|
98
|
+
? path.join(detectedRoot, "flattened-codebase.xml")
|
|
99
|
+
: path.resolve("flattened-codebase.xml");
|
|
100
|
+
|
|
101
|
+
if (detectedRoot) {
|
|
102
|
+
const useDefaults = await promptYesNo(
|
|
103
|
+
`Detected project root at "${detectedRoot}". Use it as input and write output to "${suggestedOutput}"?`,
|
|
104
|
+
true,
|
|
105
|
+
);
|
|
106
|
+
if (useDefaults) {
|
|
107
|
+
inputDir = detectedRoot;
|
|
108
|
+
outputPath = suggestedOutput;
|
|
109
|
+
} else {
|
|
110
|
+
inputDir = await promptPath(
|
|
111
|
+
"Enter input directory path",
|
|
112
|
+
process.cwd(),
|
|
113
|
+
);
|
|
114
|
+
outputPath = await promptPath(
|
|
115
|
+
"Enter output file path",
|
|
116
|
+
path.join(inputDir, "flattened-codebase.xml"),
|
|
117
|
+
);
|
|
480
118
|
}
|
|
119
|
+
} else {
|
|
120
|
+
console.log("Could not auto-detect a project root.");
|
|
121
|
+
inputDir = await promptPath(
|
|
122
|
+
"Enter input directory path",
|
|
123
|
+
process.cwd(),
|
|
124
|
+
);
|
|
125
|
+
outputPath = await promptPath(
|
|
126
|
+
"Enter output file path",
|
|
127
|
+
path.join(inputDir, "flattened-codebase.xml"),
|
|
128
|
+
);
|
|
481
129
|
}
|
|
130
|
+
} else {
|
|
131
|
+
console.error(
|
|
132
|
+
"Could not auto-detect a project root and no arguments were provided. Please specify -i/--input and -o/--output.",
|
|
133
|
+
);
|
|
134
|
+
process.exit(1);
|
|
482
135
|
}
|
|
483
136
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Convert back to absolute paths
|
|
490
|
-
return filteredRelative.map(file => path.resolve(rootDir, file));
|
|
491
|
-
}
|
|
137
|
+
// Ensure output directory exists
|
|
138
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
492
139
|
|
|
493
|
-
const program = new Command();
|
|
494
|
-
|
|
495
|
-
program
|
|
496
|
-
.name('bmad-flatten')
|
|
497
|
-
.description('BMad-Method codebase flattener tool')
|
|
498
|
-
.version('1.0.0')
|
|
499
|
-
.option('-i, --input <path>', 'Input directory to flatten', process.cwd())
|
|
500
|
-
.option('-o, --output <path>', 'Output file path', 'flattened-codebase.xml')
|
|
501
|
-
.action(async (options) => {
|
|
502
|
-
const inputDir = path.resolve(options.input);
|
|
503
|
-
const outputPath = path.resolve(options.output);
|
|
504
|
-
|
|
505
140
|
console.log(`Flattening codebase from: ${inputDir}`);
|
|
506
141
|
console.log(`Output file: ${outputPath}`);
|
|
507
142
|
|
|
@@ -513,22 +148,27 @@ program
|
|
|
513
148
|
}
|
|
514
149
|
|
|
515
150
|
// Import ora dynamically
|
|
516
|
-
const { default: ora } = await import(
|
|
151
|
+
const { default: ora } = await import("ora");
|
|
517
152
|
|
|
518
153
|
// Start file discovery with spinner
|
|
519
|
-
const discoverySpinner = ora(
|
|
154
|
+
const discoverySpinner = ora("š Discovering files...").start();
|
|
520
155
|
const files = await discoverFiles(inputDir);
|
|
521
156
|
const filteredFiles = await filterFiles(files, inputDir);
|
|
522
|
-
discoverySpinner.succeed(
|
|
157
|
+
discoverySpinner.succeed(
|
|
158
|
+
`š Found ${filteredFiles.length} files to include`,
|
|
159
|
+
);
|
|
523
160
|
|
|
524
161
|
// Process files with progress tracking
|
|
525
|
-
console.log(
|
|
526
|
-
const processingSpinner = ora(
|
|
527
|
-
const aggregatedContent = await aggregateFileContents(
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
162
|
+
console.log("Reading file contents");
|
|
163
|
+
const processingSpinner = ora("š Processing files...").start();
|
|
164
|
+
const aggregatedContent = await aggregateFileContents(
|
|
165
|
+
filteredFiles,
|
|
166
|
+
inputDir,
|
|
167
|
+
processingSpinner,
|
|
168
|
+
);
|
|
169
|
+
processingSpinner.succeed(
|
|
170
|
+
`ā
Processed ${aggregatedContent.processedFiles}/${filteredFiles.length} files`,
|
|
171
|
+
);
|
|
532
172
|
if (aggregatedContent.errors.length > 0) {
|
|
533
173
|
console.log(`Errors: ${aggregatedContent.errors.length}`);
|
|
534
174
|
}
|
|
@@ -538,27 +178,34 @@ program
|
|
|
538
178
|
}
|
|
539
179
|
|
|
540
180
|
// Generate XML output using streaming
|
|
541
|
-
const xmlSpinner = ora(
|
|
181
|
+
const xmlSpinner = ora("š§ Generating XML output...").start();
|
|
542
182
|
await generateXMLOutput(aggregatedContent, outputPath);
|
|
543
|
-
xmlSpinner.succeed(
|
|
183
|
+
xmlSpinner.succeed("š XML generation completed");
|
|
544
184
|
|
|
545
185
|
// Calculate and display statistics
|
|
546
186
|
const outputStats = await fs.stat(outputPath);
|
|
547
187
|
const stats = calculateStatistics(aggregatedContent, outputStats.size);
|
|
548
188
|
|
|
549
189
|
// Display completion summary
|
|
550
|
-
console.log(
|
|
551
|
-
console.log(
|
|
190
|
+
console.log("\nš Completion Summary:");
|
|
191
|
+
console.log(
|
|
192
|
+
`ā
Successfully processed ${filteredFiles.length} files into ${
|
|
193
|
+
path.basename(outputPath)
|
|
194
|
+
}`,
|
|
195
|
+
);
|
|
552
196
|
console.log(`š Output file: ${outputPath}`);
|
|
553
197
|
console.log(`š Total source size: ${stats.totalSize}`);
|
|
554
198
|
console.log(`š Generated XML size: ${stats.xmlSize}`);
|
|
555
|
-
console.log(
|
|
199
|
+
console.log(
|
|
200
|
+
`š Total lines of code: ${stats.totalLines.toLocaleString()}`,
|
|
201
|
+
);
|
|
556
202
|
console.log(`š¢ Estimated tokens: ${stats.estimatedTokens}`);
|
|
557
|
-
console.log(
|
|
558
|
-
|
|
203
|
+
console.log(
|
|
204
|
+
`š File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`,
|
|
205
|
+
);
|
|
559
206
|
} catch (error) {
|
|
560
|
-
console.error(
|
|
561
|
-
console.error(
|
|
207
|
+
console.error("ā Critical error:", error.message);
|
|
208
|
+
console.error("An unexpected error occurred.");
|
|
562
209
|
process.exit(1);
|
|
563
210
|
}
|
|
564
211
|
});
|