bmad-method 5.0.0-beta.2 โ†’ 5.0.1

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 (131) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +3 -3
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +3 -3
  3. package/.github/workflows/discord.yaml +11 -2
  4. package/.github/workflows/format-check.yaml +42 -0
  5. package/.github/workflows/manual-release.yaml +173 -0
  6. package/.husky/pre-commit +3 -0
  7. package/.vscode/settings.json +26 -1
  8. package/CHANGELOG.md +0 -11
  9. package/README.md +2 -0
  10. package/bmad-core/agent-teams/team-all.yaml +1 -1
  11. package/bmad-core/agents/bmad-orchestrator.md +1 -1
  12. package/bmad-core/agents/dev.md +4 -4
  13. package/bmad-core/data/bmad-kb.md +1 -1
  14. package/bmad-core/data/test-levels-framework.md +12 -12
  15. package/bmad-core/tasks/facilitate-brainstorming-session.md +1 -1
  16. package/bmad-core/tasks/nfr-assess.md +10 -10
  17. package/bmad-core/tasks/qa-gate.md +23 -23
  18. package/bmad-core/tasks/review-story.md +18 -18
  19. package/bmad-core/tasks/risk-profile.md +25 -25
  20. package/bmad-core/tasks/test-design.md +9 -9
  21. package/bmad-core/tasks/trace-requirements.md +21 -21
  22. package/bmad-core/templates/architecture-tmpl.yaml +49 -49
  23. package/bmad-core/templates/brainstorming-output-tmpl.yaml +5 -5
  24. package/bmad-core/templates/brownfield-architecture-tmpl.yaml +31 -31
  25. package/bmad-core/templates/brownfield-prd-tmpl.yaml +13 -13
  26. package/bmad-core/templates/competitor-analysis-tmpl.yaml +19 -6
  27. package/bmad-core/templates/front-end-architecture-tmpl.yaml +21 -9
  28. package/bmad-core/templates/front-end-spec-tmpl.yaml +24 -24
  29. package/bmad-core/templates/fullstack-architecture-tmpl.yaml +122 -104
  30. package/bmad-core/templates/market-research-tmpl.yaml +2 -2
  31. package/bmad-core/templates/prd-tmpl.yaml +9 -9
  32. package/bmad-core/templates/project-brief-tmpl.yaml +4 -4
  33. package/bmad-core/templates/qa-gate-tmpl.yaml +9 -9
  34. package/bmad-core/templates/story-tmpl.yaml +12 -12
  35. package/bmad-core/workflows/brownfield-fullstack.yaml +9 -9
  36. package/bmad-core/workflows/brownfield-service.yaml +1 -1
  37. package/bmad-core/workflows/brownfield-ui.yaml +1 -1
  38. package/bmad-core/workflows/greenfield-fullstack.yaml +1 -1
  39. package/bmad-core/workflows/greenfield-service.yaml +1 -1
  40. package/bmad-core/workflows/greenfield-ui.yaml +1 -1
  41. package/common/utils/bmad-doc-template.md +5 -5
  42. package/dist/agents/analyst.txt +28 -15
  43. package/dist/agents/architect.txt +220 -190
  44. package/dist/agents/bmad-master.txt +298 -255
  45. package/dist/agents/bmad-orchestrator.txt +1 -1
  46. package/dist/agents/pm.txt +20 -20
  47. package/dist/agents/po.txt +11 -11
  48. package/dist/agents/qa.txt +275 -618
  49. package/dist/agents/sm.txt +11 -11
  50. package/dist/agents/ux-expert.txt +23 -23
  51. package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-designer.txt +109 -109
  52. package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-developer.txt +75 -77
  53. package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-sm.txt +41 -41
  54. package/dist/expansion-packs/bmad-2d-phaser-game-dev/teams/phaser-2d-nodejs-game-team.txt +483 -474
  55. package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-architect.txt +1 -1
  56. package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-designer.txt +149 -149
  57. package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-sm.txt +20 -20
  58. package/dist/expansion-packs/bmad-2d-unity-game-dev/teams/unity-2d-game-team.txt +371 -358
  59. package/dist/expansion-packs/bmad-infrastructure-devops/agents/infra-devops-platform.txt +25 -25
  60. package/dist/teams/team-all.txt +581 -881
  61. package/dist/teams/team-fullstack.txt +316 -273
  62. package/dist/teams/team-ide-minimal.txt +276 -619
  63. package/dist/teams/team-no-ui.txt +281 -238
  64. package/docs/versioning-and-releases.md +114 -44
  65. package/eslint.config.mjs +119 -0
  66. 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 -26
  67. package/expansion-packs/bmad-2d-phaser-game-dev/agents/game-developer.md +4 -4
  68. package/expansion-packs/bmad-2d-phaser-game-dev/agents/game-sm.md +1 -1
  69. package/expansion-packs/bmad-2d-phaser-game-dev/data/development-guidelines.md +26 -28
  70. package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-architecture-tmpl.yaml +50 -50
  71. package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-brief-tmpl.yaml +23 -23
  72. package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-design-doc-tmpl.yaml +24 -24
  73. package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-story-tmpl.yaml +42 -42
  74. package/expansion-packs/bmad-2d-phaser-game-dev/templates/level-design-doc-tmpl.yaml +65 -65
  75. package/expansion-packs/bmad-2d-phaser-game-dev/workflows/game-dev-greenfield.yaml +5 -5
  76. package/expansion-packs/bmad-2d-phaser-game-dev/workflows/game-prototype.yaml +1 -1
  77. package/expansion-packs/bmad-2d-unity-game-dev/agents/game-developer.md +3 -3
  78. package/expansion-packs/bmad-2d-unity-game-dev/data/bmad-kb.md +1 -1
  79. package/expansion-packs/bmad-2d-unity-game-dev/templates/game-brief-tmpl.yaml +23 -23
  80. package/expansion-packs/bmad-2d-unity-game-dev/templates/game-design-doc-tmpl.yaml +63 -63
  81. package/expansion-packs/bmad-2d-unity-game-dev/templates/game-story-tmpl.yaml +20 -20
  82. package/expansion-packs/bmad-2d-unity-game-dev/templates/level-design-doc-tmpl.yaml +65 -65
  83. package/expansion-packs/bmad-2d-unity-game-dev/workflows/game-dev-greenfield.yaml +5 -5
  84. package/expansion-packs/bmad-2d-unity-game-dev/workflows/game-prototype.yaml +1 -1
  85. package/expansion-packs/bmad-infrastructure-devops/templates/infrastructure-architecture-tmpl.yaml +20 -20
  86. package/expansion-packs/bmad-infrastructure-devops/templates/infrastructure-platform-from-arch-tmpl.yaml +7 -7
  87. package/package.json +62 -39
  88. package/prettier.config.mjs +32 -0
  89. package/release_notes.md +30 -0
  90. package/tools/bmad-npx-wrapper.js +10 -10
  91. package/tools/builders/web-builder.js +124 -130
  92. package/tools/bump-all-versions.js +42 -33
  93. package/tools/bump-expansion-version.js +23 -16
  94. package/tools/cli.js +10 -12
  95. package/tools/flattener/aggregate.js +10 -10
  96. package/tools/flattener/binary.js +44 -17
  97. package/tools/flattener/discovery.js +19 -18
  98. package/tools/flattener/files.js +6 -6
  99. package/tools/flattener/ignoreRules.js +125 -125
  100. package/tools/flattener/main.js +201 -304
  101. package/tools/flattener/projectRoot.js +75 -73
  102. package/tools/flattener/prompts.js +9 -9
  103. package/tools/flattener/stats.helpers.js +131 -67
  104. package/tools/flattener/stats.js +3 -3
  105. package/tools/flattener/test-matrix.js +201 -193
  106. package/tools/flattener/xml.js +33 -31
  107. package/tools/installer/bin/bmad.js +130 -89
  108. package/tools/installer/config/ide-agent-config.yaml +1 -1
  109. package/tools/installer/config/install.config.yaml +2 -2
  110. package/tools/installer/lib/config-loader.js +46 -42
  111. package/tools/installer/lib/file-manager.js +91 -113
  112. package/tools/installer/lib/ide-base-setup.js +57 -56
  113. package/tools/installer/lib/ide-setup.js +375 -343
  114. package/tools/installer/lib/installer.js +875 -714
  115. package/tools/installer/lib/memory-profiler.js +54 -53
  116. package/tools/installer/lib/module-manager.js +19 -15
  117. package/tools/installer/lib/resource-locator.js +26 -28
  118. package/tools/installer/package.json +19 -19
  119. package/tools/lib/dependency-resolver.js +26 -30
  120. package/tools/lib/yaml-utils.js +7 -7
  121. package/tools/preview-release-notes.js +66 -0
  122. package/tools/shared/bannerArt.js +3 -3
  123. package/tools/sync-installer-version.js +7 -9
  124. package/tools/update-expansion-version.js +14 -15
  125. package/tools/upgraders/v3-to-v4-upgrader.js +203 -294
  126. package/tools/version-bump.js +41 -26
  127. package/tools/yaml-format.js +56 -43
  128. package/.github/workflows/promote-to-stable.yml +0 -144
  129. package/.github/workflows/release.yaml +0 -60
  130. package/.releaserc.json +0 -21
  131. package/tools/semantic-release-sync-installer.js +0 -30
@@ -1,20 +1,14 @@
1
- #!/usr/bin/env node
2
-
3
- const { Command } = require("commander");
4
- const fs = require("fs-extra");
5
- const path = require("node:path");
6
- const process = require("node:process");
1
+ const { Command } = require('commander');
2
+ const fs = require('fs-extra');
3
+ const path = require('node:path');
4
+ const process = require('node:process');
7
5
 
8
6
  // 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");
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');
18
12
 
19
13
  /**
20
14
  * Recursively discover all files in a directory
@@ -73,30 +67,30 @@ const { calculateStatistics } = require("./stats.js");
73
67
  const program = new Command();
74
68
 
75
69
  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")
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')
81
75
  .action(async (options) => {
82
76
  let inputDir = path.resolve(options.input);
83
77
  let outputPath = path.resolve(options.output);
84
78
 
85
79
  // Detect if user explicitly provided -i/--input or -o/--output
86
80
  const argv = process.argv.slice(2);
87
- const userSpecifiedInput = argv.some((a) =>
88
- a === "-i" || a === "--input" || a.startsWith("--input=")
81
+ const userSpecifiedInput = argv.some(
82
+ (a) => a === '-i' || a === '--input' || a.startsWith('--input='),
89
83
  );
90
- const userSpecifiedOutput = argv.some((a) =>
91
- a === "-o" || a === "--output" || a.startsWith("--output=")
84
+ const userSpecifiedOutput = argv.some(
85
+ (a) => a === '-o' || a === '--output' || a.startsWith('--output='),
92
86
  );
93
- const noPathArgs = !userSpecifiedInput && !userSpecifiedOutput;
87
+ const noPathArguments = !userSpecifiedInput && !userSpecifiedOutput;
94
88
 
95
- if (noPathArgs) {
89
+ if (noPathArguments) {
96
90
  const detectedRoot = await findProjectRoot(process.cwd());
97
91
  const suggestedOutput = detectedRoot
98
- ? path.join(detectedRoot, "flattened-codebase.xml")
99
- : path.resolve("flattened-codebase.xml");
92
+ ? path.join(detectedRoot, 'flattened-codebase.xml')
93
+ : path.resolve('flattened-codebase.xml');
100
94
 
101
95
  if (detectedRoot) {
102
96
  const useDefaults = await promptYesNo(
@@ -107,26 +101,25 @@ program
107
101
  inputDir = detectedRoot;
108
102
  outputPath = suggestedOutput;
109
103
  } else {
110
- inputDir = await promptPath(
111
- "Enter input directory path",
112
- process.cwd(),
113
- );
104
+ inputDir = await promptPath('Enter input directory path', process.cwd());
114
105
  outputPath = await promptPath(
115
- "Enter output file path",
116
- path.join(inputDir, "flattened-codebase.xml"),
106
+ 'Enter output file path',
107
+ path.join(inputDir, 'flattened-codebase.xml'),
117
108
  );
118
109
  }
119
110
  } else {
120
- console.log("Could not auto-detect a project root.");
121
- inputDir = await promptPath(
122
- "Enter input directory path",
123
- process.cwd(),
124
- );
111
+ console.log('Could not auto-detect a project root.');
112
+ inputDir = await promptPath('Enter input directory path', process.cwd());
125
113
  outputPath = await promptPath(
126
- "Enter output file path",
127
- path.join(inputDir, "flattened-codebase.xml"),
114
+ 'Enter output file path',
115
+ path.join(inputDir, 'flattened-codebase.xml'),
128
116
  );
129
117
  }
118
+ } else {
119
+ console.error(
120
+ 'Could not auto-detect a project root and no arguments were provided. Please specify -i/--input and -o/--output.',
121
+ );
122
+ process.exit(1);
130
123
  }
131
124
 
132
125
  // Ensure output directory exists
@@ -134,24 +127,23 @@ program
134
127
 
135
128
  try {
136
129
  // Verify input directory exists
137
- if (!await fs.pathExists(inputDir)) {
130
+ if (!(await fs.pathExists(inputDir))) {
138
131
  console.error(`โŒ Error: Input directory does not exist: ${inputDir}`);
139
132
  process.exit(1);
140
133
  }
141
134
 
142
135
  // Import ora dynamically
143
- const { default: ora } = await import("ora");
136
+ const { default: ora } = await import('ora');
144
137
 
145
138
  // Start file discovery with spinner
146
- const discoverySpinner = ora("๐Ÿ” Discovering files...").start();
139
+ const discoverySpinner = ora('๐Ÿ” Discovering files...').start();
147
140
  const files = await discoverFiles(inputDir);
148
141
  const filteredFiles = await filterFiles(files, inputDir);
149
- discoverySpinner.succeed(
150
- `๐Ÿ“ Found ${filteredFiles.length} files to include`,
151
- );
142
+ discoverySpinner.succeed(`๐Ÿ“ Found ${filteredFiles.length} files to include`);
152
143
 
153
144
  // Process files with progress tracking
154
- const processingSpinner = ora("๐Ÿ“„ Processing files...").start();
145
+ console.log('Reading file contents');
146
+ const processingSpinner = ora('๐Ÿ“„ Processing files...').start();
155
147
  const aggregatedContent = await aggregateFileContents(
156
148
  filteredFiles,
157
149
  inputDir,
@@ -165,31 +157,23 @@ program
165
157
  }
166
158
 
167
159
  // Generate XML output using streaming
168
- const xmlSpinner = ora("๐Ÿ”ง Generating XML output...").start();
160
+ const xmlSpinner = ora('๐Ÿ”ง Generating XML output...').start();
169
161
  await generateXMLOutput(aggregatedContent, outputPath);
170
- xmlSpinner.succeed("๐Ÿ“ XML generation completed");
162
+ xmlSpinner.succeed('๐Ÿ“ XML generation completed');
171
163
 
172
164
  // Calculate and display statistics
173
165
  const outputStats = await fs.stat(outputPath);
174
- const stats = await calculateStatistics(
175
- aggregatedContent,
176
- outputStats.size,
177
- inputDir,
178
- );
166
+ const stats = await calculateStatistics(aggregatedContent, outputStats.size, inputDir);
179
167
 
180
168
  // Display completion summary
181
- console.log("\n๐Ÿ“Š Completion Summary:");
169
+ console.log('\n๐Ÿ“Š Completion Summary:');
182
170
  console.log(
183
- `โœ… Successfully processed ${filteredFiles.length} files into ${
184
- path.basename(outputPath)
185
- }`,
171
+ `โœ… Successfully processed ${filteredFiles.length} files into ${path.basename(outputPath)}`,
186
172
  );
187
173
  console.log(`๐Ÿ“ Output file: ${outputPath}`);
188
174
  console.log(`๐Ÿ“ Total source size: ${stats.totalSize}`);
189
175
  console.log(`๐Ÿ“„ Generated XML size: ${stats.xmlSize}`);
190
- console.log(
191
- `๐Ÿ“ Total lines of code: ${stats.totalLines.toLocaleString()}`,
192
- );
176
+ console.log(`๐Ÿ“ Total lines of code: ${stats.totalLines.toLocaleString()}`);
193
177
  console.log(`๐Ÿ”ข Estimated tokens: ${stats.estimatedTokens}`);
194
178
  console.log(
195
179
  `๐Ÿ“Š File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors\n`,
@@ -197,92 +181,75 @@ program
197
181
 
198
182
  // Ask user if they want detailed stats + markdown report
199
183
  const generateDetailed = await promptYesNo(
200
- "Generate detailed stats (console + markdown) now?",
184
+ 'Generate detailed stats (console + markdown) now?',
201
185
  true,
202
186
  );
203
187
 
204
188
  if (generateDetailed) {
205
189
  // Additional detailed stats
206
- console.log("\n๐Ÿ“ˆ Size Percentiles:");
190
+ console.log('\n๐Ÿ“ˆ Size Percentiles:');
207
191
  console.log(
208
- ` Avg: ${
209
- Math.round(stats.avgFileSize).toLocaleString()
210
- } B, Median: ${
211
- Math.round(stats.medianFileSize).toLocaleString()
212
- } B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`,
192
+ ` Avg: ${Math.round(stats.avgFileSize).toLocaleString()} B, Median: ${Math.round(
193
+ stats.medianFileSize,
194
+ ).toLocaleString()} B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`,
213
195
  );
214
196
 
215
- if (Array.isArray(stats.histogram) && stats.histogram.length) {
216
- console.log("\n๐Ÿงฎ Size Histogram:");
197
+ if (Array.isArray(stats.histogram) && stats.histogram.length > 0) {
198
+ console.log('\n๐Ÿงฎ Size Histogram:');
217
199
  for (const b of stats.histogram.slice(0, 2)) {
218
- console.log(
219
- ` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`,
220
- );
200
+ console.log(` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`);
221
201
  }
222
202
  if (stats.histogram.length > 2) {
223
203
  console.log(` โ€ฆ and ${stats.histogram.length - 2} more buckets`);
224
204
  }
225
205
  }
226
206
 
227
- if (Array.isArray(stats.byExtension) && stats.byExtension.length) {
207
+ if (Array.isArray(stats.byExtension) && stats.byExtension.length > 0) {
228
208
  const topExt = stats.byExtension.slice(0, 2);
229
- console.log("\n๐Ÿ“ฆ Top Extensions:");
209
+ console.log('\n๐Ÿ“ฆ Top Extensions:');
230
210
  for (const e of topExt) {
231
- const pct = stats.totalBytes
232
- ? ((e.bytes / stats.totalBytes) * 100)
233
- : 0;
211
+ const pct = stats.totalBytes ? (e.bytes / stats.totalBytes) * 100 : 0;
234
212
  console.log(
235
- ` ${e.ext}: ${e.count} files, ${e.bytes.toLocaleString()} bytes (${
236
- pct.toFixed(2)
237
- }%)`,
213
+ ` ${e.ext}: ${e.count} files, ${e.bytes.toLocaleString()} bytes (${pct.toFixed(
214
+ 2,
215
+ )}%)`,
238
216
  );
239
217
  }
240
218
  if (stats.byExtension.length > 2) {
241
- console.log(
242
- ` โ€ฆ and ${stats.byExtension.length - 2} more extensions`,
243
- );
219
+ console.log(` โ€ฆ and ${stats.byExtension.length - 2} more extensions`);
244
220
  }
245
221
  }
246
222
 
247
- if (Array.isArray(stats.byDirectory) && stats.byDirectory.length) {
223
+ if (Array.isArray(stats.byDirectory) && stats.byDirectory.length > 0) {
248
224
  const topDir = stats.byDirectory.slice(0, 2);
249
- console.log("\n๐Ÿ“‚ Top Directories:");
225
+ console.log('\n๐Ÿ“‚ Top Directories:');
250
226
  for (const d of topDir) {
251
- const pct = stats.totalBytes
252
- ? ((d.bytes / stats.totalBytes) * 100)
253
- : 0;
227
+ const pct = stats.totalBytes ? (d.bytes / stats.totalBytes) * 100 : 0;
254
228
  console.log(
255
- ` ${d.dir}: ${d.count} files, ${d.bytes.toLocaleString()} bytes (${
256
- pct.toFixed(2)
257
- }%)`,
229
+ ` ${d.dir}: ${d.count} files, ${d.bytes.toLocaleString()} bytes (${pct.toFixed(
230
+ 2,
231
+ )}%)`,
258
232
  );
259
233
  }
260
234
  if (stats.byDirectory.length > 2) {
261
- console.log(
262
- ` โ€ฆ and ${stats.byDirectory.length - 2} more directories`,
263
- );
235
+ console.log(` โ€ฆ and ${stats.byDirectory.length - 2} more directories`);
264
236
  }
265
237
  }
266
238
 
267
- if (
268
- Array.isArray(stats.depthDistribution) &&
269
- stats.depthDistribution.length
270
- ) {
271
- console.log("\n๐ŸŒณ Depth Distribution:");
239
+ if (Array.isArray(stats.depthDistribution) && stats.depthDistribution.length > 0) {
240
+ console.log('\n๐ŸŒณ Depth Distribution:');
272
241
  const dd = stats.depthDistribution.slice(0, 2);
273
- let line = " " + dd.map((d) => `${d.depth}:${d.count}`).join(" ");
242
+ let line = ' ' + dd.map((d) => `${d.depth}:${d.count}`).join(' ');
274
243
  if (stats.depthDistribution.length > 2) {
275
244
  line += ` โ€ฆ +${stats.depthDistribution.length - 2} more`;
276
245
  }
277
246
  console.log(line);
278
247
  }
279
248
 
280
- if (Array.isArray(stats.longestPaths) && stats.longestPaths.length) {
281
- console.log("\n๐Ÿงต Longest Paths:");
249
+ if (Array.isArray(stats.longestPaths) && stats.longestPaths.length > 0) {
250
+ console.log('\n๐Ÿงต Longest Paths:');
282
251
  for (const p of stats.longestPaths.slice(0, 2)) {
283
- console.log(
284
- ` ${p.path} (${p.length} chars, ${p.size.toLocaleString()} bytes)`,
285
- );
252
+ console.log(` ${p.path} (${p.length} chars, ${p.size.toLocaleString()} bytes)`);
286
253
  }
287
254
  if (stats.longestPaths.length > 2) {
288
255
  console.log(` โ€ฆ and ${stats.longestPaths.length - 2} more paths`);
@@ -290,7 +257,7 @@ program
290
257
  }
291
258
 
292
259
  if (stats.temporal) {
293
- console.log("\nโฑ๏ธ Temporal:");
260
+ console.log('\nโฑ๏ธ Temporal:');
294
261
  if (stats.temporal.oldest) {
295
262
  console.log(
296
263
  ` Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`,
@@ -302,104 +269,82 @@ program
302
269
  );
303
270
  }
304
271
  if (Array.isArray(stats.temporal.ageBuckets)) {
305
- console.log(" Age buckets:");
272
+ console.log(' Age buckets:');
306
273
  for (const b of stats.temporal.ageBuckets.slice(0, 2)) {
307
- console.log(
308
- ` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`,
309
- );
274
+ console.log(` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`);
310
275
  }
311
276
  if (stats.temporal.ageBuckets.length > 2) {
312
- console.log(
313
- ` โ€ฆ and ${
314
- stats.temporal.ageBuckets.length - 2
315
- } more buckets`,
316
- );
277
+ console.log(` โ€ฆ and ${stats.temporal.ageBuckets.length - 2} more buckets`);
317
278
  }
318
279
  }
319
280
  }
320
281
 
321
282
  if (stats.quality) {
322
- console.log("\nโœ… Quality Signals:");
283
+ console.log('\nโœ… Quality Signals:');
323
284
  console.log(` Zero-byte files: ${stats.quality.zeroByteFiles}`);
324
285
  console.log(` Empty text files: ${stats.quality.emptyTextFiles}`);
325
286
  console.log(` Hidden files: ${stats.quality.hiddenFiles}`);
326
287
  console.log(` Symlinks: ${stats.quality.symlinks}`);
327
288
  console.log(
328
- ` Large files (>= ${
329
- (stats.quality.largeThreshold / (1024 * 1024)).toFixed(0)
330
- } MB): ${stats.quality.largeFilesCount}`,
289
+ ` Large files (>= ${(stats.quality.largeThreshold / (1024 * 1024)).toFixed(
290
+ 0,
291
+ )} MB): ${stats.quality.largeFilesCount}`,
331
292
  );
332
293
  console.log(
333
294
  ` Suspiciously large files (>= 100 MB): ${stats.quality.suspiciousLargeFilesCount}`,
334
295
  );
335
296
  }
336
297
 
337
- if (
338
- Array.isArray(stats.duplicateCandidates) &&
339
- stats.duplicateCandidates.length
340
- ) {
341
- console.log("\n๐Ÿงฌ Duplicate Candidates:");
298
+ if (Array.isArray(stats.duplicateCandidates) && stats.duplicateCandidates.length > 0) {
299
+ console.log('\n๐Ÿงฌ Duplicate Candidates:');
342
300
  for (const d of stats.duplicateCandidates.slice(0, 2)) {
343
- console.log(
344
- ` ${d.reason}: ${d.count} files @ ${d.size.toLocaleString()} bytes`,
345
- );
301
+ console.log(` ${d.reason}: ${d.count} files @ ${d.size.toLocaleString()} bytes`);
346
302
  }
347
303
  if (stats.duplicateCandidates.length > 2) {
348
- console.log(
349
- ` โ€ฆ and ${stats.duplicateCandidates.length - 2} more groups`,
350
- );
304
+ console.log(` โ€ฆ and ${stats.duplicateCandidates.length - 2} more groups`);
351
305
  }
352
306
  }
353
307
 
354
- if (typeof stats.compressibilityRatio === "number") {
308
+ if (typeof stats.compressibilityRatio === 'number') {
355
309
  console.log(
356
- `\n๐Ÿ—œ๏ธ Compressibility ratio (sampled): ${
357
- (stats.compressibilityRatio * 100).toFixed(2)
358
- }%`,
310
+ `\n๐Ÿ—œ๏ธ Compressibility ratio (sampled): ${(stats.compressibilityRatio * 100).toFixed(
311
+ 2,
312
+ )}%`,
359
313
  );
360
314
  }
361
315
 
362
316
  if (stats.git && stats.git.isRepo) {
363
- console.log("\n๐Ÿ”ง Git:");
317
+ console.log('\n๐Ÿ”ง Git:');
364
318
  console.log(
365
319
  ` Tracked: ${stats.git.trackedCount} files, ${stats.git.trackedBytes.toLocaleString()} bytes`,
366
320
  );
367
321
  console.log(
368
322
  ` Untracked: ${stats.git.untrackedCount} files, ${stats.git.untrackedBytes.toLocaleString()} bytes`,
369
323
  );
370
- if (
371
- Array.isArray(stats.git.lfsCandidates) &&
372
- stats.git.lfsCandidates.length
373
- ) {
374
- console.log(" LFS candidates (top 2):");
324
+ if (Array.isArray(stats.git.lfsCandidates) && stats.git.lfsCandidates.length > 0) {
325
+ console.log(' LFS candidates (top 2):');
375
326
  for (const f of stats.git.lfsCandidates.slice(0, 2)) {
376
327
  console.log(` ${f.path} (${f.size.toLocaleString()} bytes)`);
377
328
  }
378
329
  if (stats.git.lfsCandidates.length > 2) {
379
- console.log(
380
- ` โ€ฆ and ${stats.git.lfsCandidates.length - 2} more`,
381
- );
330
+ console.log(` โ€ฆ and ${stats.git.lfsCandidates.length - 2} more`);
382
331
  }
383
332
  }
384
333
  }
385
334
 
386
- if (Array.isArray(stats.largestFiles) && stats.largestFiles.length) {
387
- console.log("\n๐Ÿ“š Largest Files (top 2):");
335
+ if (Array.isArray(stats.largestFiles) && stats.largestFiles.length > 0) {
336
+ console.log('\n๐Ÿ“š Largest Files (top 2):');
388
337
  for (const f of stats.largestFiles.slice(0, 2)) {
389
338
  // Show LOC for text files when available; omit ext and mtime
390
- let locStr = "";
339
+ let locStr = '';
391
340
  if (!f.isBinary && Array.isArray(aggregatedContent?.textFiles)) {
392
- const tf = aggregatedContent.textFiles.find((t) =>
393
- t.path === f.path
394
- );
395
- if (tf && typeof tf.lines === "number") {
341
+ const tf = aggregatedContent.textFiles.find((t) => t.path === f.path);
342
+ if (tf && typeof tf.lines === 'number') {
396
343
  locStr = `, LOC: ${tf.lines.toLocaleString()}`;
397
344
  }
398
345
  }
399
346
  console.log(
400
- ` ${f.path} โ€“ ${f.sizeFormatted} (${
401
- f.percentOfTotal.toFixed(2)
402
- }%)${locStr}`,
347
+ ` ${f.path} โ€“ ${f.sizeFormatted} (${f.percentOfTotal.toFixed(2)}%)${locStr}`,
403
348
  );
404
349
  }
405
350
  if (stats.largestFiles.length > 2) {
@@ -409,262 +354,214 @@ program
409
354
 
410
355
  // Write a comprehensive markdown report next to the XML
411
356
  {
412
- const mdPath = outputPath.endsWith(".xml")
413
- ? outputPath.replace(/\.xml$/i, ".stats.md")
414
- : outputPath + ".stats.md";
357
+ const mdPath = outputPath.endsWith('.xml')
358
+ ? outputPath.replace(/\.xml$/i, '.stats.md')
359
+ : outputPath + '.stats.md';
415
360
  try {
416
- const pct = (num, den) => (den ? ((num / den) * 100) : 0);
361
+ const pct = (num, den) => (den ? (num / den) * 100 : 0);
417
362
  const md = [];
418
- md.push(`# ๐Ÿงพ Flatten Stats for ${path.basename(outputPath)}`);
419
- md.push("");
420
- md.push("## ๐Ÿ“Š Summary");
421
- md.push(`- Total source size: ${stats.totalSize}`);
422
- md.push(`- Generated XML size: ${stats.xmlSize}`);
423
363
  md.push(
364
+ `# ๐Ÿงพ Flatten Stats for ${path.basename(outputPath)}`,
365
+ '',
366
+ '## ๐Ÿ“Š Summary',
367
+ `- Total source size: ${stats.totalSize}`,
368
+ `- Generated XML size: ${stats.xmlSize}`,
424
369
  `- Total lines of code: ${stats.totalLines.toLocaleString()}`,
425
- );
426
- md.push(`- Estimated tokens: ${stats.estimatedTokens}`);
427
- md.push(
370
+ `- Estimated tokens: ${stats.estimatedTokens}`,
428
371
  `- File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`,
372
+ '',
373
+ '## ๐Ÿ“ˆ Size Percentiles',
374
+ `Avg: ${Math.round(stats.avgFileSize).toLocaleString()} B, Median: ${Math.round(
375
+ stats.medianFileSize,
376
+ ).toLocaleString()} B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`,
377
+ '',
429
378
  );
430
- md.push("");
431
-
432
- // Percentiles
433
- md.push("## ๐Ÿ“ˆ Size Percentiles");
434
- md.push(
435
- `Avg: ${
436
- Math.round(stats.avgFileSize).toLocaleString()
437
- } B, Median: ${
438
- Math.round(stats.medianFileSize).toLocaleString()
439
- } B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`,
440
- );
441
- md.push("");
442
379
 
443
380
  // Histogram
444
- if (Array.isArray(stats.histogram) && stats.histogram.length) {
445
- md.push("## ๐Ÿงฎ Size Histogram");
446
- md.push("| Bucket | Files | Bytes |");
447
- md.push("| --- | ---: | ---: |");
381
+ if (Array.isArray(stats.histogram) && stats.histogram.length > 0) {
382
+ md.push(
383
+ '## ๐Ÿงฎ Size Histogram',
384
+ '| Bucket | Files | Bytes |',
385
+ '| --- | ---: | ---: |',
386
+ );
448
387
  for (const b of stats.histogram) {
449
- md.push(
450
- `| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`,
451
- );
388
+ md.push(`| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`);
452
389
  }
453
- md.push("");
390
+ md.push('');
454
391
  }
455
392
 
456
393
  // Top Extensions
457
- if (Array.isArray(stats.byExtension) && stats.byExtension.length) {
458
- md.push("## ๐Ÿ“ฆ Top Extensions by Bytes (Top 20)");
459
- md.push("| Ext | Files | Bytes | % of total |");
460
- md.push("| --- | ---: | ---: | ---: |");
394
+ if (Array.isArray(stats.byExtension) && stats.byExtension.length > 0) {
395
+ md.push(
396
+ '## ๐Ÿ“ฆ Top Extensions by Bytes (Top 20)',
397
+ '| Ext | Files | Bytes | % of total |',
398
+ '| --- | ---: | ---: | ---: |',
399
+ );
461
400
  for (const e of stats.byExtension.slice(0, 20)) {
462
401
  const p = pct(e.bytes, stats.totalBytes);
463
402
  md.push(
464
- `| ${e.ext} | ${e.count} | ${e.bytes.toLocaleString()} | ${
465
- p.toFixed(2)
466
- }% |`,
403
+ `| ${e.ext} | ${e.count} | ${e.bytes.toLocaleString()} | ${p.toFixed(2)}% |`,
467
404
  );
468
405
  }
469
- md.push("");
406
+ md.push('');
470
407
  }
471
408
 
472
409
  // Top Directories
473
- if (Array.isArray(stats.byDirectory) && stats.byDirectory.length) {
474
- md.push("## ๐Ÿ“‚ Top Directories by Bytes (Top 20)");
475
- md.push("| Directory | Files | Bytes | % of total |");
476
- md.push("| --- | ---: | ---: | ---: |");
410
+ if (Array.isArray(stats.byDirectory) && stats.byDirectory.length > 0) {
411
+ md.push(
412
+ '## ๐Ÿ“‚ Top Directories by Bytes (Top 20)',
413
+ '| Directory | Files | Bytes | % of total |',
414
+ '| --- | ---: | ---: | ---: |',
415
+ );
477
416
  for (const d of stats.byDirectory.slice(0, 20)) {
478
417
  const p = pct(d.bytes, stats.totalBytes);
479
418
  md.push(
480
- `| ${d.dir} | ${d.count} | ${d.bytes.toLocaleString()} | ${
481
- p.toFixed(2)
482
- }% |`,
419
+ `| ${d.dir} | ${d.count} | ${d.bytes.toLocaleString()} | ${p.toFixed(2)}% |`,
483
420
  );
484
421
  }
485
- md.push("");
422
+ md.push('');
486
423
  }
487
424
 
488
425
  // Depth distribution
489
- if (
490
- Array.isArray(stats.depthDistribution) &&
491
- stats.depthDistribution.length
492
- ) {
493
- md.push("## ๐ŸŒณ Depth Distribution");
494
- md.push("| Depth | Count |");
495
- md.push("| ---: | ---: |");
426
+ if (Array.isArray(stats.depthDistribution) && stats.depthDistribution.length > 0) {
427
+ md.push('## ๐ŸŒณ Depth Distribution', '| Depth | Count |', '| ---: | ---: |');
496
428
  for (const d of stats.depthDistribution) {
497
429
  md.push(`| ${d.depth} | ${d.count} |`);
498
430
  }
499
- md.push("");
431
+ md.push('');
500
432
  }
501
433
 
502
434
  // Longest paths
503
- if (
504
- Array.isArray(stats.longestPaths) && stats.longestPaths.length
505
- ) {
506
- md.push("## ๐Ÿงต Longest Paths (Top 25)");
507
- md.push("| Path | Length | Bytes |");
508
- md.push("| --- | ---: | ---: |");
435
+ if (Array.isArray(stats.longestPaths) && stats.longestPaths.length > 0) {
436
+ md.push(
437
+ '## ๐Ÿงต Longest Paths (Top 25)',
438
+ '| Path | Length | Bytes |',
439
+ '| --- | ---: | ---: |',
440
+ );
509
441
  for (const pth of stats.longestPaths) {
510
- md.push(
511
- `| ${pth.path} | ${pth.length} | ${pth.size.toLocaleString()} |`,
512
- );
442
+ md.push(`| ${pth.path} | ${pth.length} | ${pth.size.toLocaleString()} |`);
513
443
  }
514
- md.push("");
444
+ md.push('');
515
445
  }
516
446
 
517
447
  // Temporal
518
448
  if (stats.temporal) {
519
- md.push("## โฑ๏ธ Temporal");
449
+ md.push('## โฑ๏ธ Temporal');
520
450
  if (stats.temporal.oldest) {
521
- md.push(
522
- `- Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`,
523
- );
451
+ md.push(`- Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`);
524
452
  }
525
453
  if (stats.temporal.newest) {
526
- md.push(
527
- `- Newest: ${stats.temporal.newest.path} (${stats.temporal.newest.mtime})`,
528
- );
454
+ md.push(`- Newest: ${stats.temporal.newest.path} (${stats.temporal.newest.mtime})`);
529
455
  }
530
456
  if (Array.isArray(stats.temporal.ageBuckets)) {
531
- md.push("");
532
- md.push("| Age | Files | Bytes |");
533
- md.push("| --- | ---: | ---: |");
457
+ md.push('', '| Age | Files | Bytes |', '| --- | ---: | ---: |');
534
458
  for (const b of stats.temporal.ageBuckets) {
535
- md.push(
536
- `| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`,
537
- );
459
+ md.push(`| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`);
538
460
  }
539
461
  }
540
- md.push("");
462
+ md.push('');
541
463
  }
542
464
 
543
465
  // Quality signals
544
466
  if (stats.quality) {
545
- md.push("## โœ… Quality Signals");
546
- md.push(`- Zero-byte files: ${stats.quality.zeroByteFiles}`);
547
- md.push(`- Empty text files: ${stats.quality.emptyTextFiles}`);
548
- md.push(`- Hidden files: ${stats.quality.hiddenFiles}`);
549
- md.push(`- Symlinks: ${stats.quality.symlinks}`);
550
- md.push(
551
- `- Large files (>= ${
552
- (stats.quality.largeThreshold / (1024 * 1024)).toFixed(0)
553
- } MB): ${stats.quality.largeFilesCount}`,
554
- );
555
467
  md.push(
468
+ '## โœ… Quality Signals',
469
+ `- Zero-byte files: ${stats.quality.zeroByteFiles}`,
470
+ `- Empty text files: ${stats.quality.emptyTextFiles}`,
471
+ `- Hidden files: ${stats.quality.hiddenFiles}`,
472
+ `- Symlinks: ${stats.quality.symlinks}`,
473
+ `- Large files (>= ${(stats.quality.largeThreshold / (1024 * 1024)).toFixed(0)} MB): ${stats.quality.largeFilesCount}`,
556
474
  `- Suspiciously large files (>= 100 MB): ${stats.quality.suspiciousLargeFilesCount}`,
475
+ '',
557
476
  );
558
- md.push("");
559
477
  }
560
478
 
561
479
  // Duplicates
562
- if (
563
- Array.isArray(stats.duplicateCandidates) &&
564
- stats.duplicateCandidates.length
565
- ) {
566
- md.push("## ๐Ÿงฌ Duplicate Candidates");
567
- md.push("| Reason | Files | Size (bytes) |");
568
- md.push("| --- | ---: | ---: |");
480
+ if (Array.isArray(stats.duplicateCandidates) && stats.duplicateCandidates.length > 0) {
481
+ md.push(
482
+ '## ๐Ÿงฌ Duplicate Candidates',
483
+ '| Reason | Files | Size (bytes) |',
484
+ '| --- | ---: | ---: |',
485
+ );
569
486
  for (const d of stats.duplicateCandidates) {
570
- md.push(
571
- `| ${d.reason} | ${d.count} | ${d.size.toLocaleString()} |`,
572
- );
487
+ md.push(`| ${d.reason} | ${d.count} | ${d.size.toLocaleString()} |`);
573
488
  }
574
- md.push("");
575
- // Detailed listing of duplicate file names and locations
576
- md.push("### ๐Ÿงฌ Duplicate Groups Details");
489
+ md.push('', '### ๐Ÿงฌ Duplicate Groups Details');
577
490
  let dupIndex = 1;
578
491
  for (const d of stats.duplicateCandidates) {
579
492
  md.push(
580
493
  `#### Group ${dupIndex}: ${d.count} files @ ${d.size.toLocaleString()} bytes (${d.reason})`,
581
494
  );
582
- if (Array.isArray(d.files) && d.files.length) {
495
+ if (Array.isArray(d.files) && d.files.length > 0) {
583
496
  for (const fp of d.files) {
584
497
  md.push(`- ${fp}`);
585
498
  }
586
499
  } else {
587
- md.push("- (file list unavailable)");
500
+ md.push('- (file list unavailable)');
588
501
  }
589
- md.push("");
502
+ md.push('');
590
503
  dupIndex++;
591
504
  }
592
- md.push("");
505
+ md.push('');
593
506
  }
594
507
 
595
508
  // Compressibility
596
- if (typeof stats.compressibilityRatio === "number") {
597
- md.push("## ๐Ÿ—œ๏ธ Compressibility");
509
+ if (typeof stats.compressibilityRatio === 'number') {
598
510
  md.push(
599
- `Sampled compressibility ratio: ${
600
- (stats.compressibilityRatio * 100).toFixed(2)
601
- }%`,
511
+ '## ๐Ÿ—œ๏ธ Compressibility',
512
+ `Sampled compressibility ratio: ${(stats.compressibilityRatio * 100).toFixed(2)}%`,
513
+ '',
602
514
  );
603
- md.push("");
604
515
  }
605
516
 
606
517
  // Git
607
518
  if (stats.git && stats.git.isRepo) {
608
- md.push("## ๐Ÿ”ง Git");
609
519
  md.push(
520
+ '## ๐Ÿ”ง Git',
610
521
  `- Tracked: ${stats.git.trackedCount} files, ${stats.git.trackedBytes.toLocaleString()} bytes`,
611
- );
612
- md.push(
613
522
  `- Untracked: ${stats.git.untrackedCount} files, ${stats.git.untrackedBytes.toLocaleString()} bytes`,
614
523
  );
615
- if (
616
- Array.isArray(stats.git.lfsCandidates) &&
617
- stats.git.lfsCandidates.length
618
- ) {
619
- md.push("");
620
- md.push("### ๐Ÿ“ฆ LFS Candidates (Top 20)");
621
- md.push("| Path | Bytes |");
622
- md.push("| --- | ---: |");
524
+ if (Array.isArray(stats.git.lfsCandidates) && stats.git.lfsCandidates.length > 0) {
525
+ md.push('', '### ๐Ÿ“ฆ LFS Candidates (Top 20)', '| Path | Bytes |', '| --- | ---: |');
623
526
  for (const f of stats.git.lfsCandidates.slice(0, 20)) {
624
527
  md.push(`| ${f.path} | ${f.size.toLocaleString()} |`);
625
528
  }
626
529
  }
627
- md.push("");
530
+ md.push('');
628
531
  }
629
532
 
630
533
  // Largest Files
631
- if (
632
- Array.isArray(stats.largestFiles) && stats.largestFiles.length
633
- ) {
634
- md.push("## ๐Ÿ“š Largest Files (Top 50)");
635
- md.push("| Path | Size | % of total | LOC |");
636
- md.push("| --- | ---: | ---: | ---: |");
534
+ if (Array.isArray(stats.largestFiles) && stats.largestFiles.length > 0) {
535
+ md.push(
536
+ '## ๐Ÿ“š Largest Files (Top 50)',
537
+ '| Path | Size | % of total | LOC |',
538
+ '| --- | ---: | ---: | ---: |',
539
+ );
637
540
  for (const f of stats.largestFiles) {
638
- let loc = "";
639
- if (
640
- !f.isBinary && Array.isArray(aggregatedContent?.textFiles)
641
- ) {
642
- const tf = aggregatedContent.textFiles.find((t) =>
643
- t.path === f.path
644
- );
645
- if (tf && typeof tf.lines === "number") {
541
+ let loc = '';
542
+ if (!f.isBinary && Array.isArray(aggregatedContent?.textFiles)) {
543
+ const tf = aggregatedContent.textFiles.find((t) => t.path === f.path);
544
+ if (tf && typeof tf.lines === 'number') {
646
545
  loc = tf.lines.toLocaleString();
647
546
  }
648
547
  }
649
548
  md.push(
650
- `| ${f.path} | ${f.sizeFormatted} | ${
651
- f.percentOfTotal.toFixed(2)
652
- }% | ${loc} |`,
549
+ `| ${f.path} | ${f.sizeFormatted} | ${f.percentOfTotal.toFixed(2)}% | ${loc} |`,
653
550
  );
654
551
  }
655
- md.push("");
552
+ md.push('');
656
553
  }
657
554
 
658
- await fs.writeFile(mdPath, md.join("\n"));
555
+ await fs.writeFile(mdPath, md.join('\n'));
659
556
  console.log(`\n๐Ÿงพ Detailed stats report written to: ${mdPath}`);
660
- } catch (e) {
661
- console.warn(`โš ๏ธ Failed to write stats markdown: ${e.message}`);
557
+ } catch (error) {
558
+ console.warn(`โš ๏ธ Failed to write stats markdown: ${error.message}`);
662
559
  }
663
560
  }
664
561
  }
665
562
  } catch (error) {
666
- console.error("โŒ Critical error:", error.message);
667
- console.error("An unexpected error occurred.");
563
+ console.error('โŒ Critical error:', error.message);
564
+ console.error('An unexpected error occurred.');
668
565
  process.exit(1);
669
566
  }
670
567
  });