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.
Files changed (25) hide show
  1. package/.github/workflows/discord.yaml +16 -0
  2. package/CHANGELOG.md +8 -2
  3. package/README.md +36 -3
  4. package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/Complete AI Agent System - Flowchart.svg +102 -0
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/README.md +109 -0
  12. package/package.json +2 -2
  13. package/tools/flattener/aggregate.js +76 -0
  14. package/tools/flattener/binary.js +53 -0
  15. package/tools/flattener/discovery.js +70 -0
  16. package/tools/flattener/files.js +35 -0
  17. package/tools/flattener/ignoreRules.js +176 -0
  18. package/tools/flattener/main.js +113 -466
  19. package/tools/flattener/projectRoot.js +45 -0
  20. package/tools/flattener/prompts.js +44 -0
  21. package/tools/flattener/stats.js +30 -0
  22. package/tools/flattener/xml.js +86 -0
  23. package/tools/installer/package.json +1 -1
  24. package/tools/shared/bannerArt.js +105 -0
  25. package/tools/installer/package-lock.json +0 -906
@@ -1,219 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { Command } = require('commander');
4
- const fs = require('fs-extra');
5
- const path = require('node:path');
6
- const { glob } = require('glob');
7
- const { minimatch } = require('minimatch');
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, '&amp;')
363
- .replace(/</g, '&lt;')
364
- .replace(/>/g, '&gt;')
365
- .replace(/"/g, '&quot;')
366
- .replace(/'/g, '&apos;');
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
- for (const file of relativeFiles) {
464
- let shouldIgnore = false;
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
- // First check positive patterns (ignore these files)
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
- // Then check negative patterns (don't ignore these files even if they match positive patterns)
475
- if (shouldIgnore) {
476
- for (const pattern of negativePatterns) {
477
- if (minimatch(file, pattern)) {
478
- shouldIgnore = false;
479
- break;
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
- if (!shouldIgnore) {
485
- filteredRelative.push(file);
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('ora');
151
+ const { default: ora } = await import("ora");
517
152
 
518
153
  // Start file discovery with spinner
519
- const discoverySpinner = ora('šŸ” Discovering files...').start();
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(`šŸ“ Found ${filteredFiles.length} files to include`);
157
+ discoverySpinner.succeed(
158
+ `šŸ“ Found ${filteredFiles.length} files to include`,
159
+ );
523
160
 
524
161
  // Process files with progress tracking
525
- console.log('Reading file contents');
526
- const processingSpinner = ora('šŸ“„ Processing files...').start();
527
- const aggregatedContent = await aggregateFileContents(filteredFiles, inputDir, processingSpinner);
528
- processingSpinner.succeed(`āœ… Processed ${aggregatedContent.processedFiles}/${filteredFiles.length} files`);
529
-
530
- // Log processing results for test validation
531
- console.log(`Processed ${aggregatedContent.processedFiles}/${filteredFiles.length} files`);
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('šŸ”§ Generating XML output...').start();
181
+ const xmlSpinner = ora("šŸ”§ Generating XML output...").start();
542
182
  await generateXMLOutput(aggregatedContent, outputPath);
543
- xmlSpinner.succeed('šŸ“ XML generation completed');
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('\nšŸ“Š Completion Summary:');
551
- console.log(`āœ… Successfully processed ${filteredFiles.length} files into ${path.basename(outputPath)}`);
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(`šŸ“ Total lines of code: ${stats.totalLines.toLocaleString()}`);
199
+ console.log(
200
+ `šŸ“ Total lines of code: ${stats.totalLines.toLocaleString()}`,
201
+ );
556
202
  console.log(`šŸ”¢ Estimated tokens: ${stats.estimatedTokens}`);
557
- console.log(`šŸ“Š File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`);
558
-
203
+ console.log(
204
+ `šŸ“Š File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`,
205
+ );
559
206
  } catch (error) {
560
- console.error('āŒ Critical error:', error.message);
561
- console.error('An unexpected error occurred.');
207
+ console.error("āŒ Critical error:", error.message);
208
+ console.error("An unexpected error occurred.");
562
209
  process.exit(1);
563
210
  }
564
211
  });