bmad-plus 0.12.1 → 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 (156) hide show
  1. package/CHANGELOG.md +612 -580
  2. package/README.md +90 -115
  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 -594
  15. package/readme-international/README.es.md +613 -611
  16. package/readme-international/README.fr.md +611 -609
  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-seo/SKILL.md +171 -171
  89. package/src/bmad-plus/packs/pack-seo/checklist.md +140 -140
  90. package/src/bmad-plus/packs/pack-seo/pagespeed-playbook.md +320 -320
  91. package/src/bmad-plus/packs/pack-seo/ref/audit-schema.json +187 -187
  92. package/src/bmad-plus/packs/pack-seo/ref/cwv-thresholds.md +87 -87
  93. package/src/bmad-plus/packs/pack-seo/ref/eeat-criteria.md +123 -123
  94. package/src/bmad-plus/packs/pack-seo/ref/geo-signals.md +167 -167
  95. package/src/bmad-plus/packs/pack-seo/ref/hreflang-rules.md +153 -153
  96. package/src/bmad-plus/packs/pack-seo/ref/quality-gates.md +133 -133
  97. package/src/bmad-plus/packs/pack-seo/ref/schema-catalog.md +91 -91
  98. package/src/bmad-plus/packs/pack-seo/ref/schema-templates.json +356 -356
  99. package/src/bmad-plus/packs/pack-seo/requirements.txt +17 -0
  100. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +456 -0
  101. package/src/bmad-plus/packs/pack-seo/scripts/seo_crawl.py +359 -0
  102. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +304 -0
  103. package/src/bmad-plus/packs/pack-seo/scripts/seo_parse.py +255 -0
  104. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +410 -0
  105. package/src/bmad-plus/packs/pack-seo/scripts/seo_screenshot.py +202 -0
  106. package/src/bmad-plus/packs/pack-seo/seo-chief.md +294 -294
  107. package/src/bmad-plus/packs/pack-seo/seo-judge.md +241 -241
  108. package/src/bmad-plus/packs/pack-seo/seo-scout.md +171 -171
  109. package/src/bmad-plus/packs/pack-seo/templates/seo-audit-workflow.md +241 -241
  110. package/src/bmad-plus/packs/pack-shield/README.md +6 -6
  111. package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -2
  112. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/csrd-agent.md +11 -11
  113. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/section508-agent.md +11 -11
  114. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/wcag-agent.md +11 -11
  115. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/eu-ai-act-agent.md +11 -11
  116. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/iso42001-agent.md +11 -11
  117. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/nist-ai-rmf-agent.md +11 -11
  118. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/cis-controls-agent.md +11 -11
  119. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/ism-agent.md +11 -11
  120. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/iso27001-agent.md +11 -11
  121. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nis2-agent.md +11 -11
  122. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-800-53-agent.md +11 -11
  123. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-csf-agent.md +11 -11
  124. package/src/bmad-plus/packs/pack-shield/categories/defense-export/cmmc-agent.md +11 -11
  125. package/src/bmad-plus/packs/pack-shield/categories/defense-export/ear-agent.md +11 -11
  126. package/src/bmad-plus/packs/pack-shield/categories/defense-export/itar-agent.md +11 -11
  127. package/src/bmad-plus/packs/pack-shield/categories/defense-export/tsa-agent.md +11 -11
  128. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/dora-agent.md +11 -11
  129. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/fedramp-agent.md +11 -11
  130. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/hipaa-agent.md +11 -11
  131. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/pci-dss-agent.md +11 -11
  132. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/soc2-agent.md +11 -11
  133. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/swift-csp-agent.md +11 -11
  134. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +1 -1
  135. package/tools/build/check-counts.js +628 -0
  136. package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
  137. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
  138. package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
  139. package/tools/build/generated-adapters/AGENTS.md +1 -1
  140. package/tools/build/generated-adapters/CLAUDE.md +1 -1
  141. package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
  142. package/tools/build/generated-adapters/GEMINI.md +1 -1
  143. package/tools/cli/commands/autoconfig.js +470 -470
  144. package/tools/cli/commands/doctor.js +233 -233
  145. package/tools/cli/commands/install.js +598 -501
  146. package/tools/cli/commands/memory-journal-cmd.js +311 -311
  147. package/tools/cli/commands/memory.js +195 -195
  148. package/tools/cli/commands/scan.js +348 -348
  149. package/tools/cli/commands/uninstall.js +101 -101
  150. package/tools/cli/commands/update.js +134 -134
  151. package/tools/cli/i18n.js +845 -845
  152. package/tools/cli/lib/README-memory-journal.md +125 -125
  153. package/tools/cli/lib/ide-config.js +267 -259
  154. package/tools/cli/lib/python-provision.js +508 -508
  155. package/tools/cli/lib/stack-detect.js +102 -102
  156. 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
+ };