bmad-method 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/FUNDING.yaml +15 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
- package/.github/workflows/discord.yaml +25 -0
- package/.github/workflows/format-check.yaml +42 -0
- package/.github/workflows/manual-release.yaml +173 -0
- package/.husky/pre-commit +3 -2
- package/.vscode/settings.json +67 -74
- package/CHANGELOG.md +564 -19
- package/CONTRIBUTING.md +168 -5
- package/LICENSE +1 -1
- package/README.md +146 -218
- package/bmad-core/agent-teams/team-all.yaml +14 -0
- package/bmad-core/agent-teams/team-fullstack.yaml +18 -0
- package/bmad-core/agent-teams/team-ide-minimal.yaml +10 -0
- package/bmad-core/agent-teams/team-no-ui.yaml +13 -0
- package/bmad-core/agents/analyst.md +81 -0
- package/bmad-core/agents/architect.md +83 -0
- package/bmad-core/agents/bmad-master.md +107 -0
- package/bmad-core/agents/bmad-orchestrator.md +149 -0
- package/bmad-core/agents/dev.md +75 -0
- package/bmad-core/agents/pm.md +81 -0
- package/bmad-core/agents/po.md +76 -0
- package/bmad-core/agents/qa.md +88 -0
- package/bmad-core/agents/sm.md +62 -0
- package/bmad-core/agents/ux-expert.md +66 -0
- package/{.bmad-core → bmad-core}/checklists/architect-checklist.md +0 -5
- package/{.bmad-core → bmad-core}/checklists/change-checklist.md +2 -2
- package/{.bmad-core → bmad-core}/checklists/pm-checklist.md +0 -5
- package/{.bmad-core → bmad-core}/checklists/po-master-checklist.md +0 -9
- package/{.bmad-core → bmad-core}/checklists/story-dod-checklist.md +0 -7
- package/{.bmad-core → bmad-core}/checklists/story-draft-checklist.md +1 -4
- package/bmad-core/core-config.yaml +20 -0
- package/bmad-core/data/bmad-kb.md +806 -0
- package/bmad-core/data/brainstorming-techniques.md +36 -0
- package/bmad-core/data/elicitation-methods.md +154 -0
- package/bmad-core/data/test-levels-framework.md +146 -0
- package/bmad-core/data/test-priorities-matrix.md +172 -0
- package/bmad-core/tasks/advanced-elicitation.md +117 -0
- package/{.bmad-core → bmad-core}/tasks/correct-course.md +9 -12
- package/bmad-core/tasks/create-brownfield-story.md +312 -0
- package/{.bmad-core → bmad-core}/tasks/create-deep-research-prompt.md +4 -27
- package/bmad-core/tasks/create-next-story.md +112 -0
- package/bmad-core/tasks/document-project.md +343 -0
- package/bmad-core/tasks/facilitate-brainstorming-session.md +136 -0
- package/bmad-core/tasks/generate-ai-frontend-prompt.md +51 -0
- package/{.bmad-core → bmad-core}/tasks/index-docs.md +3 -13
- package/bmad-core/tasks/kb-mode-interaction.md +75 -0
- package/bmad-core/tasks/nfr-assess.md +343 -0
- package/bmad-core/tasks/qa-gate.md +159 -0
- package/bmad-core/tasks/review-story.md +314 -0
- package/bmad-core/tasks/risk-profile.md +353 -0
- package/{.bmad-core → bmad-core}/tasks/shard-doc.md +27 -15
- package/bmad-core/tasks/test-design.md +174 -0
- package/bmad-core/tasks/trace-requirements.md +264 -0
- package/bmad-core/tasks/validate-next-story.md +134 -0
- package/bmad-core/templates/architecture-tmpl.yaml +650 -0
- package/bmad-core/templates/brainstorming-output-tmpl.yaml +156 -0
- package/bmad-core/templates/brownfield-architecture-tmpl.yaml +476 -0
- package/bmad-core/templates/brownfield-prd-tmpl.yaml +280 -0
- package/bmad-core/templates/competitor-analysis-tmpl.yaml +306 -0
- package/bmad-core/templates/front-end-architecture-tmpl.yaml +218 -0
- package/bmad-core/templates/front-end-spec-tmpl.yaml +349 -0
- package/bmad-core/templates/fullstack-architecture-tmpl.yaml +823 -0
- package/bmad-core/templates/market-research-tmpl.yaml +252 -0
- package/bmad-core/templates/prd-tmpl.yaml +202 -0
- package/bmad-core/templates/project-brief-tmpl.yaml +221 -0
- package/bmad-core/templates/qa-gate-tmpl.yaml +102 -0
- package/bmad-core/templates/story-tmpl.yaml +137 -0
- package/bmad-core/workflows/brownfield-fullstack.yaml +297 -0
- package/bmad-core/workflows/brownfield-service.yaml +187 -0
- package/bmad-core/workflows/brownfield-ui.yaml +197 -0
- package/{.bmad-core/workflows/greenfield-fullstack.yml → bmad-core/workflows/greenfield-fullstack.yaml} +140 -77
- package/bmad-core/workflows/greenfield-service.yaml +206 -0
- package/bmad-core/workflows/greenfield-ui.yaml +235 -0
- package/common/tasks/create-doc.md +101 -0
- package/{.bmad-core → common}/tasks/execute-checklist.md +2 -13
- package/common/utils/bmad-doc-template.md +325 -0
- package/common/utils/workflow-management.md +69 -0
- package/dist/agents/analyst.txt +2889 -0
- package/dist/agents/architect.txt +3552 -0
- package/dist/agents/bmad-master.txt +8769 -0
- package/dist/agents/bmad-orchestrator.txt +1513 -0
- package/dist/agents/dev.txt +414 -0
- package/{.bmad-core/web-bundles → dist}/agents/pm.txt +668 -1119
- package/{.bmad-core/web-bundles → dist}/agents/po.txt +341 -484
- package/dist/agents/qa.txt +1987 -0
- package/dist/agents/sm.txt +658 -0
- package/dist/agents/ux-expert.txt +694 -0
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-designer.txt +2371 -0
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-developer.txt +1620 -0
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-sm.txt +815 -0
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/teams/phaser-2d-nodejs-game-team.txt +10952 -0
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-architect.txt +4012 -0
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-designer.txt +3698 -0
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-developer.txt +450 -0
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-sm.txt +973 -0
- package/dist/expansion-packs/bmad-2d-unity-game-dev/teams/unity-2d-game-team.txt +15376 -0
- package/dist/expansion-packs/bmad-infrastructure-devops/agents/infra-devops-platform.txt +2075 -0
- package/dist/teams/team-all.txt +12682 -0
- package/dist/teams/team-fullstack.txt +10421 -0
- package/dist/teams/team-ide-minimal.txt +5103 -0
- package/dist/teams/team-no-ui.txt +8980 -0
- package/docs/GUIDING-PRINCIPLES.md +91 -0
- package/docs/core-architecture.md +219 -0
- package/docs/enhanced-ide-development-workflow.md +248 -0
- package/docs/expansion-packs.md +280 -0
- package/docs/how-to-contribute-with-pull-requests.md +158 -0
- package/docs/user-guide.md +504 -0
- package/docs/versioning-and-releases.md +8 -16
- package/docs/versions.md +4 -5
- package/docs/working-in-the-brownfield.md +597 -0
- package/eslint.config.mjs +119 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/Complete AI Agent System - Flowchart.svg +102 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.1 Google Cloud Project Setup/1.1.1 - Initial Project Configuration - bash copy.txt +13 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.1 Google Cloud Project Setup/1.1.1 - Initial Project Configuration - bash.txt +13 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.2 Agent Development Kit Installation/1.2.2 - Basic Project Structure - txt.txt +25 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.3 Core Configuration Files/1.3.1 - settings.py +34 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.3 Core Configuration Files/1.3.2 - main.py - Base Application.py +70 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.4 Deployment Configuration/1.4.2 - cloudbuild.yaml +26 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/README.md +109 -0
- package/expansion-packs/README.md +2 -112
- package/expansion-packs/bmad-2d-phaser-game-dev/agent-teams/phaser-2d-nodejs-game-team.yaml +13 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/agents/game-designer.md +71 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/agents/game-developer.md +78 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/agents/game-sm.md +64 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/checklists/game-design-checklist.md +201 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/checklists/game-story-dod-checklist.md +160 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/config.yaml +8 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/data/bmad-kb.md +250 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/data/development-guidelines.md +647 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/tasks/advanced-elicitation.md +110 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/tasks/create-game-story.md +216 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/tasks/game-design-brainstorming.md +290 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-architecture-tmpl.yaml +613 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-brief-tmpl.yaml +356 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-design-doc-tmpl.yaml +343 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-story-tmpl.yaml +253 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/templates/level-design-doc-tmpl.yaml +484 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/workflows/game-dev-greenfield.yaml +183 -0
- package/expansion-packs/bmad-2d-phaser-game-dev/workflows/game-prototype.yaml +175 -0
- package/expansion-packs/bmad-2d-unity-game-dev/agent-teams/unity-2d-game-team.yaml +14 -0
- package/expansion-packs/bmad-2d-unity-game-dev/agents/game-architect.md +80 -0
- package/expansion-packs/bmad-2d-unity-game-dev/agents/game-designer.md +77 -0
- package/expansion-packs/bmad-2d-unity-game-dev/agents/game-developer.md +78 -0
- package/expansion-packs/bmad-2d-unity-game-dev/agents/game-sm.md +65 -0
- package/expansion-packs/bmad-2d-unity-game-dev/checklists/game-architect-checklist.md +391 -0
- package/expansion-packs/bmad-2d-unity-game-dev/checklists/game-change-checklist.md +203 -0
- package/expansion-packs/bmad-2d-unity-game-dev/checklists/game-design-checklist.md +201 -0
- package/expansion-packs/bmad-2d-unity-game-dev/checklists/game-story-dod-checklist.md +124 -0
- package/expansion-packs/bmad-2d-unity-game-dev/config.yaml +6 -0
- package/expansion-packs/bmad-2d-unity-game-dev/data/bmad-kb.md +769 -0
- package/expansion-packs/bmad-2d-unity-game-dev/data/development-guidelines.md +586 -0
- package/expansion-packs/bmad-2d-unity-game-dev/tasks/advanced-elicitation.md +110 -0
- package/expansion-packs/bmad-2d-unity-game-dev/tasks/correct-course-game.md +141 -0
- package/expansion-packs/bmad-2d-unity-game-dev/tasks/create-game-story.md +184 -0
- package/expansion-packs/bmad-2d-unity-game-dev/tasks/game-design-brainstorming.md +290 -0
- package/expansion-packs/bmad-2d-unity-game-dev/tasks/validate-game-story.md +200 -0
- package/expansion-packs/bmad-2d-unity-game-dev/templates/game-architecture-tmpl.yaml +1030 -0
- package/expansion-packs/bmad-2d-unity-game-dev/templates/game-brief-tmpl.yaml +356 -0
- package/expansion-packs/bmad-2d-unity-game-dev/templates/game-design-doc-tmpl.yaml +705 -0
- package/expansion-packs/bmad-2d-unity-game-dev/templates/game-story-tmpl.yaml +256 -0
- package/expansion-packs/bmad-2d-unity-game-dev/templates/level-design-doc-tmpl.yaml +484 -0
- package/expansion-packs/bmad-2d-unity-game-dev/workflows/game-dev-greenfield.yaml +183 -0
- package/expansion-packs/bmad-2d-unity-game-dev/workflows/game-prototype.yaml +175 -0
- package/expansion-packs/{infrastructure-devops → bmad-infrastructure-devops}/README.md +9 -9
- package/expansion-packs/{infrastructure-devops → bmad-infrastructure-devops}/agents/infra-devops-platform.md +30 -18
- package/expansion-packs/{infrastructure-devops → bmad-infrastructure-devops}/checklists/infrastructure-checklist.md +1 -1
- package/expansion-packs/bmad-infrastructure-devops/config.yaml +9 -0
- package/expansion-packs/bmad-infrastructure-devops/data/bmad-kb.md +305 -0
- package/expansion-packs/{infrastructure-devops → bmad-infrastructure-devops}/tasks/review-infrastructure.md +4 -5
- package/expansion-packs/{infrastructure-devops → bmad-infrastructure-devops}/tasks/validate-infrastructure.md +4 -5
- package/expansion-packs/bmad-infrastructure-devops/templates/infrastructure-architecture-tmpl.yaml +424 -0
- package/expansion-packs/bmad-infrastructure-devops/templates/infrastructure-platform-from-arch-tmpl.yaml +629 -0
- package/package.json +74 -42
- package/prettier.config.mjs +32 -0
- package/release_notes.md +25 -0
- package/tools/bmad-npx-wrapper.js +13 -15
- package/tools/builders/web-builder.js +544 -15
- package/tools/bump-all-versions.js +115 -0
- package/tools/bump-expansion-version.js +90 -0
- package/tools/cli.js +65 -32
- package/tools/flattener/aggregate.js +76 -0
- package/tools/flattener/binary.js +80 -0
- package/tools/flattener/discovery.js +71 -0
- package/tools/flattener/files.js +35 -0
- package/tools/flattener/ignoreRules.js +176 -0
- package/tools/flattener/main.js +573 -0
- package/tools/flattener/projectRoot.js +206 -0
- package/tools/flattener/prompts.js +44 -0
- package/tools/flattener/stats.helpers.js +395 -0
- package/tools/flattener/stats.js +80 -0
- package/tools/flattener/test-matrix.js +413 -0
- package/tools/flattener/xml.js +88 -0
- package/tools/installer/README.md +3 -53
- package/tools/installer/bin/bmad.js +475 -90
- package/tools/installer/config/ide-agent-config.yaml +58 -0
- package/tools/installer/config/install.config.yaml +123 -0
- package/tools/installer/lib/config-loader.js +208 -40
- package/tools/installer/lib/file-manager.js +258 -55
- package/tools/installer/lib/ide-base-setup.js +228 -0
- package/tools/installer/lib/ide-setup.js +1265 -253
- package/tools/installer/lib/installer.js +1651 -310
- package/tools/installer/lib/memory-profiler.js +225 -0
- package/tools/installer/lib/module-manager.js +114 -0
- package/tools/installer/lib/resource-locator.js +308 -0
- package/tools/installer/package.json +25 -24
- package/tools/lib/dependency-resolver.js +44 -48
- package/tools/lib/yaml-utils.js +29 -0
- package/tools/md-assets/web-agent-startup-instructions.md +39 -0
- package/tools/preview-release-notes.js +66 -0
- package/tools/shared/bannerArt.js +105 -0
- package/tools/sync-installer-version.js +7 -9
- package/tools/update-expansion-version.js +53 -0
- package/tools/upgraders/v3-to-v4-upgrader.js +221 -320
- package/tools/version-bump.js +42 -27
- package/tools/yaml-format.js +57 -44
- package/.bmad-core/agent-teams/team-all.yml +0 -16
- package/.bmad-core/agent-teams/team-fullstack.yml +0 -26
- package/.bmad-core/agent-teams/team-no-ui.yml +0 -15
- package/.bmad-core/agents/analyst.md +0 -59
- package/.bmad-core/agents/architect.md +0 -66
- package/.bmad-core/agents/bmad-master.md +0 -104
- package/.bmad-core/agents/bmad-orchestrator.md +0 -81
- package/.bmad-core/agents/dev.md +0 -70
- package/.bmad-core/agents/pm.md +0 -59
- package/.bmad-core/agents/po.md +0 -60
- package/.bmad-core/agents/qa.md +0 -52
- package/.bmad-core/agents/sm.md +0 -55
- package/.bmad-core/agents/ux-expert.md +0 -66
- package/.bmad-core/data/bmad-kb.md +0 -47
- package/.bmad-core/schemas/agent-team-schema.yml +0 -153
- package/.bmad-core/tasks/advanced-elicitation.md +0 -92
- package/.bmad-core/tasks/brainstorming-techniques.md +0 -238
- package/.bmad-core/tasks/core-dump.md +0 -74
- package/.bmad-core/tasks/create-agent.md +0 -202
- package/.bmad-core/tasks/create-doc.md +0 -74
- package/.bmad-core/tasks/create-expansion-pack.md +0 -425
- package/.bmad-core/tasks/create-next-story.md +0 -206
- package/.bmad-core/tasks/create-team.md +0 -229
- package/.bmad-core/tasks/doc-migration-task.md +0 -143
- package/.bmad-core/tasks/generate-ai-frontend-prompt.md +0 -58
- package/.bmad-core/templates/agent-tmpl.md +0 -58
- package/.bmad-core/templates/architecture-tmpl.md +0 -771
- package/.bmad-core/templates/brownfield-architecture-tmpl.md +0 -542
- package/.bmad-core/templates/brownfield-prd-tmpl.md +0 -240
- package/.bmad-core/templates/competitor-analysis-tmpl.md +0 -289
- package/.bmad-core/templates/expansion-pack-plan-tmpl.md +0 -91
- package/.bmad-core/templates/front-end-architecture-tmpl.md +0 -173
- package/.bmad-core/templates/front-end-spec-tmpl.md +0 -411
- package/.bmad-core/templates/fullstack-architecture-tmpl.md +0 -1016
- package/.bmad-core/templates/market-research-tmpl.md +0 -261
- package/.bmad-core/templates/prd-tmpl.md +0 -200
- package/.bmad-core/templates/project-brief-tmpl.md +0 -228
- package/.bmad-core/templates/simple-project-prd-tmpl.md +0 -461
- package/.bmad-core/templates/story-tmpl.md +0 -61
- package/.bmad-core/templates/web-agent-startup-instructions-template.md +0 -39
- package/.bmad-core/utils/agent-switcher.ide.md +0 -112
- package/.bmad-core/utils/template-format.md +0 -26
- package/.bmad-core/utils/workflow-management.md +0 -224
- package/.bmad-core/web-bundles/agents/analyst.txt +0 -1684
- package/.bmad-core/web-bundles/agents/architect.txt +0 -3584
- package/.bmad-core/web-bundles/agents/bmad-master.txt +0 -9491
- package/.bmad-core/web-bundles/agents/bmad-orchestrator.txt +0 -1466
- package/.bmad-core/web-bundles/agents/dev.txt +0 -316
- package/.bmad-core/web-bundles/agents/qa.txt +0 -129
- package/.bmad-core/web-bundles/agents/sm.txt +0 -658
- package/.bmad-core/web-bundles/agents/ux-expert.txt +0 -1099
- package/.bmad-core/web-bundles/teams/team-all.txt +0 -10757
- package/.bmad-core/web-bundles/teams/team-fullstack.txt +0 -10109
- package/.bmad-core/web-bundles/teams/team-no-ui.txt +0 -8950
- package/.bmad-core/workflows/brownfield-fullstack.yml +0 -116
- package/.bmad-core/workflows/brownfield-service.yml +0 -117
- package/.bmad-core/workflows/brownfield-ui.yml +0 -127
- package/.bmad-core/workflows/greenfield-service.yml +0 -143
- package/.bmad-core/workflows/greenfield-ui.yml +0 -172
- package/.claude/commands/analyst.md +0 -63
- package/.claude/commands/architect.md +0 -70
- package/.claude/commands/bmad-master.md +0 -108
- package/.claude/commands/bmad-orchestrator.md +0 -85
- package/.claude/commands/dev.md +0 -74
- package/.claude/commands/pm.md +0 -63
- package/.claude/commands/po.md +0 -64
- package/.claude/commands/qa.md +0 -56
- package/.claude/commands/sm.md +0 -59
- package/.claude/commands/ux-expert.md +0 -70
- package/.cursor/rules/analyst.mdc +0 -77
- package/.cursor/rules/architect.mdc +0 -84
- package/.cursor/rules/bmad-master.mdc +0 -122
- package/.cursor/rules/bmad-orchestrator.mdc +0 -99
- package/.cursor/rules/dev.mdc +0 -88
- package/.cursor/rules/pm.mdc +0 -77
- package/.cursor/rules/po.mdc +0 -78
- package/.cursor/rules/qa.mdc +0 -70
- package/.cursor/rules/sm.mdc +0 -73
- package/.cursor/rules/ux-expert.mdc +0 -84
- package/.github/workflows/release.yml +0 -59
- package/.releaserc.json +0 -18
- package/.roo/.roomodes +0 -95
- package/.roo/README.md +0 -38
- package/.vscode/extensions.json +0 -6
- package/.windsurf/rules/analyst.md +0 -71
- package/.windsurf/rules/architect.md +0 -78
- package/.windsurf/rules/bmad-master.md +0 -116
- package/.windsurf/rules/bmad-orchestrator.md +0 -93
- package/.windsurf/rules/dev.md +0 -82
- package/.windsurf/rules/pm.md +0 -71
- package/.windsurf/rules/po.md +0 -72
- package/.windsurf/rules/qa.md +0 -64
- package/.windsurf/rules/sm.md +0 -67
- package/.windsurf/rules/ux-expert.md +0 -78
- package/docs/bmad-workflow-guide.md +0 -161
- package/docs/claude-code-guide.md +0 -119
- package/docs/cursor-guide.md +0 -127
- package/docs/roo-code-guide.md +0 -140
- package/docs/sample-output/simple-fullstack-greenfield/prd.md +0 -42
- package/docs/windsurf-guide.md +0 -127
- package/expansion-packs/infrastructure-devops/manifest.yml +0 -38
- package/expansion-packs/infrastructure-devops/templates/infrastructure-architecture-tmpl.md +0 -415
- package/expansion-packs/infrastructure-devops/templates/infrastructure-platform-from-arch-tmpl.md +0 -0
- package/tools/installer/config/install.config.yml +0 -139
- package/tools/installer/package-lock.json +0 -906
- package/tools/installer/templates/claude-commands.md +0 -7
- package/tools/installer/templates/cursor-rules.md +0 -22
- package/tools/installer/templates/windsurf-rules.md +0 -22
- package/tools/semantic-release-sync-installer.js +0 -31
- /package/{.bmad-core → bmad-core}/data/technical-preferences.md +0 -0
- /package/{.bmad-core → bmad-core}/tasks/brownfield-create-epic.md +0 -0
- /package/{.bmad-core → bmad-core}/tasks/brownfield-create-story.md +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
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.
|
|
53
|
+
* @param {string} startDir
|
|
54
|
+
* @returns {Promise<string|null>} project root directory or null if not found
|
|
55
|
+
*/
|
|
56
|
+
async function findProjectRoot(startDir) {
|
|
57
|
+
try {
|
|
58
|
+
// Resolve symlinks for robustness (e.g., when invoked from a symlinked path)
|
|
59
|
+
let dir = path.resolve(startDir);
|
|
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
|
+
}
|
|
176
|
+
|
|
177
|
+
while (true) {
|
|
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)) })),
|
|
185
|
+
);
|
|
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
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (dir === fsRoot) break;
|
|
195
|
+
dir = path.dirname(dir);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const out = best ? best.dir : null;
|
|
199
|
+
_cache.set(startKey, out);
|
|
200
|
+
return out;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = { findProjectRoot };
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
|
|
6
|
+
function expandHome(p) {
|
|
7
|
+
if (!p) return p;
|
|
8
|
+
if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
|
|
9
|
+
return p;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createRl() {
|
|
13
|
+
return readline.createInterface({
|
|
14
|
+
input: process.stdin,
|
|
15
|
+
output: process.stdout,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function promptQuestion(question) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const rl = createRl();
|
|
22
|
+
rl.question(question, (answer) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(answer);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function promptYesNo(question, defaultYes = true) {
|
|
30
|
+
const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
|
|
31
|
+
const ans = (await promptQuestion(`${question}${suffix}`)).trim().toLowerCase();
|
|
32
|
+
if (!ans) return defaultYes;
|
|
33
|
+
if (['y', 'yes'].includes(ans)) return true;
|
|
34
|
+
if (['n', 'no'].includes(ans)) return false;
|
|
35
|
+
return promptYesNo(question, defaultYes);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function promptPath(question, defaultValue) {
|
|
39
|
+
const prompt = `${question}${defaultValue ? ` (default: ${defaultValue})` : ''}: `;
|
|
40
|
+
const ans = (await promptQuestion(prompt)).trim();
|
|
41
|
+
return expandHome(ans || defaultValue);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { promptYesNo, promptPath, promptQuestion, expandHome };
|
|
@@ -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
|
+
};
|