bmad-plus 0.12.0 → 0.12.2

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 (160) hide show
  1. package/CHANGELOG.md +612 -554
  2. package/README.md +557 -577
  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 +104 -91
  14. package/readme-international/README.de.md +596 -589
  15. package/readme-international/README.es.md +613 -606
  16. package/readme-international/README.fr.md +611 -604
  17. package/src/bmad-plus/agents/agent-shadow/SKILL.md +18 -0
  18. package/src/bmad-plus/data/role-triggers.yaml +52 -0
  19. package/src/bmad-plus/module.yaml +283 -283
  20. package/src/bmad-plus/packs/pack-animated/animated-website-agent.md +325 -325
  21. package/src/bmad-plus/packs/pack-animated/templates/animated-website-workflow.md +55 -55
  22. package/src/bmad-plus/packs/pack-backup/backup-agent.md +71 -71
  23. package/src/bmad-plus/packs/pack-backup/templates/backup-workflow.md +51 -51
  24. package/src/bmad-plus/packs/pack-dev-studio/README.md +162 -162
  25. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +73 -73
  26. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +61 -61
  27. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +95 -95
  28. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +95 -95
  29. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +134 -134
  30. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +80 -80
  31. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +73 -73
  32. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +95 -95
  33. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +73 -73
  34. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +73 -73
  35. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +92 -92
  36. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +80 -80
  37. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +90 -90
  38. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +153 -153
  39. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +173 -173
  40. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +224 -224
  41. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +329 -329
  42. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +318 -318
  43. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +359 -359
  44. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +379 -379
  45. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +361 -361
  46. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +81 -81
  47. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +67 -67
  48. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +85 -85
  49. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +35 -35
  50. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +49 -49
  51. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +131 -131
  52. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +89 -89
  53. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +300 -300
  54. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +428 -428
  55. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +73 -73
  56. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +80 -80
  57. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +484 -484
  58. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +193 -193
  59. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +175 -175
  60. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +110 -110
  61. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +1511 -1511
  62. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +298 -298
  63. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +296 -296
  64. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +29 -29
  65. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +74 -74
  66. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +29 -29
  67. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +73 -73
  68. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +89 -89
  69. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +73 -73
  70. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +29 -29
  71. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +141 -141
  72. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +37 -37
  73. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +75 -75
  74. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +6 -6
  75. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +110 -110
  76. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +176 -176
  77. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +67 -67
  78. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +86 -86
  79. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +179 -179
  80. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +66 -66
  81. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +127 -127
  82. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +105 -105
  83. package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +120 -120
  84. package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +12 -12
  85. package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +328 -328
  86. package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +32 -32
  87. package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +81 -81
  88. package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +14 -0
  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 +304 -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 +410 -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/tools/build/check-counts.js +628 -0
  137. package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
  138. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
  139. package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
  140. package/tools/build/generated-adapters/AGENTS.md +1 -1
  141. package/tools/build/generated-adapters/CLAUDE.md +1 -1
  142. package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
  143. package/tools/build/generated-adapters/GEMINI.md +1 -1
  144. package/tools/cli/commands/autoconfig.js +470 -472
  145. package/tools/cli/commands/doctor.js +233 -233
  146. package/tools/cli/commands/install.js +598 -495
  147. package/tools/cli/commands/memory-journal-cmd.js +311 -311
  148. package/tools/cli/commands/memory.js +195 -195
  149. package/tools/cli/commands/scan.js +348 -364
  150. package/tools/cli/commands/uninstall.js +101 -101
  151. package/tools/cli/commands/update.js +134 -134
  152. package/tools/cli/i18n.js +845 -845
  153. package/tools/cli/lib/README-memory-journal.md +125 -125
  154. package/tools/cli/lib/ide-config.js +267 -259
  155. package/tools/cli/lib/memory-init.js +204 -113
  156. package/tools/cli/lib/packs.js +209 -209
  157. package/tools/cli/lib/path-hash.js +39 -0
  158. package/tools/cli/lib/python-provision.js +508 -508
  159. package/tools/cli/lib/stack-detect.js +102 -102
  160. package/tools/cli/lib/validate.js +50 -50
@@ -1,364 +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 crypto = require('node:crypto');
13
- const clack = require('@clack/prompts');
14
- const pc = require('picocolors');
15
- const { PROJECT_MARKERS } = require('../lib/stack-detect');
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
- * Normalize a filesystem path for stable hashing.
123
- * On Windows the same project can be referenced as `D:\proj` or `d:\proj`
124
- * (case-insensitive drive) with mixed separators. Resolving + lowercasing the
125
- * drive letter ensures the same project produces the same index key and is not
126
- * double-indexed.
127
- * @param {string} p - Raw filesystem path
128
- * @returns {string} Normalized path suitable for hashing
129
- */
130
- function normalizePathForHash(p) {
131
- let resolved = path.resolve(p);
132
- // Lowercase a leading Windows drive letter (e.g. "D:" → "d:").
133
- resolved = resolved.replace(/^([A-Za-z]):/, (_m, drive) => drive.toLowerCase() + ':');
134
- return resolved;
135
- }
136
-
137
- /**
138
- * Index a single project in the global brain by writing its metadata YAML file.
139
- * @param {object} project - Project metadata object
140
- * @param {string} globalBrainDir - Path to the brain projects directory
141
- * @returns {void}
142
- */
143
- function indexProject(project, globalBrainDir) {
144
- const hash = crypto.createHash('sha256').update(normalizePathForHash(project.path)).digest('hex').slice(0, 8);
145
- const meta = {
146
- path: project.path,
147
- name: project.name,
148
- hash,
149
- stack: project.stack,
150
- status: project.status,
151
- bmad_installed: project.bmad,
152
- has_git: project.hasGit,
153
- last_scanned: new Date().toISOString().slice(0, 10),
154
- };
155
- fs.writeFileSync(
156
- path.join(globalBrainDir, `${hash}.yaml`),
157
- Object.entries(meta).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join('\n'),
158
- 'utf8'
159
- );
160
- }
161
-
162
- /**
163
- * Index multiple projects in the global brain.
164
- * @param {object[]} projectList - Array of project metadata objects
165
- * @param {string} brainDir - Path to the brain projects directory
166
- * @returns {number} Number of projects indexed
167
- */
168
- function indexProjects(projectList, brainDir) {
169
- let count = 0;
170
- for (const proj of projectList) {
171
- indexProject(proj, brainDir);
172
- count++;
173
- }
174
- return count;
175
- }
176
-
177
- module.exports = {
178
- command: 'scan',
179
- description: 'Scan directories to discover and index projects in the global brain',
180
- options: [
181
- ['-d, --directory <path>', 'Directory to scan (default: current directory)'],
182
- ['--depth <n>', 'Max depth to scan (default: 4)', '4'],
183
- ['--active-days <n>', 'Days since last modified to consider a project "active" (default: 30)', '30'],
184
- ['--paused-days <n>', 'Days since last modified to consider a project "paused" (default: 180)', '180'],
185
- ['-y, --yes', 'Index all projects without prompting'],
186
- ['-l, --lang <code>', 'Language code: en, fr, es, de, pt-br, ru, zh, he, ja, it'],
187
- ],
188
- action: async (options) => {
189
- const scanDir = path.resolve(options.directory || process.cwd());
190
-
191
- const rawDepth = parseInt(options.depth, 10);
192
- const maxDepth = (!isNaN(rawDepth) && rawDepth > 0) ? rawDepth : (() => { clack.log.warn(`Invalid --depth "${options.depth}", defaulting to 4`); return 4; })();
193
-
194
- const rawActive = parseInt(options.activeDays, 10);
195
- const activeDays = (!isNaN(rawActive) && rawActive > 0) ? rawActive : (() => { clack.log.warn(`Invalid --active-days "${options.activeDays}", defaulting to 30`); return 30; })();
196
-
197
- const rawPaused = parseInt(options.pausedDays, 10);
198
- const pausedDays = (!isNaN(rawPaused) && rawPaused > 0) ? rawPaused : (() => { clack.log.warn(`Invalid --paused-days "${options.pausedDays}", defaulting to 180`); return 180; })();
199
-
200
- clack.intro(pc.bgMagenta(pc.white(' 🧠 BMAD+ Project Scanner ')));
201
-
202
- // Verify directory exists
203
- if (!fs.existsSync(scanDir)) {
204
- clack.log.error(`Directory not found: ${scanDir}`);
205
- clack.outro(pc.red('Scan failed.'));
206
- return;
207
- }
208
-
209
- // Scan
210
- const spinner = clack.spinner();
211
- spinner.start(`Scanning ${scanDir} (depth: ${maxDepth})...`);
212
-
213
- const projects = scanDirectory(scanDir, maxDepth, 0, activeDays, pausedDays);
214
-
215
- if (projects.length === 0) {
216
- spinner.stop('No projects found.');
217
- clack.outro('Try scanning a different directory or increasing --depth');
218
- return;
219
- }
220
-
221
- spinner.stop(`Found ${pc.bold(projects.length)} project(s)`);
222
-
223
- // Display legend
224
- const activeCount = projects.filter(p => p.status === 'active').length;
225
- const pausedCount = projects.filter(p => p.status === 'paused').length;
226
- const archivedCount = projects.filter(p => p.status === 'archived').length;
227
-
228
- clack.log.info('');
229
- clack.log.info(pc.dim(' Legend:'));
230
- clack.log.info(` ${pc.green('●')} active modified < ${activeDays} days ago ${pc.dim(`(${activeCount} found)`)}`);
231
- clack.log.info(` ${pc.yellow('◐')} paused modified ${activeDays}–${pausedDays} days ago ${pc.dim(`(${pausedCount} found)`)}`);
232
- clack.log.info(` ${pc.dim('○')} archived modified > ${pausedDays} days ago ${pc.dim(`(${archivedCount} found)`)}`);
233
- clack.log.info('');
234
-
235
- // Display table
236
- clack.log.info(pc.bold(' # Status BMAD+ Stack Name Path'));
237
- clack.log.info(pc.dim(' ' + '─'.repeat(90)));
238
-
239
- projects.forEach((p, i) => {
240
- const num = String(i + 1).padStart(3);
241
- const status = getStatusIcon(p.status) + ' ' + p.status.padEnd(8);
242
- const bmad = p.bmad ? pc.green('✓') : pc.dim('·');
243
- const stack = p.stack.padEnd(16);
244
- const name = p.name.substring(0, 20).padEnd(20);
245
- const projPath = p.path.length > 40 ? '...' + p.path.slice(-37) : p.path;
246
- clack.log.info(` ${num} ${status} ${bmad} ${stack} ${name} ${pc.dim(projPath)}`);
247
- });
248
-
249
- clack.log.info('');
250
-
251
- // Interactive validation
252
- const globalBrainDir = path.join(os.homedir(), '.bmad-plus', 'brain', 'projects');
253
-
254
- if (options.yes) {
255
- // Auto-index all
256
- const fsExtra = require('fs-extra');
257
- fsExtra.ensureDirSync(globalBrainDir);
258
-
259
- const indexed = indexProjects(projects, globalBrainDir);
260
- clack.log.success(`✅ ${indexed} project(s) indexed in ${globalBrainDir}`);
261
- } else {
262
- // Interactive mode
263
- const action = await clack.select({
264
- message: `${projects.length} project(s) found. What to do?`,
265
- options: [
266
- { value: 'all', label: `✅ Index all ${projects.length} projects` },
267
- { value: 'select', label: '✏️ Select which to index' },
268
- { value: 'none', label: '⏭️ Skip — don\'t index anything' },
269
- ],
270
- });
271
-
272
- if (clack.isCancel(action) || action === 'none') {
273
- clack.cancel('Scan cancelled.');
274
- return;
275
- }
276
-
277
- const fsExtra = require('fs-extra');
278
- fsExtra.ensureDirSync(globalBrainDir);
279
-
280
- let toIndex = projects;
281
-
282
- if (action === 'select') {
283
- const selected = await clack.multiselect({
284
- message: 'Select projects to index:',
285
- options: projects.map((p, i) => ({
286
- value: i,
287
- label: `${p.name} (${p.stack})`,
288
- hint: `${p.status} — ${p.path}`,
289
- })),
290
- required: false,
291
- });
292
-
293
- if (clack.isCancel(selected)) {
294
- clack.cancel('Scan cancelled.');
295
- return;
296
- }
297
-
298
- toIndex = selected.map(i => projects[i]);
299
- }
300
-
301
- const indexed = indexProjects(toIndex, globalBrainDir);
302
- clack.log.success(`✅ ${indexed} project(s) indexed in global brain`);
303
- }
304
-
305
- // Generate human-readable index
306
- const indexPath = path.join(os.homedir(), '.bmad-plus', 'brain', 'projects-index.md');
307
- const existingProjects = [];
308
- if (fs.existsSync(globalBrainDir)) {
309
- for (const f of fs.readdirSync(globalBrainDir)) {
310
- if (!f.endsWith('.yaml')) continue;
311
- try {
312
- const content = fs.readFileSync(path.join(globalBrainDir, f), 'utf8');
313
- const meta = {};
314
- for (const line of content.split('\n')) {
315
- const m = line.match(/^(\w+):\s*(.+)$/);
316
- if (m) {
317
- try { meta[m[1]] = JSON.parse(m[2]); } catch { meta[m[1]] = m[2]; }
318
- }
319
- }
320
- existingProjects.push(meta);
321
- } catch {
322
- // Skip a project entry whose index file is unreadable/corrupt.
323
- }
324
- }
325
- }
326
-
327
- const indexContent = [
328
- '---',
329
- 'title: Project Index',
330
- `last_updated: "${new Date().toISOString().slice(0, 10)}"`,
331
- `total_projects: ${existingProjects.length}`,
332
- '---',
333
- '',
334
- '# Project Index',
335
- '',
336
- `> Auto-generated by \`npx bmad-plus scan\` — ${new Date().toISOString().slice(0, 10)}`,
337
- '',
338
- '| Status | Name | Stack | BMAD+ | Path |',
339
- '|--------|------|-------|-------|------|',
340
- ...existingProjects.map(p =>
341
- `| ${p.status || '?'} | ${p.name || '?'} | ${p.stack || '?'} | ${p.bmad_installed ? '✓' : '·'} | \`${p.path || '?'}\` |`
342
- ),
343
- '',
344
- ];
345
-
346
- fs.writeFileSync(indexPath, indexContent.join('\n'), 'utf8');
347
- clack.log.info(`📋 Project index updated: ${indexPath}`);
348
-
349
- clack.outro(pc.green('Scan complete! 🧠'));
350
- },
351
- // Exported for functional testing (not part of the public API)
352
- _internal: {
353
- PROJECT_MARKERS,
354
- SKIP_DIRS,
355
- getProjectStatus,
356
- getStatusIcon,
357
- getProjectName,
358
- hasBmadInstalled,
359
- scanDirectory,
360
- normalizePathForHash,
361
- indexProject,
362
- indexProjects,
363
- },
364
- };
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
+ };