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.
- package/CHANGELOG.md +612 -580
- package/README.md +90 -115
- package/osint-agent-package/agents/osint-investigator.md +12 -0
- package/osint-agent-package/skills/bmad-osint-investigate/osint/SKILL.md +491 -482
- package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/dossier-template.md +126 -126
- package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/lawful-basis-record.md +48 -48
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/content-extraction.md +100 -100
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/gdpr-osint.md +48 -48
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/platforms.md +130 -130
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/psychoprofile.md +69 -69
- package/osint-agent-package/skills/bmad-osint-investigate/osint/references/tools.md +281 -281
- package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/mcp-client.py +136 -136
- package/package.json +104 -91
- package/readme-international/README.de.md +596 -594
- package/readme-international/README.es.md +613 -611
- package/readme-international/README.fr.md +611 -609
- package/src/bmad-plus/agents/agent-shadow/SKILL.md +18 -0
- package/src/bmad-plus/data/role-triggers.yaml +52 -0
- package/src/bmad-plus/module.yaml +283 -283
- package/src/bmad-plus/packs/pack-animated/animated-website-agent.md +325 -325
- package/src/bmad-plus/packs/pack-animated/templates/animated-website-workflow.md +55 -55
- package/src/bmad-plus/packs/pack-backup/backup-agent.md +71 -71
- package/src/bmad-plus/packs/pack-backup/templates/backup-workflow.md +51 -51
- package/src/bmad-plus/packs/pack-dev-studio/README.md +162 -162
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +61 -61
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +95 -95
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +95 -95
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +134 -134
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +80 -80
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +95 -95
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +92 -92
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +80 -80
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +90 -90
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +153 -153
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +173 -173
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +224 -224
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +329 -329
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +318 -318
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +359 -359
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +379 -379
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +361 -361
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +81 -81
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +67 -67
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +85 -85
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +35 -35
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +49 -49
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +131 -131
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +89 -89
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +300 -300
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +428 -428
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +80 -80
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +484 -484
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +193 -193
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +175 -175
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +110 -110
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +1511 -1511
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +298 -298
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +296 -296
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +29 -29
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +74 -74
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +29 -29
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +89 -89
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +73 -73
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +29 -29
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +141 -141
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +37 -37
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +75 -75
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +6 -6
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +110 -110
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +176 -176
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +67 -67
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +86 -86
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +179 -179
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +66 -66
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +127 -127
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +105 -105
- package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +120 -120
- package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +12 -12
- package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +328 -328
- package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +32 -32
- package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +81 -81
- package/src/bmad-plus/packs/pack-seo/SKILL.md +171 -171
- package/src/bmad-plus/packs/pack-seo/checklist.md +140 -140
- package/src/bmad-plus/packs/pack-seo/pagespeed-playbook.md +320 -320
- package/src/bmad-plus/packs/pack-seo/ref/audit-schema.json +187 -187
- package/src/bmad-plus/packs/pack-seo/ref/cwv-thresholds.md +87 -87
- package/src/bmad-plus/packs/pack-seo/ref/eeat-criteria.md +123 -123
- package/src/bmad-plus/packs/pack-seo/ref/geo-signals.md +167 -167
- package/src/bmad-plus/packs/pack-seo/ref/hreflang-rules.md +153 -153
- package/src/bmad-plus/packs/pack-seo/ref/quality-gates.md +133 -133
- package/src/bmad-plus/packs/pack-seo/ref/schema-catalog.md +91 -91
- package/src/bmad-plus/packs/pack-seo/ref/schema-templates.json +356 -356
- package/src/bmad-plus/packs/pack-seo/requirements.txt +17 -0
- package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +456 -0
- package/src/bmad-plus/packs/pack-seo/scripts/seo_crawl.py +359 -0
- package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +304 -0
- package/src/bmad-plus/packs/pack-seo/scripts/seo_parse.py +255 -0
- package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +410 -0
- package/src/bmad-plus/packs/pack-seo/scripts/seo_screenshot.py +202 -0
- package/src/bmad-plus/packs/pack-seo/seo-chief.md +294 -294
- package/src/bmad-plus/packs/pack-seo/seo-judge.md +241 -241
- package/src/bmad-plus/packs/pack-seo/seo-scout.md +171 -171
- package/src/bmad-plus/packs/pack-seo/templates/seo-audit-workflow.md +241 -241
- package/src/bmad-plus/packs/pack-shield/README.md +6 -6
- package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -2
- package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/csrd-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/section508-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/wcag-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/ai-governance/eu-ai-act-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/ai-governance/iso42001-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/ai-governance/nist-ai-rmf-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/cis-controls-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/ism-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/iso27001-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nis2-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-800-53-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-csf-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/defense-export/cmmc-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/defense-export/ear-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/defense-export/itar-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/defense-export/tsa-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/dora-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/fedramp-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/hipaa-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/pci-dss-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/soc2-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/swift-csp-agent.md +11 -11
- package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +1 -1
- package/tools/build/check-counts.js +628 -0
- package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
- package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
- package/tools/build/generated-adapters/AGENTS.md +1 -1
- package/tools/build/generated-adapters/CLAUDE.md +1 -1
- package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
- package/tools/build/generated-adapters/GEMINI.md +1 -1
- package/tools/cli/commands/autoconfig.js +470 -470
- package/tools/cli/commands/doctor.js +233 -233
- package/tools/cli/commands/install.js +598 -501
- package/tools/cli/commands/memory-journal-cmd.js +311 -311
- package/tools/cli/commands/memory.js +195 -195
- package/tools/cli/commands/scan.js +348 -348
- package/tools/cli/commands/uninstall.js +101 -101
- package/tools/cli/commands/update.js +134 -134
- package/tools/cli/i18n.js +845 -845
- package/tools/cli/lib/README-memory-journal.md +125 -125
- package/tools/cli/lib/ide-config.js +267 -259
- package/tools/cli/lib/python-provision.js +508 -508
- package/tools/cli/lib/stack-detect.js +102 -102
- 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
|
+
};
|