bmad-plus 0.12.1 → 0.13.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 (176) hide show
  1. package/CHANGELOG.md +637 -580
  2. package/README.md +123 -120
  3. package/osint-agent-package/agents/osint-investigator.md +12 -0
  4. package/osint-agent-package/skills/bmad-osint-investigate/osint/SKILL.md +491 -482
  5. package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/dossier-template.md +126 -126
  6. package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/lawful-basis-record.md +48 -48
  7. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/content-extraction.md +100 -100
  8. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/gdpr-osint.md +48 -48
  9. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/platforms.md +130 -130
  10. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/psychoprofile.md +69 -69
  11. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/tools.md +281 -281
  12. package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/mcp-client.py +136 -136
  13. package/package.json +106 -91
  14. package/readme-international/README.de.md +625 -594
  15. package/readme-international/README.es.md +642 -611
  16. package/readme-international/README.fr.md +640 -609
  17. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +2 -0
  18. package/src/bmad-plus/agents/agent-shadow/SKILL.md +18 -0
  19. package/src/bmad-plus/data/role-triggers.yaml +52 -0
  20. package/src/bmad-plus/module.yaml +333 -283
  21. package/src/bmad-plus/packs/pack-animated/animated-website-agent.md +325 -325
  22. package/src/bmad-plus/packs/pack-animated/templates/animated-website-workflow.md +55 -55
  23. package/src/bmad-plus/packs/pack-backup/backup-agent.md +71 -71
  24. package/src/bmad-plus/packs/pack-backup/templates/backup-workflow.md +51 -51
  25. package/src/bmad-plus/packs/pack-dev-studio/README.md +162 -162
  26. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +73 -73
  27. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +61 -61
  28. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +95 -95
  29. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +95 -95
  30. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +134 -134
  31. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +80 -80
  32. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +73 -73
  33. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +95 -95
  34. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +73 -73
  35. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +73 -73
  36. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +92 -92
  37. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +80 -80
  38. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +90 -90
  39. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +153 -153
  40. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +173 -173
  41. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +224 -224
  42. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +329 -329
  43. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +318 -318
  44. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +359 -359
  45. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +379 -379
  46. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +361 -361
  47. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +81 -81
  48. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +67 -67
  49. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +85 -85
  50. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +35 -35
  51. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +49 -49
  52. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +131 -131
  53. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +89 -89
  54. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +300 -300
  55. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +428 -428
  56. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +73 -73
  57. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +80 -80
  58. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +484 -484
  59. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +193 -193
  60. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +175 -175
  61. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +110 -110
  62. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +1511 -1511
  63. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +298 -298
  64. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +296 -296
  65. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +29 -29
  66. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +74 -74
  67. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +29 -29
  68. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +73 -73
  69. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +89 -89
  70. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +73 -73
  71. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +29 -29
  72. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +141 -141
  73. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +37 -37
  74. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +75 -75
  75. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +6 -6
  76. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +110 -110
  77. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +176 -176
  78. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +67 -67
  79. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +86 -86
  80. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +179 -179
  81. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +66 -66
  82. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +127 -127
  83. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +105 -105
  84. package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +120 -120
  85. package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +12 -12
  86. package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +328 -328
  87. package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +32 -32
  88. package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +81 -81
  89. package/src/bmad-plus/packs/pack-seo/SKILL.md +171 -171
  90. package/src/bmad-plus/packs/pack-seo/checklist.md +140 -140
  91. package/src/bmad-plus/packs/pack-seo/pagespeed-playbook.md +320 -320
  92. package/src/bmad-plus/packs/pack-seo/ref/audit-schema.json +187 -187
  93. package/src/bmad-plus/packs/pack-seo/ref/cwv-thresholds.md +87 -87
  94. package/src/bmad-plus/packs/pack-seo/ref/eeat-criteria.md +123 -123
  95. package/src/bmad-plus/packs/pack-seo/ref/geo-signals.md +167 -167
  96. package/src/bmad-plus/packs/pack-seo/ref/hreflang-rules.md +153 -153
  97. package/src/bmad-plus/packs/pack-seo/ref/quality-gates.md +133 -133
  98. package/src/bmad-plus/packs/pack-seo/ref/schema-catalog.md +91 -91
  99. package/src/bmad-plus/packs/pack-seo/ref/schema-templates.json +356 -356
  100. package/src/bmad-plus/packs/pack-seo/requirements.txt +17 -0
  101. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +456 -0
  102. package/src/bmad-plus/packs/pack-seo/scripts/seo_crawl.py +359 -0
  103. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +303 -0
  104. package/src/bmad-plus/packs/pack-seo/scripts/seo_parse.py +255 -0
  105. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +409 -0
  106. package/src/bmad-plus/packs/pack-seo/scripts/seo_screenshot.py +202 -0
  107. package/src/bmad-plus/packs/pack-seo/seo-chief.md +294 -294
  108. package/src/bmad-plus/packs/pack-seo/seo-judge.md +241 -241
  109. package/src/bmad-plus/packs/pack-seo/seo-scout.md +171 -171
  110. package/src/bmad-plus/packs/pack-seo/templates/seo-audit-workflow.md +241 -241
  111. package/src/bmad-plus/packs/pack-shield/README.md +6 -6
  112. package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -2
  113. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/csrd-agent.md +11 -11
  114. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/section508-agent.md +11 -11
  115. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/wcag-agent.md +11 -11
  116. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/eu-ai-act-agent.md +11 -11
  117. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/iso42001-agent.md +11 -11
  118. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/nist-ai-rmf-agent.md +11 -11
  119. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/cis-controls-agent.md +11 -11
  120. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/ism-agent.md +11 -11
  121. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/iso27001-agent.md +11 -11
  122. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nis2-agent.md +11 -11
  123. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-800-53-agent.md +11 -11
  124. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-csf-agent.md +11 -11
  125. package/src/bmad-plus/packs/pack-shield/categories/defense-export/cmmc-agent.md +11 -11
  126. package/src/bmad-plus/packs/pack-shield/categories/defense-export/ear-agent.md +11 -11
  127. package/src/bmad-plus/packs/pack-shield/categories/defense-export/itar-agent.md +11 -11
  128. package/src/bmad-plus/packs/pack-shield/categories/defense-export/tsa-agent.md +11 -11
  129. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/dora-agent.md +11 -11
  130. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/fedramp-agent.md +11 -11
  131. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/hipaa-agent.md +11 -11
  132. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/pci-dss-agent.md +11 -11
  133. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/soc2-agent.md +11 -11
  134. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/swift-csp-agent.md +11 -11
  135. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +1 -1
  136. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +1 -1
  137. package/tools/bmad-plus-npx.js +4 -2
  138. package/tools/build/adapters.config.js +60 -51
  139. package/tools/build/check-counts.js +626 -0
  140. package/tools/build/check-install-contract.js +298 -0
  141. package/tools/build/generate-adapters.js +252 -56
  142. package/tools/build/generate.js +187 -10
  143. package/tools/build/generated-adapters/.codex/AGENTS.md +20 -7
  144. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +20 -7
  145. package/tools/build/generated-adapters/.opencode/AGENTS.md +20 -7
  146. package/tools/build/generated-adapters/AGENTS.md +20 -7
  147. package/tools/build/generated-adapters/CLAUDE.md +20 -7
  148. package/tools/build/generated-adapters/CONVENTIONS.md +20 -7
  149. package/tools/build/generated-adapters/GEMINI.md +20 -7
  150. package/tools/build/module.template.yaml +82 -0
  151. package/tools/cli/bmad-plus-cli.js +16 -1
  152. package/tools/cli/commands/autoconfig.js +470 -470
  153. package/tools/cli/commands/doctor.js +205 -233
  154. package/tools/cli/commands/install.js +543 -501
  155. package/tools/cli/commands/memory-journal-cmd.js +311 -311
  156. package/tools/cli/commands/memory.js +195 -195
  157. package/tools/cli/commands/scan.js +348 -348
  158. package/tools/cli/commands/uninstall.js +209 -101
  159. package/tools/cli/commands/update-check.js +31 -0
  160. package/tools/cli/commands/update-policy.js +39 -0
  161. package/tools/cli/commands/update.js +123 -134
  162. package/tools/cli/i18n.js +905 -845
  163. package/tools/cli/lib/README-memory-journal.md +125 -125
  164. package/tools/cli/lib/ide-config.js +10 -259
  165. package/tools/cli/lib/install-manifest.js +17 -0
  166. package/tools/cli/lib/installed-adapters.js +89 -0
  167. package/tools/cli/lib/npm-runner.js +177 -0
  168. package/tools/cli/lib/pack-copy.js +62 -66
  169. package/tools/cli/lib/packs.js +437 -3
  170. package/tools/cli/lib/python-provision.js +508 -508
  171. package/tools/cli/lib/stack-detect.js +102 -102
  172. package/tools/cli/lib/update-check.js +153 -0
  173. package/tools/cli/lib/update-dispatch.js +182 -0
  174. package/tools/cli/lib/update-policy.js +90 -0
  175. package/tools/cli/lib/update-transaction.js +334 -0
  176. package/tools/cli/lib/validate.js +50 -50
@@ -1,348 +1,348 @@
1
- /**
2
- * BMAD+ Scan Command
3
- * Scan directories to discover projects, detect stacks, and index them in the global brain.
4
- * Interactive validation — user confirms each project before indexing.
5
- *
6
- * Author: Laurent Rochetta
7
- */
8
-
9
- const path = require('node:path');
10
- const fs = require('node:fs');
11
- const os = require('node:os');
12
- const clack = require('@clack/prompts');
13
- const pc = require('picocolors');
14
- const { PROJECT_MARKERS } = require('../lib/stack-detect');
15
- const { normalizePathForHash, projectHash } = require('../lib/path-hash');
16
-
17
- // Directories to skip during scanning
18
- const SKIP_DIRS = new Set([
19
- 'node_modules', '.git', 'vendor', '__pycache__', 'dist', 'build',
20
- '.next', '.nuxt', '.svelte-kit', 'target', '.venv', 'venv',
21
- '.cache', '.output', 'coverage', '.turbo', '.angular',
22
- '$RECYCLE.BIN', 'System Volume Information', 'Windows',
23
- 'Program Files', 'Program Files (x86)', 'ProgramData',
24
- 'AppData', 'Recovery', 'PerfLogs',
25
- ]);
26
-
27
- function getProjectStatus(dir, activeDays = 30, pausedDays = 180) {
28
- try {
29
- const stat = fs.statSync(dir);
30
- const daysSince = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24);
31
- if (daysSince < activeDays) return 'active';
32
- if (daysSince < pausedDays) return 'paused';
33
- return 'archived';
34
- } catch { return 'unknown'; }
35
- }
36
-
37
- function getStatusIcon(status) {
38
- switch (status) {
39
- case 'active': return pc.green('●');
40
- case 'paused': return pc.yellow('◐');
41
- case 'archived': return pc.dim('○');
42
- default: return pc.dim('?');
43
- }
44
- }
45
-
46
- function getProjectName(dir) {
47
- try {
48
- const pkgPath = path.join(dir, 'package.json');
49
- if (fs.existsSync(pkgPath)) {
50
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
51
- if (pkg.name) return pkg.name;
52
- }
53
- } catch {
54
- // Malformed/unreadable package.json; fall back to directory name.
55
- }
56
- return path.basename(dir);
57
- }
58
-
59
- function hasBmadInstalled(dir) {
60
- return fs.existsSync(path.join(dir, '.agents')) ||
61
- fs.existsSync(path.join(dir, '_bmad'));
62
- }
63
-
64
- function scanDirectory(rootDir, maxDepth = 4, currentDepth = 0, activeDays = 30, pausedDays = 180) {
65
- const projects = [];
66
-
67
- if (currentDepth > maxDepth) return projects;
68
-
69
- let entries;
70
- try {
71
- entries = fs.readdirSync(rootDir, { withFileTypes: true });
72
- } catch {
73
- return projects; // Permission denied or inaccessible
74
- }
75
-
76
- // Check if current dir is a project
77
- for (const marker of PROJECT_MARKERS) {
78
- if (fs.existsSync(path.join(rootDir, marker.file))) {
79
- const stack = marker.detect ? marker.detect(rootDir) : marker.stack;
80
- projects.push({
81
- path: rootDir,
82
- name: getProjectName(rootDir),
83
- stack,
84
- status: getProjectStatus(rootDir, activeDays, pausedDays),
85
- bmad: hasBmadInstalled(rootDir),
86
- hasAgentsMd: fs.existsSync(path.join(rootDir, 'AGENTS.md')),
87
- hasGit: fs.existsSync(path.join(rootDir, '.git')),
88
- });
89
- return projects; // Don't recurse into project subdirs
90
- }
91
- }
92
-
93
- // Also detect by .git alone (any project with version control)
94
- if (fs.existsSync(path.join(rootDir, '.git')) && currentDepth > 0) {
95
- projects.push({
96
- path: rootDir,
97
- name: getProjectName(rootDir),
98
- stack: 'Unknown',
99
- status: getProjectStatus(rootDir, activeDays, pausedDays),
100
- bmad: hasBmadInstalled(rootDir),
101
- hasAgentsMd: fs.existsSync(path.join(rootDir, 'AGENTS.md')),
102
- hasGit: true,
103
- });
104
- return projects;
105
- }
106
-
107
- // Recurse into subdirectories
108
- for (const entry of entries) {
109
- if (!entry.isDirectory()) continue;
110
- if (SKIP_DIRS.has(entry.name)) continue;
111
- if (entry.name.startsWith('.') && entry.name !== '.git') continue;
112
-
113
- const subPath = path.join(rootDir, entry.name);
114
- const subProjects = scanDirectory(subPath, maxDepth, currentDepth + 1, activeDays, pausedDays);
115
- projects.push(...subProjects);
116
- }
117
-
118
- return projects;
119
- }
120
-
121
- /**
122
- * Index a single project in the global brain by writing its metadata YAML file.
123
- * @param {object} project - Project metadata object
124
- * @param {string} globalBrainDir - Path to the brain projects directory
125
- * @returns {void}
126
- */
127
- function indexProject(project, globalBrainDir) {
128
- const hash = projectHash(project.path);
129
- const meta = {
130
- path: project.path,
131
- name: project.name,
132
- hash,
133
- stack: project.stack,
134
- status: project.status,
135
- bmad_installed: project.bmad,
136
- has_git: project.hasGit,
137
- last_scanned: new Date().toISOString().slice(0, 10),
138
- };
139
- fs.writeFileSync(
140
- path.join(globalBrainDir, `${hash}.yaml`),
141
- Object.entries(meta).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join('\n'),
142
- 'utf8'
143
- );
144
- }
145
-
146
- /**
147
- * Index multiple projects in the global brain.
148
- * @param {object[]} projectList - Array of project metadata objects
149
- * @param {string} brainDir - Path to the brain projects directory
150
- * @returns {number} Number of projects indexed
151
- */
152
- function indexProjects(projectList, brainDir) {
153
- let count = 0;
154
- for (const proj of projectList) {
155
- indexProject(proj, brainDir);
156
- count++;
157
- }
158
- return count;
159
- }
160
-
161
- module.exports = {
162
- command: 'scan',
163
- description: 'Scan directories to discover and index projects in the global brain',
164
- options: [
165
- ['-d, --directory <path>', 'Directory to scan (default: current directory)'],
166
- ['--depth <n>', 'Max depth to scan (default: 4)', '4'],
167
- ['--active-days <n>', 'Days since last modified to consider a project "active" (default: 30)', '30'],
168
- ['--paused-days <n>', 'Days since last modified to consider a project "paused" (default: 180)', '180'],
169
- ['-y, --yes', 'Index all projects without prompting'],
170
- ['-l, --lang <code>', 'Language code: en, fr, es, de, pt-br, ru, zh, he, ja, it'],
171
- ],
172
- action: async (options) => {
173
- const scanDir = path.resolve(options.directory || process.cwd());
174
-
175
- const rawDepth = parseInt(options.depth, 10);
176
- const maxDepth = (!isNaN(rawDepth) && rawDepth > 0) ? rawDepth : (() => { clack.log.warn(`Invalid --depth "${options.depth}", defaulting to 4`); return 4; })();
177
-
178
- const rawActive = parseInt(options.activeDays, 10);
179
- const activeDays = (!isNaN(rawActive) && rawActive > 0) ? rawActive : (() => { clack.log.warn(`Invalid --active-days "${options.activeDays}", defaulting to 30`); return 30; })();
180
-
181
- const rawPaused = parseInt(options.pausedDays, 10);
182
- const pausedDays = (!isNaN(rawPaused) && rawPaused > 0) ? rawPaused : (() => { clack.log.warn(`Invalid --paused-days "${options.pausedDays}", defaulting to 180`); return 180; })();
183
-
184
- clack.intro(pc.bgMagenta(pc.white(' 🧠 BMAD+ Project Scanner ')));
185
-
186
- // Verify directory exists
187
- if (!fs.existsSync(scanDir)) {
188
- clack.log.error(`Directory not found: ${scanDir}`);
189
- clack.outro(pc.red('Scan failed.'));
190
- return;
191
- }
192
-
193
- // Scan
194
- const spinner = clack.spinner();
195
- spinner.start(`Scanning ${scanDir} (depth: ${maxDepth})...`);
196
-
197
- const projects = scanDirectory(scanDir, maxDepth, 0, activeDays, pausedDays);
198
-
199
- if (projects.length === 0) {
200
- spinner.stop('No projects found.');
201
- clack.outro('Try scanning a different directory or increasing --depth');
202
- return;
203
- }
204
-
205
- spinner.stop(`Found ${pc.bold(projects.length)} project(s)`);
206
-
207
- // Display legend
208
- const activeCount = projects.filter(p => p.status === 'active').length;
209
- const pausedCount = projects.filter(p => p.status === 'paused').length;
210
- const archivedCount = projects.filter(p => p.status === 'archived').length;
211
-
212
- clack.log.info('');
213
- clack.log.info(pc.dim(' Legend:'));
214
- clack.log.info(` ${pc.green('●')} active modified < ${activeDays} days ago ${pc.dim(`(${activeCount} found)`)}`);
215
- clack.log.info(` ${pc.yellow('◐')} paused modified ${activeDays}–${pausedDays} days ago ${pc.dim(`(${pausedCount} found)`)}`);
216
- clack.log.info(` ${pc.dim('○')} archived modified > ${pausedDays} days ago ${pc.dim(`(${archivedCount} found)`)}`);
217
- clack.log.info('');
218
-
219
- // Display table
220
- clack.log.info(pc.bold(' # Status BMAD+ Stack Name Path'));
221
- clack.log.info(pc.dim(' ' + '─'.repeat(90)));
222
-
223
- projects.forEach((p, i) => {
224
- const num = String(i + 1).padStart(3);
225
- const status = getStatusIcon(p.status) + ' ' + p.status.padEnd(8);
226
- const bmad = p.bmad ? pc.green('✓') : pc.dim('·');
227
- const stack = p.stack.padEnd(16);
228
- const name = p.name.substring(0, 20).padEnd(20);
229
- const projPath = p.path.length > 40 ? '...' + p.path.slice(-37) : p.path;
230
- clack.log.info(` ${num} ${status} ${bmad} ${stack} ${name} ${pc.dim(projPath)}`);
231
- });
232
-
233
- clack.log.info('');
234
-
235
- // Interactive validation
236
- const globalBrainDir = path.join(os.homedir(), '.bmad-plus', 'brain', 'projects');
237
-
238
- if (options.yes) {
239
- // Auto-index all
240
- const fsExtra = require('fs-extra');
241
- fsExtra.ensureDirSync(globalBrainDir);
242
-
243
- const indexed = indexProjects(projects, globalBrainDir);
244
- clack.log.success(`✅ ${indexed} project(s) indexed in ${globalBrainDir}`);
245
- } else {
246
- // Interactive mode
247
- const action = await clack.select({
248
- message: `${projects.length} project(s) found. What to do?`,
249
- options: [
250
- { value: 'all', label: `✅ Index all ${projects.length} projects` },
251
- { value: 'select', label: '✏️ Select which to index' },
252
- { value: 'none', label: '⏭️ Skip — don\'t index anything' },
253
- ],
254
- });
255
-
256
- if (clack.isCancel(action) || action === 'none') {
257
- clack.cancel('Scan cancelled.');
258
- return;
259
- }
260
-
261
- const fsExtra = require('fs-extra');
262
- fsExtra.ensureDirSync(globalBrainDir);
263
-
264
- let toIndex = projects;
265
-
266
- if (action === 'select') {
267
- const selected = await clack.multiselect({
268
- message: 'Select projects to index:',
269
- options: projects.map((p, i) => ({
270
- value: i,
271
- label: `${p.name} (${p.stack})`,
272
- hint: `${p.status} — ${p.path}`,
273
- })),
274
- required: false,
275
- });
276
-
277
- if (clack.isCancel(selected)) {
278
- clack.cancel('Scan cancelled.');
279
- return;
280
- }
281
-
282
- toIndex = selected.map(i => projects[i]);
283
- }
284
-
285
- const indexed = indexProjects(toIndex, globalBrainDir);
286
- clack.log.success(`✅ ${indexed} project(s) indexed in global brain`);
287
- }
288
-
289
- // Generate human-readable index
290
- const indexPath = path.join(os.homedir(), '.bmad-plus', 'brain', 'projects-index.md');
291
- const existingProjects = [];
292
- if (fs.existsSync(globalBrainDir)) {
293
- for (const f of fs.readdirSync(globalBrainDir)) {
294
- if (!f.endsWith('.yaml')) continue;
295
- try {
296
- const content = fs.readFileSync(path.join(globalBrainDir, f), 'utf8');
297
- const meta = {};
298
- for (const line of content.split('\n')) {
299
- const m = line.match(/^(\w+):\s*(.+)$/);
300
- if (m) {
301
- try { meta[m[1]] = JSON.parse(m[2]); } catch { meta[m[1]] = m[2]; }
302
- }
303
- }
304
- existingProjects.push(meta);
305
- } catch {
306
- // Skip a project entry whose index file is unreadable/corrupt.
307
- }
308
- }
309
- }
310
-
311
- const indexContent = [
312
- '---',
313
- 'title: Project Index',
314
- `last_updated: "${new Date().toISOString().slice(0, 10)}"`,
315
- `total_projects: ${existingProjects.length}`,
316
- '---',
317
- '',
318
- '# Project Index',
319
- '',
320
- `> Auto-generated by \`npx bmad-plus scan\` — ${new Date().toISOString().slice(0, 10)}`,
321
- '',
322
- '| Status | Name | Stack | BMAD+ | Path |',
323
- '|--------|------|-------|-------|------|',
324
- ...existingProjects.map(p =>
325
- `| ${p.status || '?'} | ${p.name || '?'} | ${p.stack || '?'} | ${p.bmad_installed ? '✓' : '·'} | \`${p.path || '?'}\` |`
326
- ),
327
- '',
328
- ];
329
-
330
- fs.writeFileSync(indexPath, indexContent.join('\n'), 'utf8');
331
- clack.log.info(`📋 Project index updated: ${indexPath}`);
332
-
333
- clack.outro(pc.green('Scan complete! 🧠'));
334
- },
335
- // Exported for functional testing (not part of the public API)
336
- _internal: {
337
- PROJECT_MARKERS,
338
- SKIP_DIRS,
339
- getProjectStatus,
340
- getStatusIcon,
341
- getProjectName,
342
- hasBmadInstalled,
343
- scanDirectory,
344
- normalizePathForHash,
345
- indexProject,
346
- indexProjects,
347
- },
348
- };
1
+ /**
2
+ * BMAD+ Scan Command
3
+ * Scan directories to discover projects, detect stacks, and index them in the global brain.
4
+ * Interactive validation — user confirms each project before indexing.
5
+ *
6
+ * Author: Laurent Rochetta
7
+ */
8
+
9
+ const path = require('node:path');
10
+ const fs = require('node:fs');
11
+ const os = require('node:os');
12
+ const clack = require('@clack/prompts');
13
+ const pc = require('picocolors');
14
+ const { PROJECT_MARKERS } = require('../lib/stack-detect');
15
+ const { normalizePathForHash, projectHash } = require('../lib/path-hash');
16
+
17
+ // Directories to skip during scanning
18
+ const SKIP_DIRS = new Set([
19
+ 'node_modules', '.git', 'vendor', '__pycache__', 'dist', 'build',
20
+ '.next', '.nuxt', '.svelte-kit', 'target', '.venv', 'venv',
21
+ '.cache', '.output', 'coverage', '.turbo', '.angular',
22
+ '$RECYCLE.BIN', 'System Volume Information', 'Windows',
23
+ 'Program Files', 'Program Files (x86)', 'ProgramData',
24
+ 'AppData', 'Recovery', 'PerfLogs',
25
+ ]);
26
+
27
+ function getProjectStatus(dir, activeDays = 30, pausedDays = 180) {
28
+ try {
29
+ const stat = fs.statSync(dir);
30
+ const daysSince = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24);
31
+ if (daysSince < activeDays) return 'active';
32
+ if (daysSince < pausedDays) return 'paused';
33
+ return 'archived';
34
+ } catch { return 'unknown'; }
35
+ }
36
+
37
+ function getStatusIcon(status) {
38
+ switch (status) {
39
+ case 'active': return pc.green('●');
40
+ case 'paused': return pc.yellow('◐');
41
+ case 'archived': return pc.dim('○');
42
+ default: return pc.dim('?');
43
+ }
44
+ }
45
+
46
+ function getProjectName(dir) {
47
+ try {
48
+ const pkgPath = path.join(dir, 'package.json');
49
+ if (fs.existsSync(pkgPath)) {
50
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
51
+ if (pkg.name) return pkg.name;
52
+ }
53
+ } catch {
54
+ // Malformed/unreadable package.json; fall back to directory name.
55
+ }
56
+ return path.basename(dir);
57
+ }
58
+
59
+ function hasBmadInstalled(dir) {
60
+ return fs.existsSync(path.join(dir, '.agents')) ||
61
+ fs.existsSync(path.join(dir, '_bmad'));
62
+ }
63
+
64
+ function scanDirectory(rootDir, maxDepth = 4, currentDepth = 0, activeDays = 30, pausedDays = 180) {
65
+ const projects = [];
66
+
67
+ if (currentDepth > maxDepth) return projects;
68
+
69
+ let entries;
70
+ try {
71
+ entries = fs.readdirSync(rootDir, { withFileTypes: true });
72
+ } catch {
73
+ return projects; // Permission denied or inaccessible
74
+ }
75
+
76
+ // Check if current dir is a project
77
+ for (const marker of PROJECT_MARKERS) {
78
+ if (fs.existsSync(path.join(rootDir, marker.file))) {
79
+ const stack = marker.detect ? marker.detect(rootDir) : marker.stack;
80
+ projects.push({
81
+ path: rootDir,
82
+ name: getProjectName(rootDir),
83
+ stack,
84
+ status: getProjectStatus(rootDir, activeDays, pausedDays),
85
+ bmad: hasBmadInstalled(rootDir),
86
+ hasAgentsMd: fs.existsSync(path.join(rootDir, 'AGENTS.md')),
87
+ hasGit: fs.existsSync(path.join(rootDir, '.git')),
88
+ });
89
+ return projects; // Don't recurse into project subdirs
90
+ }
91
+ }
92
+
93
+ // Also detect by .git alone (any project with version control)
94
+ if (fs.existsSync(path.join(rootDir, '.git')) && currentDepth > 0) {
95
+ projects.push({
96
+ path: rootDir,
97
+ name: getProjectName(rootDir),
98
+ stack: 'Unknown',
99
+ status: getProjectStatus(rootDir, activeDays, pausedDays),
100
+ bmad: hasBmadInstalled(rootDir),
101
+ hasAgentsMd: fs.existsSync(path.join(rootDir, 'AGENTS.md')),
102
+ hasGit: true,
103
+ });
104
+ return projects;
105
+ }
106
+
107
+ // Recurse into subdirectories
108
+ for (const entry of entries) {
109
+ if (!entry.isDirectory()) continue;
110
+ if (SKIP_DIRS.has(entry.name)) continue;
111
+ if (entry.name.startsWith('.') && entry.name !== '.git') continue;
112
+
113
+ const subPath = path.join(rootDir, entry.name);
114
+ const subProjects = scanDirectory(subPath, maxDepth, currentDepth + 1, activeDays, pausedDays);
115
+ projects.push(...subProjects);
116
+ }
117
+
118
+ return projects;
119
+ }
120
+
121
+ /**
122
+ * Index a single project in the global brain by writing its metadata YAML file.
123
+ * @param {object} project - Project metadata object
124
+ * @param {string} globalBrainDir - Path to the brain projects directory
125
+ * @returns {void}
126
+ */
127
+ function indexProject(project, globalBrainDir) {
128
+ const hash = projectHash(project.path);
129
+ const meta = {
130
+ path: project.path,
131
+ name: project.name,
132
+ hash,
133
+ stack: project.stack,
134
+ status: project.status,
135
+ bmad_installed: project.bmad,
136
+ has_git: project.hasGit,
137
+ last_scanned: new Date().toISOString().slice(0, 10),
138
+ };
139
+ fs.writeFileSync(
140
+ path.join(globalBrainDir, `${hash}.yaml`),
141
+ Object.entries(meta).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join('\n'),
142
+ 'utf8'
143
+ );
144
+ }
145
+
146
+ /**
147
+ * Index multiple projects in the global brain.
148
+ * @param {object[]} projectList - Array of project metadata objects
149
+ * @param {string} brainDir - Path to the brain projects directory
150
+ * @returns {number} Number of projects indexed
151
+ */
152
+ function indexProjects(projectList, brainDir) {
153
+ let count = 0;
154
+ for (const proj of projectList) {
155
+ indexProject(proj, brainDir);
156
+ count++;
157
+ }
158
+ return count;
159
+ }
160
+
161
+ module.exports = {
162
+ command: 'scan',
163
+ description: 'Scan directories to discover and index projects in the global brain',
164
+ options: [
165
+ ['-d, --directory <path>', 'Directory to scan (default: current directory)'],
166
+ ['--depth <n>', 'Max depth to scan (default: 4)', '4'],
167
+ ['--active-days <n>', 'Days since last modified to consider a project "active" (default: 30)', '30'],
168
+ ['--paused-days <n>', 'Days since last modified to consider a project "paused" (default: 180)', '180'],
169
+ ['-y, --yes', 'Index all projects without prompting'],
170
+ ['-l, --lang <code>', 'Language code: en, fr, es, de, pt-br, ru, zh, he, ja, it'],
171
+ ],
172
+ action: async (options) => {
173
+ const scanDir = path.resolve(options.directory || process.cwd());
174
+
175
+ const rawDepth = parseInt(options.depth, 10);
176
+ const maxDepth = (!isNaN(rawDepth) && rawDepth > 0) ? rawDepth : (() => { clack.log.warn(`Invalid --depth "${options.depth}", defaulting to 4`); return 4; })();
177
+
178
+ const rawActive = parseInt(options.activeDays, 10);
179
+ const activeDays = (!isNaN(rawActive) && rawActive > 0) ? rawActive : (() => { clack.log.warn(`Invalid --active-days "${options.activeDays}", defaulting to 30`); return 30; })();
180
+
181
+ const rawPaused = parseInt(options.pausedDays, 10);
182
+ const pausedDays = (!isNaN(rawPaused) && rawPaused > 0) ? rawPaused : (() => { clack.log.warn(`Invalid --paused-days "${options.pausedDays}", defaulting to 180`); return 180; })();
183
+
184
+ clack.intro(pc.bgMagenta(pc.white(' 🧠 BMAD+ Project Scanner ')));
185
+
186
+ // Verify directory exists
187
+ if (!fs.existsSync(scanDir)) {
188
+ clack.log.error(`Directory not found: ${scanDir}`);
189
+ clack.outro(pc.red('Scan failed.'));
190
+ return;
191
+ }
192
+
193
+ // Scan
194
+ const spinner = clack.spinner();
195
+ spinner.start(`Scanning ${scanDir} (depth: ${maxDepth})...`);
196
+
197
+ const projects = scanDirectory(scanDir, maxDepth, 0, activeDays, pausedDays);
198
+
199
+ if (projects.length === 0) {
200
+ spinner.stop('No projects found.');
201
+ clack.outro('Try scanning a different directory or increasing --depth');
202
+ return;
203
+ }
204
+
205
+ spinner.stop(`Found ${pc.bold(projects.length)} project(s)`);
206
+
207
+ // Display legend
208
+ const activeCount = projects.filter(p => p.status === 'active').length;
209
+ const pausedCount = projects.filter(p => p.status === 'paused').length;
210
+ const archivedCount = projects.filter(p => p.status === 'archived').length;
211
+
212
+ clack.log.info('');
213
+ clack.log.info(pc.dim(' Legend:'));
214
+ clack.log.info(` ${pc.green('●')} active modified < ${activeDays} days ago ${pc.dim(`(${activeCount} found)`)}`);
215
+ clack.log.info(` ${pc.yellow('◐')} paused modified ${activeDays}–${pausedDays} days ago ${pc.dim(`(${pausedCount} found)`)}`);
216
+ clack.log.info(` ${pc.dim('○')} archived modified > ${pausedDays} days ago ${pc.dim(`(${archivedCount} found)`)}`);
217
+ clack.log.info('');
218
+
219
+ // Display table
220
+ clack.log.info(pc.bold(' # Status BMAD+ Stack Name Path'));
221
+ clack.log.info(pc.dim(' ' + '─'.repeat(90)));
222
+
223
+ projects.forEach((p, i) => {
224
+ const num = String(i + 1).padStart(3);
225
+ const status = getStatusIcon(p.status) + ' ' + p.status.padEnd(8);
226
+ const bmad = p.bmad ? pc.green('✓') : pc.dim('·');
227
+ const stack = p.stack.padEnd(16);
228
+ const name = p.name.substring(0, 20).padEnd(20);
229
+ const projPath = p.path.length > 40 ? '...' + p.path.slice(-37) : p.path;
230
+ clack.log.info(` ${num} ${status} ${bmad} ${stack} ${name} ${pc.dim(projPath)}`);
231
+ });
232
+
233
+ clack.log.info('');
234
+
235
+ // Interactive validation
236
+ const globalBrainDir = path.join(os.homedir(), '.bmad-plus', 'brain', 'projects');
237
+
238
+ if (options.yes) {
239
+ // Auto-index all
240
+ const fsExtra = require('fs-extra');
241
+ fsExtra.ensureDirSync(globalBrainDir);
242
+
243
+ const indexed = indexProjects(projects, globalBrainDir);
244
+ clack.log.success(`✅ ${indexed} project(s) indexed in ${globalBrainDir}`);
245
+ } else {
246
+ // Interactive mode
247
+ const action = await clack.select({
248
+ message: `${projects.length} project(s) found. What to do?`,
249
+ options: [
250
+ { value: 'all', label: `✅ Index all ${projects.length} projects` },
251
+ { value: 'select', label: '✏️ Select which to index' },
252
+ { value: 'none', label: '⏭️ Skip — don\'t index anything' },
253
+ ],
254
+ });
255
+
256
+ if (clack.isCancel(action) || action === 'none') {
257
+ clack.cancel('Scan cancelled.');
258
+ return;
259
+ }
260
+
261
+ const fsExtra = require('fs-extra');
262
+ fsExtra.ensureDirSync(globalBrainDir);
263
+
264
+ let toIndex = projects;
265
+
266
+ if (action === 'select') {
267
+ const selected = await clack.multiselect({
268
+ message: 'Select projects to index:',
269
+ options: projects.map((p, i) => ({
270
+ value: i,
271
+ label: `${p.name} (${p.stack})`,
272
+ hint: `${p.status} — ${p.path}`,
273
+ })),
274
+ required: false,
275
+ });
276
+
277
+ if (clack.isCancel(selected)) {
278
+ clack.cancel('Scan cancelled.');
279
+ return;
280
+ }
281
+
282
+ toIndex = selected.map(i => projects[i]);
283
+ }
284
+
285
+ const indexed = indexProjects(toIndex, globalBrainDir);
286
+ clack.log.success(`✅ ${indexed} project(s) indexed in global brain`);
287
+ }
288
+
289
+ // Generate human-readable index
290
+ const indexPath = path.join(os.homedir(), '.bmad-plus', 'brain', 'projects-index.md');
291
+ const existingProjects = [];
292
+ if (fs.existsSync(globalBrainDir)) {
293
+ for (const f of fs.readdirSync(globalBrainDir)) {
294
+ if (!f.endsWith('.yaml')) continue;
295
+ try {
296
+ const content = fs.readFileSync(path.join(globalBrainDir, f), 'utf8');
297
+ const meta = {};
298
+ for (const line of content.split('\n')) {
299
+ const m = line.match(/^(\w+):\s*(.+)$/);
300
+ if (m) {
301
+ try { meta[m[1]] = JSON.parse(m[2]); } catch { meta[m[1]] = m[2]; }
302
+ }
303
+ }
304
+ existingProjects.push(meta);
305
+ } catch {
306
+ // Skip a project entry whose index file is unreadable/corrupt.
307
+ }
308
+ }
309
+ }
310
+
311
+ const indexContent = [
312
+ '---',
313
+ 'title: Project Index',
314
+ `last_updated: "${new Date().toISOString().slice(0, 10)}"`,
315
+ `total_projects: ${existingProjects.length}`,
316
+ '---',
317
+ '',
318
+ '# Project Index',
319
+ '',
320
+ `> Auto-generated by \`npx bmad-plus scan\` — ${new Date().toISOString().slice(0, 10)}`,
321
+ '',
322
+ '| Status | Name | Stack | BMAD+ | Path |',
323
+ '|--------|------|-------|-------|------|',
324
+ ...existingProjects.map(p =>
325
+ `| ${p.status || '?'} | ${p.name || '?'} | ${p.stack || '?'} | ${p.bmad_installed ? '✓' : '·'} | \`${p.path || '?'}\` |`
326
+ ),
327
+ '',
328
+ ];
329
+
330
+ fs.writeFileSync(indexPath, indexContent.join('\n'), 'utf8');
331
+ clack.log.info(`📋 Project index updated: ${indexPath}`);
332
+
333
+ clack.outro(pc.green('Scan complete! 🧠'));
334
+ },
335
+ // Exported for functional testing (not part of the public API)
336
+ _internal: {
337
+ PROJECT_MARKERS,
338
+ SKIP_DIRS,
339
+ getProjectStatus,
340
+ getStatusIcon,
341
+ getProjectName,
342
+ hasBmadInstalled,
343
+ scanDirectory,
344
+ normalizePathForHash,
345
+ indexProject,
346
+ indexProjects,
347
+ },
348
+ };