bmad-method 4.37.0 → 4.39.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 (251) 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 +2 -23
  9. package/README.md +2 -0
  10. package/bmad-core/agent-teams/team-all.yaml +1 -1
  11. package/bmad-core/agents/analyst.md +16 -15
  12. package/bmad-core/agents/architect.md +11 -11
  13. package/bmad-core/agents/bmad-master.md +23 -22
  14. package/bmad-core/agents/bmad-orchestrator.md +13 -17
  15. package/bmad-core/agents/dev.md +14 -11
  16. package/bmad-core/agents/pm.md +15 -14
  17. package/bmad-core/agents/po.md +9 -8
  18. package/bmad-core/agents/qa.md +42 -22
  19. package/bmad-core/agents/sm.md +7 -6
  20. package/bmad-core/agents/ux-expert.md +6 -5
  21. package/bmad-core/core-config.yaml +2 -0
  22. package/bmad-core/data/bmad-kb.md +1 -1
  23. package/bmad-core/data/test-levels-framework.md +146 -0
  24. package/bmad-core/data/test-priorities-matrix.md +172 -0
  25. package/bmad-core/tasks/apply-qa-fixes.md +148 -0
  26. package/bmad-core/tasks/facilitate-brainstorming-session.md +1 -1
  27. package/bmad-core/tasks/nfr-assess.md +343 -0
  28. package/bmad-core/tasks/qa-gate.md +161 -0
  29. package/bmad-core/tasks/review-story.md +234 -74
  30. package/bmad-core/tasks/risk-profile.md +353 -0
  31. package/bmad-core/tasks/test-design.md +174 -0
  32. package/bmad-core/tasks/trace-requirements.md +264 -0
  33. package/bmad-core/templates/architecture-tmpl.yaml +49 -49
  34. package/bmad-core/templates/brainstorming-output-tmpl.yaml +5 -5
  35. package/bmad-core/templates/brownfield-architecture-tmpl.yaml +31 -31
  36. package/bmad-core/templates/brownfield-prd-tmpl.yaml +13 -13
  37. package/bmad-core/templates/competitor-analysis-tmpl.yaml +19 -6
  38. package/bmad-core/templates/front-end-architecture-tmpl.yaml +21 -9
  39. package/bmad-core/templates/front-end-spec-tmpl.yaml +24 -24
  40. package/bmad-core/templates/fullstack-architecture-tmpl.yaml +122 -104
  41. package/bmad-core/templates/market-research-tmpl.yaml +2 -2
  42. package/bmad-core/templates/prd-tmpl.yaml +9 -9
  43. package/bmad-core/templates/project-brief-tmpl.yaml +4 -4
  44. package/bmad-core/templates/qa-gate-tmpl.yaml +102 -0
  45. package/bmad-core/templates/story-tmpl.yaml +12 -12
  46. package/bmad-core/workflows/brownfield-fullstack.yaml +9 -9
  47. package/bmad-core/workflows/brownfield-service.yaml +1 -1
  48. package/bmad-core/workflows/brownfield-ui.yaml +1 -1
  49. package/bmad-core/workflows/greenfield-fullstack.yaml +1 -1
  50. package/bmad-core/workflows/greenfield-service.yaml +1 -1
  51. package/bmad-core/workflows/greenfield-ui.yaml +1 -1
  52. package/common/utils/bmad-doc-template.md +5 -5
  53. package/dist/agents/analyst.txt +1086 -1079
  54. package/dist/agents/architect.txt +1534 -1526
  55. package/dist/agents/bmad-master.txt +646 -632
  56. package/dist/agents/bmad-orchestrator.txt +40 -18
  57. package/dist/agents/dev.txt +158 -19
  58. package/dist/agents/pm.txt +1082 -1107
  59. package/dist/agents/po.txt +314 -332
  60. package/dist/agents/qa.txt +1754 -151
  61. package/dist/agents/sm.txt +88 -98
  62. package/dist/agents/ux-expert.txt +80 -87
  63. package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-designer.txt +109 -146
  64. package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-developer.txt +75 -86
  65. package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-sm.txt +41 -48
  66. package/dist/expansion-packs/bmad-2d-phaser-game-dev/teams/phaser-2d-nodejs-game-team.txt +1903 -1941
  67. package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-architect.txt +15 -50
  68. package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-designer.txt +149 -195
  69. package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-developer.txt +0 -15
  70. package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-sm.txt +20 -37
  71. package/dist/expansion-packs/bmad-2d-unity-game-dev/teams/unity-2d-game-team.txt +2660 -2752
  72. package/dist/expansion-packs/bmad-creative-writing/agents/beta-reader.txt +871 -0
  73. package/dist/expansion-packs/bmad-creative-writing/agents/book-critic.txt +78 -0
  74. package/dist/expansion-packs/bmad-creative-writing/agents/character-psychologist.txt +839 -0
  75. package/dist/expansion-packs/bmad-creative-writing/agents/cover-designer.txt +85 -0
  76. package/dist/expansion-packs/bmad-creative-writing/agents/dialog-specialist.txt +861 -0
  77. package/dist/expansion-packs/bmad-creative-writing/agents/editor.txt +796 -0
  78. package/dist/expansion-packs/bmad-creative-writing/agents/genre-specialist.txt +927 -0
  79. package/dist/expansion-packs/bmad-creative-writing/agents/narrative-designer.txt +842 -0
  80. package/dist/expansion-packs/bmad-creative-writing/agents/plot-architect.txt +1126 -0
  81. package/dist/expansion-packs/bmad-creative-writing/agents/world-builder.txt +864 -0
  82. package/dist/expansion-packs/bmad-creative-writing/teams/agent-team.txt +5917 -0
  83. package/dist/expansion-packs/bmad-infrastructure-devops/agents/infra-devops-platform.txt +25 -27
  84. package/dist/teams/team-all.txt +5541 -3768
  85. package/dist/teams/team-fullstack.txt +3014 -2987
  86. package/dist/teams/team-ide-minimal.txt +2219 -469
  87. package/dist/teams/team-no-ui.txt +2993 -2966
  88. package/docs/enhanced-ide-development-workflow.md +220 -15
  89. package/docs/user-guide.md +271 -18
  90. package/docs/versioning-and-releases.md +122 -44
  91. package/docs/working-in-the-brownfield.md +264 -31
  92. package/eslint.config.mjs +119 -0
  93. package/expansion-packs/bmad-2d-phaser-game-dev/agents/game-developer.md +4 -4
  94. package/expansion-packs/bmad-2d-phaser-game-dev/agents/game-sm.md +1 -1
  95. package/expansion-packs/bmad-2d-phaser-game-dev/config.yaml +1 -1
  96. package/expansion-packs/bmad-2d-phaser-game-dev/data/development-guidelines.md +26 -28
  97. package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-architecture-tmpl.yaml +50 -50
  98. package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-brief-tmpl.yaml +23 -23
  99. package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-design-doc-tmpl.yaml +24 -24
  100. package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-story-tmpl.yaml +42 -42
  101. package/expansion-packs/bmad-2d-phaser-game-dev/templates/level-design-doc-tmpl.yaml +65 -65
  102. package/expansion-packs/bmad-2d-phaser-game-dev/workflows/game-dev-greenfield.yaml +5 -5
  103. package/expansion-packs/bmad-2d-phaser-game-dev/workflows/game-prototype.yaml +1 -1
  104. package/expansion-packs/bmad-2d-unity-game-dev/agents/game-developer.md +3 -3
  105. package/expansion-packs/bmad-2d-unity-game-dev/config.yaml +1 -1
  106. package/expansion-packs/bmad-2d-unity-game-dev/data/bmad-kb.md +1 -1
  107. package/expansion-packs/bmad-2d-unity-game-dev/templates/game-brief-tmpl.yaml +23 -23
  108. package/expansion-packs/bmad-2d-unity-game-dev/templates/game-design-doc-tmpl.yaml +63 -63
  109. package/expansion-packs/bmad-2d-unity-game-dev/templates/game-story-tmpl.yaml +20 -20
  110. package/expansion-packs/bmad-2d-unity-game-dev/templates/level-design-doc-tmpl.yaml +65 -65
  111. package/expansion-packs/bmad-2d-unity-game-dev/workflows/game-dev-greenfield.yaml +5 -5
  112. package/expansion-packs/bmad-2d-unity-game-dev/workflows/game-prototype.yaml +1 -1
  113. package/expansion-packs/bmad-creative-writing/README.md +132 -0
  114. package/expansion-packs/bmad-creative-writing/agent-teams/agent-team.yaml +19 -0
  115. package/expansion-packs/bmad-creative-writing/agents/beta-reader.md +91 -0
  116. package/expansion-packs/bmad-creative-writing/agents/book-critic.md +35 -0
  117. package/expansion-packs/bmad-creative-writing/agents/character-psychologist.md +90 -0
  118. package/expansion-packs/bmad-creative-writing/agents/cover-designer.md +41 -0
  119. package/expansion-packs/bmad-creative-writing/agents/dialog-specialist.md +89 -0
  120. package/expansion-packs/bmad-creative-writing/agents/editor.md +90 -0
  121. package/expansion-packs/bmad-creative-writing/agents/genre-specialist.md +92 -0
  122. package/expansion-packs/bmad-creative-writing/agents/narrative-designer.md +90 -0
  123. package/expansion-packs/bmad-creative-writing/agents/plot-architect.md +92 -0
  124. package/expansion-packs/bmad-creative-writing/agents/world-builder.md +91 -0
  125. package/expansion-packs/bmad-creative-writing/checklists/beta-feedback-closure-checklist.md +16 -0
  126. package/expansion-packs/bmad-creative-writing/checklists/character-consistency-checklist.md +16 -0
  127. package/expansion-packs/bmad-creative-writing/checklists/comedic-timing-checklist.md +16 -0
  128. package/expansion-packs/bmad-creative-writing/checklists/cyberpunk-aesthetic-checklist.md +16 -0
  129. package/expansion-packs/bmad-creative-writing/checklists/ebook-formatting-checklist.md +15 -0
  130. package/expansion-packs/bmad-creative-writing/checklists/epic-poetry-meter-checklist.md +16 -0
  131. package/expansion-packs/bmad-creative-writing/checklists/fantasy-magic-system-checklist.md +16 -0
  132. package/expansion-packs/bmad-creative-writing/checklists/foreshadowing-payoff-checklist.md +15 -0
  133. package/expansion-packs/bmad-creative-writing/checklists/genre-tropes-checklist.md +15 -0
  134. package/expansion-packs/bmad-creative-writing/checklists/historical-accuracy-checklist.md +16 -0
  135. package/expansion-packs/bmad-creative-writing/checklists/horror-suspense-checklist.md +16 -0
  136. package/expansion-packs/bmad-creative-writing/checklists/kdp-cover-ready-checklist.md +18 -0
  137. package/expansion-packs/bmad-creative-writing/checklists/line-edit-quality-checklist.md +16 -0
  138. package/expansion-packs/bmad-creative-writing/checklists/marketing-copy-checklist.md +16 -0
  139. package/expansion-packs/bmad-creative-writing/checklists/mystery-clue-trail-checklist.md +16 -0
  140. package/expansion-packs/bmad-creative-writing/checklists/orbital-mechanics-checklist.md +16 -0
  141. package/expansion-packs/bmad-creative-writing/checklists/plot-structure-checklist.md +49 -0
  142. package/expansion-packs/bmad-creative-writing/checklists/publication-readiness-checklist.md +16 -0
  143. package/expansion-packs/bmad-creative-writing/checklists/romance-emotional-beats-checklist.md +16 -0
  144. package/expansion-packs/bmad-creative-writing/checklists/scene-quality-checklist.md +16 -0
  145. package/expansion-packs/bmad-creative-writing/checklists/scifi-technology-plausibility-checklist.md +15 -0
  146. package/expansion-packs/bmad-creative-writing/checklists/sensitivity-representation-checklist.md +16 -0
  147. package/expansion-packs/bmad-creative-writing/checklists/steampunk-gadget-checklist.md +16 -0
  148. package/expansion-packs/bmad-creative-writing/checklists/thriller-pacing-stakes-checklist.md +16 -0
  149. package/expansion-packs/bmad-creative-writing/checklists/timeline-continuity-checklist.md +16 -0
  150. package/expansion-packs/bmad-creative-writing/checklists/world-building-continuity-checklist.md +16 -0
  151. package/expansion-packs/bmad-creative-writing/checklists/ya-appropriateness-checklist.md +16 -0
  152. package/expansion-packs/bmad-creative-writing/config.yaml +11 -0
  153. package/expansion-packs/bmad-creative-writing/data/bmad-kb.md +197 -0
  154. package/expansion-packs/bmad-creative-writing/data/story-structures.md +58 -0
  155. package/expansion-packs/bmad-creative-writing/docs/brief.md +183 -0
  156. package/expansion-packs/bmad-creative-writing/tasks/advanced-elicitation.md +117 -0
  157. package/expansion-packs/bmad-creative-writing/tasks/analyze-reader-feedback.md +16 -0
  158. package/expansion-packs/bmad-creative-writing/tasks/analyze-story-structure.md +55 -0
  159. package/expansion-packs/bmad-creative-writing/tasks/assemble-kdp-package.md +22 -0
  160. package/expansion-packs/bmad-creative-writing/tasks/brainstorm-premise.md +16 -0
  161. package/expansion-packs/bmad-creative-writing/tasks/build-world.md +17 -0
  162. package/expansion-packs/bmad-creative-writing/tasks/character-depth-pass.md +15 -0
  163. package/expansion-packs/bmad-creative-writing/tasks/create-doc.md +101 -0
  164. package/expansion-packs/bmad-creative-writing/tasks/create-draft-section.md +19 -0
  165. package/expansion-packs/bmad-creative-writing/tasks/critical-review.md +19 -0
  166. package/expansion-packs/bmad-creative-writing/tasks/develop-character.md +17 -0
  167. package/expansion-packs/bmad-creative-writing/tasks/execute-checklist.md +93 -0
  168. package/expansion-packs/bmad-creative-writing/tasks/expand-premise.md +16 -0
  169. package/expansion-packs/bmad-creative-writing/tasks/expand-synopsis.md +16 -0
  170. package/expansion-packs/bmad-creative-writing/tasks/final-polish.md +16 -0
  171. package/expansion-packs/bmad-creative-writing/tasks/generate-cover-brief.md +18 -0
  172. package/expansion-packs/bmad-creative-writing/tasks/generate-cover-prompts.md +19 -0
  173. package/expansion-packs/bmad-creative-writing/tasks/generate-scene-list.md +16 -0
  174. package/expansion-packs/bmad-creative-writing/tasks/incorporate-feedback.md +18 -0
  175. package/expansion-packs/bmad-creative-writing/tasks/outline-scenes.md +16 -0
  176. package/expansion-packs/bmad-creative-writing/tasks/provide-feedback.md +17 -0
  177. package/expansion-packs/bmad-creative-writing/tasks/publish-chapter.md +16 -0
  178. package/expansion-packs/bmad-creative-writing/tasks/quick-feedback.md +15 -0
  179. package/expansion-packs/bmad-creative-writing/tasks/select-next-arc.md +16 -0
  180. package/expansion-packs/bmad-creative-writing/tasks/workshop-dialog.md +51 -0
  181. package/expansion-packs/bmad-creative-writing/templates/beta-feedback-form.yaml +96 -0
  182. package/expansion-packs/bmad-creative-writing/templates/chapter-draft-tmpl.yaml +81 -0
  183. package/expansion-packs/bmad-creative-writing/templates/character-profile-tmpl.yaml +92 -0
  184. package/expansion-packs/bmad-creative-writing/templates/cover-design-brief-tmpl.yaml +97 -0
  185. package/expansion-packs/bmad-creative-writing/templates/premise-brief-tmpl.yaml +77 -0
  186. package/expansion-packs/bmad-creative-writing/templates/scene-list-tmpl.yaml +54 -0
  187. package/expansion-packs/bmad-creative-writing/templates/story-outline-tmpl.yaml +96 -0
  188. package/expansion-packs/bmad-creative-writing/templates/world-guide-tmpl.yaml +88 -0
  189. package/expansion-packs/bmad-creative-writing/workflows/book-cover-design-workflow.md +176 -0
  190. package/expansion-packs/bmad-creative-writing/workflows/novel-greenfield-workflow.yaml +58 -0
  191. package/expansion-packs/bmad-creative-writing/workflows/novel-serial-workflow.yaml +51 -0
  192. package/expansion-packs/bmad-creative-writing/workflows/novel-snowflake-workflow.yaml +69 -0
  193. package/expansion-packs/bmad-creative-writing/workflows/novel-writing.yaml +92 -0
  194. package/expansion-packs/bmad-creative-writing/workflows/screenplay-development.yaml +86 -0
  195. package/expansion-packs/bmad-creative-writing/workflows/series-planning.yaml +79 -0
  196. package/expansion-packs/bmad-creative-writing/workflows/short-story-creation.yaml +65 -0
  197. package/expansion-packs/bmad-infrastructure-devops/config.yaml +1 -1
  198. package/expansion-packs/bmad-infrastructure-devops/templates/infrastructure-architecture-tmpl.yaml +20 -20
  199. package/expansion-packs/bmad-infrastructure-devops/templates/infrastructure-platform-from-arch-tmpl.yaml +7 -7
  200. package/package.json +62 -39
  201. package/prettier.config.mjs +32 -0
  202. package/sync-version.sh +23 -0
  203. package/tools/bmad-npx-wrapper.js +10 -10
  204. package/tools/builders/web-builder.js +124 -130
  205. package/tools/bump-all-versions.js +42 -33
  206. package/tools/bump-expansion-version.js +23 -16
  207. package/tools/cli.js +10 -12
  208. package/tools/flattener/aggregate.js +10 -10
  209. package/tools/flattener/binary.js +44 -17
  210. package/tools/flattener/discovery.js +19 -18
  211. package/tools/flattener/files.js +6 -6
  212. package/tools/flattener/ignoreRules.js +125 -125
  213. package/tools/flattener/main.js +426 -70
  214. package/tools/flattener/projectRoot.js +186 -25
  215. package/tools/flattener/prompts.js +9 -9
  216. package/tools/flattener/stats.helpers.js +395 -0
  217. package/tools/flattener/stats.js +64 -14
  218. package/tools/flattener/test-matrix.js +413 -0
  219. package/tools/flattener/xml.js +33 -31
  220. package/tools/installer/bin/bmad.js +156 -113
  221. package/tools/installer/config/ide-agent-config.yaml +1 -1
  222. package/tools/installer/config/install.config.yaml +13 -3
  223. package/tools/installer/lib/config-loader.js +46 -42
  224. package/tools/installer/lib/file-manager.js +91 -113
  225. package/tools/installer/lib/ide-base-setup.js +57 -56
  226. package/tools/installer/lib/ide-setup.js +545 -399
  227. package/tools/installer/lib/installer.js +875 -714
  228. package/tools/installer/lib/memory-profiler.js +54 -53
  229. package/tools/installer/lib/module-manager.js +19 -15
  230. package/tools/installer/lib/resource-locator.js +26 -28
  231. package/tools/installer/package.json +19 -19
  232. package/tools/lib/dependency-resolver.js +26 -30
  233. package/tools/lib/yaml-utils.js +7 -7
  234. package/tools/preview-release-notes.js +66 -0
  235. package/tools/shared/bannerArt.js +3 -3
  236. package/tools/sync-installer-version.js +7 -9
  237. package/tools/update-expansion-version.js +14 -15
  238. package/tools/upgraders/v3-to-v4-upgrader.js +203 -294
  239. package/tools/version-bump.js +41 -26
  240. package/tools/yaml-format.js +56 -43
  241. package/.github/workflows/release.yaml +0 -60
  242. package/.releaserc.json +0 -21
  243. package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/Complete AI Agent System - Flowchart.svg +0 -102
  244. 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 +0 -13
  245. 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 +0 -13
  246. 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 +0 -25
  247. 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 +0 -34
  248. 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 +0 -70
  249. 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 +0 -26
  250. package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/README.md +0 -109
  251. package/tools/semantic-release-sync-installer.js +0 -30
@@ -1,42 +1,203 @@
1
- const fs = require("fs-extra");
2
- const path = require("node:path");
1
+ const fs = require('fs-extra');
2
+ const path = require('node:path');
3
+
4
+ // Deno/Node compatibility: explicitly import process
5
+ const process = require('node:process');
6
+ const { execFile } = require('node:child_process');
7
+ const { promisify } = require('node:util');
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ // Simple memoization across calls (keyed by realpath of startDir)
11
+ const _cache = new Map();
12
+
13
+ async function _tryRun(cmd, args, cwd, timeoutMs = 500) {
14
+ try {
15
+ const { stdout } = await execFileAsync(cmd, args, {
16
+ cwd,
17
+ timeout: timeoutMs,
18
+ windowsHide: true,
19
+ maxBuffer: 1024 * 1024,
20
+ });
21
+ const out = String(stdout || '').trim();
22
+ return out || null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ async function _detectVcsTopLevel(startDir) {
29
+ // Run common VCS root queries in parallel; ignore failures
30
+ const gitP = _tryRun('git', ['rev-parse', '--show-toplevel'], startDir);
31
+ const hgP = _tryRun('hg', ['root'], startDir);
32
+ const svnP = (async () => {
33
+ const show = await _tryRun('svn', ['info', '--show-item', 'wc-root'], startDir);
34
+ if (show) return show;
35
+ const info = await _tryRun('svn', ['info'], startDir);
36
+ if (info) {
37
+ const line = info
38
+ .split(/\r?\n/)
39
+ .find((l) => l.toLowerCase().startsWith('working copy root path:'));
40
+ if (line) return line.split(':').slice(1).join(':').trim();
41
+ }
42
+ return null;
43
+ })();
44
+ const [git, hg, svn] = await Promise.all([gitP, hgP, svnP]);
45
+ return git || hg || svn || null;
46
+ }
3
47
 
4
48
  /**
5
- * Attempt to find the project root by walking up from startDir
6
- * Looks for common project markers like .git, package.json, pyproject.toml, etc.
49
+ * Attempt to find the project root by walking up from startDir.
50
+ * Uses a robust, prioritized set of ecosystem markers (VCS > workspaces/monorepo > lock/build > language config).
51
+ * Also recognizes package.json with "workspaces" as a workspace root.
52
+ * You can augment markers via env PROJECT_ROOT_MARKERS as a comma-separated list of file/dir names.
7
53
  * @param {string} startDir
8
54
  * @returns {Promise<string|null>} project root directory or null if not found
9
55
  */
10
56
  async function findProjectRoot(startDir) {
11
57
  try {
58
+ // Resolve symlinks for robustness (e.g., when invoked from a symlinked path)
12
59
  let dir = path.resolve(startDir);
13
- const root = path.parse(dir).root;
14
- const markers = [
15
- ".git",
16
- "package.json",
17
- "pnpm-workspace.yaml",
18
- "yarn.lock",
19
- "pnpm-lock.yaml",
20
- "pyproject.toml",
21
- "requirements.txt",
22
- "go.mod",
23
- "Cargo.toml",
24
- "composer.json",
25
- ".hg",
26
- ".svn",
27
- ];
60
+ try {
61
+ dir = await fs.realpath(dir);
62
+ } catch {
63
+ // ignore if realpath fails; continue with resolved path
64
+ }
65
+ const startKey = dir; // preserve starting point for caching
66
+ if (_cache.has(startKey)) return _cache.get(startKey);
67
+ const fsRoot = path.parse(dir).root;
68
+
69
+ // Helper to safely check for existence
70
+ const exists = (p) => fs.pathExists(p);
71
+
72
+ // Build checks: an array of { makePath: (dir) => string, weight }
73
+ const checks = [];
74
+
75
+ const add = (rel, weight) => {
76
+ const makePath = (d) => (Array.isArray(rel) ? path.join(d, ...rel) : path.join(d, rel));
77
+ checks.push({ makePath, weight });
78
+ };
79
+
80
+ // Highest priority: explicit sentinel markers
81
+ add('.project-root', 110);
82
+ add('.workspace-root', 110);
83
+ add('.repo-root', 110);
84
+
85
+ // Highest priority: VCS roots
86
+ add('.git', 100);
87
+ add('.hg', 95);
88
+ add('.svn', 95);
89
+
90
+ // Monorepo/workspace indicators
91
+ add('pnpm-workspace.yaml', 90);
92
+ add('lerna.json', 90);
93
+ add('turbo.json', 90);
94
+ add('nx.json', 90);
95
+ add('rush.json', 90);
96
+ add('go.work', 90);
97
+ add('WORKSPACE', 90);
98
+ add('WORKSPACE.bazel', 90);
99
+ add('MODULE.bazel', 90);
100
+ add('pants.toml', 90);
101
+
102
+ // Lockfiles and package-manager/top-level locks
103
+ add('yarn.lock', 85);
104
+ add('pnpm-lock.yaml', 85);
105
+ add('package-lock.json', 85);
106
+ add('bun.lockb', 85);
107
+ add('Cargo.lock', 85);
108
+ add('composer.lock', 85);
109
+ add('poetry.lock', 85);
110
+ add('Pipfile.lock', 85);
111
+ add('Gemfile.lock', 85);
112
+
113
+ // Build-system root indicators
114
+ add('settings.gradle', 80);
115
+ add('settings.gradle.kts', 80);
116
+ add('gradlew', 80);
117
+ add('pom.xml', 80);
118
+ add('build.sbt', 80);
119
+ add(['project', 'build.properties'], 80);
120
+
121
+ // Language/project config markers
122
+ add('deno.json', 75);
123
+ add('deno.jsonc', 75);
124
+ add('pyproject.toml', 75);
125
+ add('Pipfile', 75);
126
+ add('requirements.txt', 75);
127
+ add('go.mod', 75);
128
+ add('Cargo.toml', 75);
129
+ add('composer.json', 75);
130
+ add('mix.exs', 75);
131
+ add('Gemfile', 75);
132
+ add('CMakeLists.txt', 75);
133
+ add('stack.yaml', 75);
134
+ add('cabal.project', 75);
135
+ add('rebar.config', 75);
136
+ add('pubspec.yaml', 75);
137
+ add('flake.nix', 75);
138
+ add('shell.nix', 75);
139
+ add('default.nix', 75);
140
+ add('.tool-versions', 75);
141
+ add('package.json', 74); // generic Node project (lower than lockfiles/workspaces)
142
+
143
+ // Changesets
144
+ add(['.changeset', 'config.json'], 70);
145
+ add('.changeset', 70);
146
+
147
+ // Custom markers via env (comma-separated names)
148
+ if (process.env.PROJECT_ROOT_MARKERS) {
149
+ for (const name of process.env.PROJECT_ROOT_MARKERS.split(',')
150
+ .map((s) => s.trim())
151
+ .filter(Boolean)) {
152
+ add(name, 72);
153
+ }
154
+ }
155
+
156
+ /** Check for package.json with "workspaces" */
157
+ const hasWorkspacePackageJson = async (d) => {
158
+ const pkgPath = path.join(d, 'package.json');
159
+ if (!(await exists(pkgPath))) return false;
160
+ try {
161
+ const raw = await fs.readFile(pkgPath, 'utf8');
162
+ const pkg = JSON.parse(raw);
163
+ return Boolean(pkg && pkg.workspaces);
164
+ } catch {
165
+ return false;
166
+ }
167
+ };
168
+
169
+ let best = null; // { dir, weight }
170
+
171
+ // Try to detect VCS toplevel once up-front; treat as authoritative slightly above .git marker
172
+ const vcsTop = await _detectVcsTopLevel(dir);
173
+ if (vcsTop) {
174
+ best = { dir: vcsTop, weight: 101 };
175
+ }
28
176
 
29
177
  while (true) {
30
- const exists = await Promise.all(
31
- markers.map((m) => fs.pathExists(path.join(dir, m))),
178
+ // Special check: package.json with "workspaces"
179
+ if ((await hasWorkspacePackageJson(dir)) && (!best || 90 >= best.weight))
180
+ best = { dir, weight: 90 };
181
+
182
+ // Evaluate all other checks in parallel
183
+ const results = await Promise.all(
184
+ checks.map(async (c) => ({ c, ok: await exists(c.makePath(dir)) })),
32
185
  );
33
- if (exists.some(Boolean)) {
34
- return dir;
186
+
187
+ for (const { c, ok } of results) {
188
+ if (!ok) continue;
189
+ if (!best || c.weight >= best.weight) {
190
+ best = { dir, weight: c.weight };
191
+ }
35
192
  }
36
- if (dir === root) break;
193
+
194
+ if (dir === fsRoot) break;
37
195
  dir = path.dirname(dir);
38
196
  }
39
- return null;
197
+
198
+ const out = best ? best.dir : null;
199
+ _cache.set(startKey, out);
200
+ return out;
40
201
  } catch {
41
202
  return null;
42
203
  }
@@ -1,11 +1,11 @@
1
- const os = require("node:os");
2
- const path = require("node:path");
3
- const readline = require("node:readline");
4
- const process = require("node:process");
1
+ const os = require('node:os');
2
+ const path = require('node:path');
3
+ const readline = require('node:readline');
4
+ const process = require('node:process');
5
5
 
6
6
  function expandHome(p) {
7
7
  if (!p) return p;
8
- if (p.startsWith("~")) return path.join(os.homedir(), p.slice(1));
8
+ if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
9
9
  return p;
10
10
  }
11
11
 
@@ -27,16 +27,16 @@ function promptQuestion(question) {
27
27
  }
28
28
 
29
29
  async function promptYesNo(question, defaultYes = true) {
30
- const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
30
+ const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
31
31
  const ans = (await promptQuestion(`${question}${suffix}`)).trim().toLowerCase();
32
32
  if (!ans) return defaultYes;
33
- if (["y", "yes"].includes(ans)) return true;
34
- if (["n", "no"].includes(ans)) return false;
33
+ if (['y', 'yes'].includes(ans)) return true;
34
+ if (['n', 'no'].includes(ans)) return false;
35
35
  return promptYesNo(question, defaultYes);
36
36
  }
37
37
 
38
38
  async function promptPath(question, defaultValue) {
39
- const prompt = `${question}${defaultValue ? ` (default: ${defaultValue})` : ""}: `;
39
+ const prompt = `${question}${defaultValue ? ` (default: ${defaultValue})` : ''}: `;
40
40
  const ans = (await promptQuestion(prompt)).trim();
41
41
  return expandHome(ans || defaultValue);
42
42
  }
@@ -0,0 +1,395 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const zlib = require('node:zlib');
6
+ const { Buffer } = require('node:buffer');
7
+ const crypto = require('node:crypto');
8
+ const cp = require('node:child_process');
9
+
10
+ const KB = 1024;
11
+ const MB = 1024 * KB;
12
+
13
+ const formatSize = (bytes) => {
14
+ if (bytes < 1024) return `${bytes} B`;
15
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
16
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
17
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
18
+ };
19
+
20
+ const percentile = (sorted, p) => {
21
+ if (sorted.length === 0) return 0;
22
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1));
23
+ return sorted[idx];
24
+ };
25
+
26
+ async function processWithLimit(items, fn, concurrency = 64) {
27
+ for (let i = 0; i < items.length; i += concurrency) {
28
+ await Promise.all(items.slice(i, i + concurrency).map(fn));
29
+ }
30
+ }
31
+
32
+ async function enrichAllFiles(textFiles, binaryFiles) {
33
+ /** @type {Array<{ path: string; absolutePath: string; size: number; lines?: number; isBinary: boolean; ext: string; dir: string; depth: number; hidden: boolean; mtimeMs: number; isSymlink: boolean; }>} */
34
+ const allFiles = [];
35
+
36
+ async function enrich(file, isBinary) {
37
+ const ext = (path.extname(file.path) || '').toLowerCase();
38
+ const dir = path.dirname(file.path) || '.';
39
+ const depth = file.path.split(path.sep).filter(Boolean).length;
40
+ const hidden = file.path.split(path.sep).some((seg) => seg.startsWith('.'));
41
+ let mtimeMs = 0;
42
+ let isSymlink = false;
43
+ try {
44
+ const lst = await fs.lstat(file.absolutePath);
45
+ mtimeMs = lst.mtimeMs;
46
+ isSymlink = lst.isSymbolicLink();
47
+ } catch {
48
+ /* ignore lstat errors during enrichment */
49
+ }
50
+ allFiles.push({
51
+ path: file.path,
52
+ absolutePath: file.absolutePath,
53
+ size: file.size || 0,
54
+ lines: file.lines,
55
+ isBinary,
56
+ ext,
57
+ dir,
58
+ depth,
59
+ hidden,
60
+ mtimeMs,
61
+ isSymlink,
62
+ });
63
+ }
64
+
65
+ await processWithLimit(textFiles, (f) => enrich(f, false));
66
+ await processWithLimit(binaryFiles, (f) => enrich(f, true));
67
+ return allFiles;
68
+ }
69
+
70
+ function buildHistogram(allFiles) {
71
+ const buckets = [
72
+ [1 * KB, '0–1KB'],
73
+ [10 * KB, '1–10KB'],
74
+ [100 * KB, '10–100KB'],
75
+ [1 * MB, '100KB–1MB'],
76
+ [10 * MB, '1–10MB'],
77
+ [100 * MB, '10–100MB'],
78
+ [Infinity, '>=100MB'],
79
+ ];
80
+ const histogram = buckets.map(([_, label]) => ({ label, count: 0, bytes: 0 }));
81
+ for (const f of allFiles) {
82
+ for (const [i, bucket] of buckets.entries()) {
83
+ if (f.size < bucket[0]) {
84
+ histogram[i].count++;
85
+ histogram[i].bytes += f.size;
86
+ break;
87
+ }
88
+ }
89
+ }
90
+ return histogram;
91
+ }
92
+
93
+ function aggregateByExtension(allFiles) {
94
+ const byExtension = new Map();
95
+ for (const f of allFiles) {
96
+ const key = f.ext || '<none>';
97
+ const v = byExtension.get(key) || { ext: key, count: 0, bytes: 0 };
98
+ v.count++;
99
+ v.bytes += f.size;
100
+ byExtension.set(key, v);
101
+ }
102
+ return [...byExtension.values()].sort((a, b) => b.bytes - a.bytes);
103
+ }
104
+
105
+ function aggregateByDirectory(allFiles) {
106
+ const byDirectory = new Map();
107
+ function addDirBytes(dir, bytes) {
108
+ const v = byDirectory.get(dir) || { dir, count: 0, bytes: 0 };
109
+ v.count++;
110
+ v.bytes += bytes;
111
+ byDirectory.set(dir, v);
112
+ }
113
+ for (const f of allFiles) {
114
+ const parts = f.dir === '.' ? [] : f.dir.split(path.sep);
115
+ let acc = '';
116
+ for (let i = 0; i < parts.length; i++) {
117
+ acc = i === 0 ? parts[0] : acc + path.sep + parts[i];
118
+ addDirBytes(acc, f.size);
119
+ }
120
+ if (parts.length === 0) addDirBytes('.', f.size);
121
+ }
122
+ return [...byDirectory.values()].sort((a, b) => b.bytes - a.bytes);
123
+ }
124
+
125
+ function computeDepthAndLongest(allFiles) {
126
+ const depthDistribution = new Map();
127
+ for (const f of allFiles) {
128
+ depthDistribution.set(f.depth, (depthDistribution.get(f.depth) || 0) + 1);
129
+ }
130
+ const longestPaths = [...allFiles]
131
+ .sort((a, b) => b.path.length - a.path.length)
132
+ .slice(0, 25)
133
+ .map((f) => ({ path: f.path, length: f.path.length, size: f.size }));
134
+ const depthDist = [...depthDistribution.entries()]
135
+ .sort((a, b) => a[0] - b[0])
136
+ .map(([depth, count]) => ({ depth, count }));
137
+ return { depthDist, longestPaths };
138
+ }
139
+
140
+ function computeTemporal(allFiles, nowMs) {
141
+ let oldest = null,
142
+ newest = null;
143
+ const ageBuckets = [
144
+ { label: '> 1 year', minDays: 365, maxDays: Infinity, count: 0, bytes: 0 },
145
+ { label: '6–12 months', minDays: 180, maxDays: 365, count: 0, bytes: 0 },
146
+ { label: '1–6 months', minDays: 30, maxDays: 180, count: 0, bytes: 0 },
147
+ { label: '7–30 days', minDays: 7, maxDays: 30, count: 0, bytes: 0 },
148
+ { label: '1–7 days', minDays: 1, maxDays: 7, count: 0, bytes: 0 },
149
+ { label: '< 1 day', minDays: 0, maxDays: 1, count: 0, bytes: 0 },
150
+ ];
151
+ for (const f of allFiles) {
152
+ const ageDays = Math.max(0, (nowMs - (f.mtimeMs || nowMs)) / (24 * 60 * 60 * 1000));
153
+ for (const b of ageBuckets) {
154
+ if (ageDays >= b.minDays && ageDays < b.maxDays) {
155
+ b.count++;
156
+ b.bytes += f.size;
157
+ break;
158
+ }
159
+ }
160
+ if (!oldest || f.mtimeMs < oldest.mtimeMs) oldest = f;
161
+ if (!newest || f.mtimeMs > newest.mtimeMs) newest = f;
162
+ }
163
+ return {
164
+ oldest: oldest
165
+ ? { path: oldest.path, mtime: oldest.mtimeMs ? new Date(oldest.mtimeMs).toISOString() : null }
166
+ : null,
167
+ newest: newest
168
+ ? { path: newest.path, mtime: newest.mtimeMs ? new Date(newest.mtimeMs).toISOString() : null }
169
+ : null,
170
+ ageBuckets,
171
+ };
172
+ }
173
+
174
+ function computeQuality(allFiles, textFiles) {
175
+ const zeroByteFiles = allFiles.filter((f) => f.size === 0).length;
176
+ const emptyTextFiles = textFiles.filter(
177
+ (f) => (f.size || 0) === 0 || (f.lines || 0) === 0,
178
+ ).length;
179
+ const hiddenFiles = allFiles.filter((f) => f.hidden).length;
180
+ const symlinks = allFiles.filter((f) => f.isSymlink).length;
181
+ const largeThreshold = 50 * MB;
182
+ const suspiciousThreshold = 100 * MB;
183
+ const largeFilesCount = allFiles.filter((f) => f.size >= largeThreshold).length;
184
+ const suspiciousLargeFilesCount = allFiles.filter((f) => f.size >= suspiciousThreshold).length;
185
+ return {
186
+ zeroByteFiles,
187
+ emptyTextFiles,
188
+ hiddenFiles,
189
+ symlinks,
190
+ largeFilesCount,
191
+ suspiciousLargeFilesCount,
192
+ largeThreshold,
193
+ };
194
+ }
195
+
196
+ function computeDuplicates(allFiles, textFiles) {
197
+ const duplicatesBySize = new Map();
198
+ for (const f of allFiles) {
199
+ const key = String(f.size);
200
+ const arr = duplicatesBySize.get(key) || [];
201
+ arr.push(f);
202
+ duplicatesBySize.set(key, arr);
203
+ }
204
+ const duplicateCandidates = [];
205
+ for (const [sizeKey, arr] of duplicatesBySize.entries()) {
206
+ if (arr.length < 2) continue;
207
+ const textGroup = arr.filter((f) => !f.isBinary);
208
+ const otherGroup = arr.filter((f) => f.isBinary);
209
+ const contentHashGroups = new Map();
210
+ for (const tf of textGroup) {
211
+ try {
212
+ const src = textFiles.find((x) => x.absolutePath === tf.absolutePath);
213
+ const content = src ? src.content : '';
214
+ const h = crypto.createHash('sha1').update(content).digest('hex');
215
+ const g = contentHashGroups.get(h) || [];
216
+ g.push(tf);
217
+ contentHashGroups.set(h, g);
218
+ } catch {
219
+ /* ignore hashing errors for duplicate detection */
220
+ }
221
+ }
222
+ for (const [_h, g] of contentHashGroups.entries()) {
223
+ if (g.length > 1)
224
+ duplicateCandidates.push({
225
+ reason: 'same-size+text-hash',
226
+ size: Number(sizeKey),
227
+ count: g.length,
228
+ files: g.map((f) => f.path),
229
+ });
230
+ }
231
+ if (otherGroup.length > 1) {
232
+ duplicateCandidates.push({
233
+ reason: 'same-size',
234
+ size: Number(sizeKey),
235
+ count: otherGroup.length,
236
+ files: otherGroup.map((f) => f.path),
237
+ });
238
+ }
239
+ }
240
+ return duplicateCandidates;
241
+ }
242
+
243
+ function estimateCompressibility(textFiles) {
244
+ let compSampleBytes = 0;
245
+ let compCompressedBytes = 0;
246
+ for (const tf of textFiles) {
247
+ try {
248
+ const sampleLen = Math.min(256 * 1024, tf.size || 0);
249
+ if (sampleLen <= 0) continue;
250
+ const sample = tf.content.slice(0, sampleLen);
251
+ const gz = zlib.gzipSync(Buffer.from(sample, 'utf8'));
252
+ compSampleBytes += sampleLen;
253
+ compCompressedBytes += gz.length;
254
+ } catch {
255
+ /* ignore compression errors during sampling */
256
+ }
257
+ }
258
+ return compSampleBytes > 0 ? compCompressedBytes / compSampleBytes : null;
259
+ }
260
+
261
+ function computeGitInfo(allFiles, rootDir, largeThreshold) {
262
+ const info = {
263
+ isRepo: false,
264
+ trackedCount: 0,
265
+ trackedBytes: 0,
266
+ untrackedCount: 0,
267
+ untrackedBytes: 0,
268
+ lfsCandidates: [],
269
+ };
270
+ try {
271
+ if (!rootDir) return info;
272
+ const top = cp
273
+ .execFileSync('git', ['rev-parse', '--show-toplevel'], {
274
+ cwd: rootDir,
275
+ stdio: ['ignore', 'pipe', 'ignore'],
276
+ })
277
+ .toString()
278
+ .trim();
279
+ if (!top) return info;
280
+ info.isRepo = true;
281
+ const out = cp.execFileSync('git', ['ls-files', '-z'], {
282
+ cwd: rootDir,
283
+ stdio: ['ignore', 'pipe', 'ignore'],
284
+ });
285
+ const tracked = new Set(out.toString().split('\0').filter(Boolean));
286
+ let trackedBytes = 0,
287
+ trackedCount = 0,
288
+ untrackedBytes = 0,
289
+ untrackedCount = 0;
290
+ const lfsCandidates = [];
291
+ for (const f of allFiles) {
292
+ const isTracked = tracked.has(f.path);
293
+ if (isTracked) {
294
+ trackedCount++;
295
+ trackedBytes += f.size;
296
+ if (f.size >= largeThreshold) lfsCandidates.push({ path: f.path, size: f.size });
297
+ } else {
298
+ untrackedCount++;
299
+ untrackedBytes += f.size;
300
+ }
301
+ }
302
+ info.trackedCount = trackedCount;
303
+ info.trackedBytes = trackedBytes;
304
+ info.untrackedCount = untrackedCount;
305
+ info.untrackedBytes = untrackedBytes;
306
+ info.lfsCandidates = lfsCandidates.sort((a, b) => b.size - a.size).slice(0, 50);
307
+ } catch {
308
+ /* git not available or not a repo, ignore */
309
+ }
310
+ return info;
311
+ }
312
+
313
+ function computeLargestFiles(allFiles, totalBytes) {
314
+ const toPct = (num, den) => (den === 0 ? 0 : (num / den) * 100);
315
+ return [...allFiles]
316
+ .sort((a, b) => b.size - a.size)
317
+ .slice(0, 50)
318
+ .map((f) => ({
319
+ path: f.path,
320
+ size: f.size,
321
+ sizeFormatted: formatSize(f.size),
322
+ percentOfTotal: toPct(f.size, totalBytes),
323
+ ext: f.ext || '',
324
+ isBinary: f.isBinary,
325
+ mtime: f.mtimeMs ? new Date(f.mtimeMs).toISOString() : null,
326
+ }));
327
+ }
328
+
329
+ function mdTable(rows, headers) {
330
+ const header = `| ${headers.join(' | ')} |`;
331
+ const sep = `| ${headers.map(() => '---').join(' | ')} |`;
332
+ const body = rows.map((r) => `| ${r.join(' | ')} |`).join('\n');
333
+ return `${header}\n${sep}\n${body}`;
334
+ }
335
+
336
+ function buildMarkdownReport(largestFiles, byExtensionArr, byDirectoryArr, totalBytes) {
337
+ const toPct = (num, den) => (den === 0 ? 0 : (num / den) * 100);
338
+ const md = [];
339
+ md.push(
340
+ '\n### Top Largest Files (Top 50)\n',
341
+ mdTable(
342
+ largestFiles.map((f) => [
343
+ f.path,
344
+ f.sizeFormatted,
345
+ `${f.percentOfTotal.toFixed(2)}%`,
346
+ f.ext || '',
347
+ f.isBinary ? 'binary' : 'text',
348
+ ]),
349
+ ['Path', 'Size', '% of total', 'Ext', 'Type'],
350
+ ),
351
+ '\n\n### Top Extensions by Bytes (Top 20)\n',
352
+ );
353
+ const topExtRows = byExtensionArr
354
+ .slice(0, 20)
355
+ .map((e) => [
356
+ e.ext,
357
+ String(e.count),
358
+ formatSize(e.bytes),
359
+ `${toPct(e.bytes, totalBytes).toFixed(2)}%`,
360
+ ]);
361
+ md.push(
362
+ mdTable(topExtRows, ['Ext', 'Count', 'Bytes', '% of total']),
363
+ '\n\n### Top Directories by Bytes (Top 20)\n',
364
+ );
365
+ const topDirRows = byDirectoryArr
366
+ .slice(0, 20)
367
+ .map((d) => [
368
+ d.dir,
369
+ String(d.count),
370
+ formatSize(d.bytes),
371
+ `${toPct(d.bytes, totalBytes).toFixed(2)}%`,
372
+ ]);
373
+ md.push(mdTable(topDirRows, ['Directory', 'Files', 'Bytes', '% of total']));
374
+ return md.join('\n');
375
+ }
376
+
377
+ module.exports = {
378
+ KB,
379
+ MB,
380
+ formatSize,
381
+ percentile,
382
+ processWithLimit,
383
+ enrichAllFiles,
384
+ buildHistogram,
385
+ aggregateByExtension,
386
+ aggregateByDirectory,
387
+ computeDepthAndLongest,
388
+ computeTemporal,
389
+ computeQuality,
390
+ computeDuplicates,
391
+ estimateCompressibility,
392
+ computeGitInfo,
393
+ computeLargestFiles,
394
+ buildMarkdownReport,
395
+ };