bmad-plus 0.6.0 → 0.7.1

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.
@@ -0,0 +1,335 @@
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
+
16
+ // Project detection markers (priority order)
17
+ const PROJECT_MARKERS = [
18
+ { file: 'package.json', stack: 'Node.js', detect: (dir) => {
19
+ try {
20
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
21
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
22
+ if (deps['next']) return 'Next.js';
23
+ if (deps['nuxt']) return 'Nuxt';
24
+ if (deps['react']) return 'React';
25
+ if (deps['vue']) return 'Vue.js';
26
+ if (deps['svelte']) return 'Svelte';
27
+ if (deps['express']) return 'Express';
28
+ if (deps['fastify']) return 'Fastify';
29
+ if (deps['electron']) return 'Electron';
30
+ if (deps['tauri']) return 'Tauri';
31
+ return 'Node.js';
32
+ } catch { return 'Node.js'; }
33
+ }},
34
+ { file: 'Cargo.toml', stack: 'Rust' },
35
+ { file: 'pyproject.toml', stack: 'Python' },
36
+ { file: 'requirements.txt', stack: 'Python' },
37
+ { file: 'go.mod', stack: 'Go' },
38
+ { file: 'composer.json', stack: 'PHP' },
39
+ { file: 'Gemfile', stack: 'Ruby' },
40
+ { file: 'pom.xml', stack: 'Java' },
41
+ { file: 'build.gradle', stack: 'Java/Kotlin' },
42
+ ];
43
+
44
+ // Directories to skip during scanning
45
+ const SKIP_DIRS = new Set([
46
+ 'node_modules', '.git', 'vendor', '__pycache__', 'dist', 'build',
47
+ '.next', '.nuxt', '.svelte-kit', 'target', '.venv', 'venv',
48
+ '.cache', '.output', 'coverage', '.turbo', '.angular',
49
+ '$RECYCLE.BIN', 'System Volume Information', 'Windows',
50
+ 'Program Files', 'Program Files (x86)', 'ProgramData',
51
+ 'AppData', 'Recovery', 'PerfLogs',
52
+ ]);
53
+
54
+ function getProjectStatus(dir) {
55
+ try {
56
+ const stat = fs.statSync(dir);
57
+ const daysSince = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24);
58
+ if (daysSince < 30) return 'active';
59
+ if (daysSince < 180) return 'paused';
60
+ return 'archived';
61
+ } catch { return 'unknown'; }
62
+ }
63
+
64
+ function getStatusIcon(status) {
65
+ switch (status) {
66
+ case 'active': return pc.green('●');
67
+ case 'paused': return pc.yellow('◐');
68
+ case 'archived': return pc.dim('○');
69
+ default: return pc.dim('?');
70
+ }
71
+ }
72
+
73
+ function getProjectName(dir) {
74
+ try {
75
+ const pkgPath = path.join(dir, 'package.json');
76
+ if (fs.existsSync(pkgPath)) {
77
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
78
+ if (pkg.name) return pkg.name;
79
+ }
80
+ } catch {}
81
+ return path.basename(dir);
82
+ }
83
+
84
+ function hasBmadInstalled(dir) {
85
+ return fs.existsSync(path.join(dir, '.agents')) ||
86
+ fs.existsSync(path.join(dir, '_bmad'));
87
+ }
88
+
89
+ function scanDirectory(rootDir, maxDepth = 4, currentDepth = 0) {
90
+ const projects = [];
91
+
92
+ if (currentDepth > maxDepth) return projects;
93
+
94
+ let entries;
95
+ try {
96
+ entries = fs.readdirSync(rootDir, { withFileTypes: true });
97
+ } catch {
98
+ return projects; // Permission denied or inaccessible
99
+ }
100
+
101
+ // Check if current dir is a project
102
+ for (const marker of PROJECT_MARKERS) {
103
+ if (fs.existsSync(path.join(rootDir, marker.file))) {
104
+ const stack = marker.detect ? marker.detect(rootDir) : marker.stack;
105
+ projects.push({
106
+ path: rootDir,
107
+ name: getProjectName(rootDir),
108
+ stack,
109
+ status: getProjectStatus(rootDir),
110
+ bmad: hasBmadInstalled(rootDir),
111
+ hasAgentsMd: fs.existsSync(path.join(rootDir, 'AGENTS.md')),
112
+ hasGit: fs.existsSync(path.join(rootDir, '.git')),
113
+ });
114
+ return projects; // Don't recurse into project subdirs
115
+ }
116
+ }
117
+
118
+ // Also detect by .git alone (any project with version control)
119
+ if (fs.existsSync(path.join(rootDir, '.git')) && currentDepth > 0) {
120
+ projects.push({
121
+ path: rootDir,
122
+ name: getProjectName(rootDir),
123
+ stack: 'Unknown',
124
+ status: getProjectStatus(rootDir),
125
+ bmad: hasBmadInstalled(rootDir),
126
+ hasAgentsMd: fs.existsSync(path.join(rootDir, 'AGENTS.md')),
127
+ hasGit: true,
128
+ });
129
+ return projects;
130
+ }
131
+
132
+ // Recurse into subdirectories
133
+ for (const entry of entries) {
134
+ if (!entry.isDirectory()) continue;
135
+ if (SKIP_DIRS.has(entry.name)) continue;
136
+ if (entry.name.startsWith('.') && entry.name !== '.git') continue;
137
+
138
+ const subPath = path.join(rootDir, entry.name);
139
+ const subProjects = scanDirectory(subPath, maxDepth, currentDepth + 1);
140
+ projects.push(...subProjects);
141
+ }
142
+
143
+ return projects;
144
+ }
145
+
146
+ module.exports = {
147
+ command: 'scan',
148
+ description: 'Scan directories to discover and index projects in the global brain',
149
+ options: [
150
+ ['-d, --directory <path>', 'Directory to scan (default: current directory)'],
151
+ ['--depth <n>', 'Max depth to scan (default: 4)', '4'],
152
+ ['-y, --yes', 'Index all projects without prompting'],
153
+ ],
154
+ action: async (options) => {
155
+ const scanDir = path.resolve(options.directory || process.cwd());
156
+ const maxDepth = parseInt(options.depth) || 4;
157
+
158
+ clack.intro(pc.bgMagenta(pc.white(' 🧠 BMAD+ Project Scanner ')));
159
+
160
+ // Verify directory exists
161
+ if (!fs.existsSync(scanDir)) {
162
+ clack.log.error(`Directory not found: ${scanDir}`);
163
+ clack.outro(pc.red('Scan failed.'));
164
+ return;
165
+ }
166
+
167
+ // Scan
168
+ const spinner = clack.spinner();
169
+ spinner.start(`Scanning ${scanDir} (depth: ${maxDepth})...`);
170
+
171
+ const projects = scanDirectory(scanDir, maxDepth);
172
+
173
+ if (projects.length === 0) {
174
+ spinner.stop('No projects found.');
175
+ clack.outro('Try scanning a different directory or increasing --depth');
176
+ return;
177
+ }
178
+
179
+ spinner.stop(`Found ${pc.bold(projects.length)} project(s)`);
180
+
181
+ // Display table
182
+ clack.log.info('');
183
+ clack.log.info(pc.bold(' # Status BMAD+ Stack Name Path'));
184
+ clack.log.info(pc.dim(' ' + '─'.repeat(90)));
185
+
186
+ projects.forEach((p, i) => {
187
+ const num = String(i + 1).padStart(3);
188
+ const status = getStatusIcon(p.status) + ' ' + p.status.padEnd(8);
189
+ const bmad = p.bmad ? pc.green('✓') : pc.dim('·');
190
+ const stack = p.stack.padEnd(16);
191
+ const name = p.name.substring(0, 20).padEnd(20);
192
+ const projPath = p.path.length > 40 ? '...' + p.path.slice(-37) : p.path;
193
+ clack.log.info(` ${num} ${status} ${bmad} ${stack} ${name} ${pc.dim(projPath)}`);
194
+ });
195
+
196
+ clack.log.info('');
197
+
198
+ // Interactive validation
199
+ const globalBrainDir = path.join(os.homedir(), '.bmad-plus', 'brain', 'projects');
200
+
201
+ if (options.yes) {
202
+ // Auto-index all
203
+ const fsExtra = require('fs-extra');
204
+ fsExtra.ensureDirSync(globalBrainDir);
205
+
206
+ let indexed = 0;
207
+ for (const proj of projects) {
208
+ const hash = crypto.createHash('sha256').update(proj.path).digest('hex').slice(0, 8);
209
+ const meta = {
210
+ path: proj.path,
211
+ name: proj.name,
212
+ hash,
213
+ stack: proj.stack,
214
+ status: proj.status,
215
+ bmad_installed: proj.bmad,
216
+ has_git: proj.hasGit,
217
+ last_scanned: new Date().toISOString().slice(0, 10),
218
+ };
219
+ fs.writeFileSync(
220
+ path.join(globalBrainDir, `${hash}.yaml`),
221
+ Object.entries(meta).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join('\n'),
222
+ 'utf8'
223
+ );
224
+ indexed++;
225
+ }
226
+ clack.log.success(`✅ ${indexed} project(s) indexed in ${globalBrainDir}`);
227
+ } else {
228
+ // Interactive mode
229
+ const action = await clack.select({
230
+ message: `${projects.length} project(s) found. What to do?`,
231
+ options: [
232
+ { value: 'all', label: `✅ Index all ${projects.length} projects` },
233
+ { value: 'select', label: '✏️ Select which to index' },
234
+ { value: 'none', label: '⏭️ Skip — don\'t index anything' },
235
+ ],
236
+ });
237
+
238
+ if (clack.isCancel(action) || action === 'none') {
239
+ clack.cancel('Scan cancelled.');
240
+ return;
241
+ }
242
+
243
+ const fsExtra = require('fs-extra');
244
+ fsExtra.ensureDirSync(globalBrainDir);
245
+
246
+ let toIndex = projects;
247
+
248
+ if (action === 'select') {
249
+ const selected = await clack.multiselect({
250
+ message: 'Select projects to index:',
251
+ options: projects.map((p, i) => ({
252
+ value: i,
253
+ label: `${p.name} (${p.stack})`,
254
+ hint: `${p.status} — ${p.path}`,
255
+ })),
256
+ required: false,
257
+ });
258
+
259
+ if (clack.isCancel(selected)) {
260
+ clack.cancel('Scan cancelled.');
261
+ return;
262
+ }
263
+
264
+ toIndex = selected.map(i => projects[i]);
265
+ }
266
+
267
+ let indexed = 0;
268
+ for (const proj of toIndex) {
269
+ const hash = crypto.createHash('sha256').update(proj.path).digest('hex').slice(0, 8);
270
+ const meta = {
271
+ path: proj.path,
272
+ name: proj.name,
273
+ hash,
274
+ stack: proj.stack,
275
+ status: proj.status,
276
+ bmad_installed: proj.bmad,
277
+ has_git: proj.hasGit,
278
+ last_scanned: new Date().toISOString().slice(0, 10),
279
+ };
280
+ fs.writeFileSync(
281
+ path.join(globalBrainDir, `${hash}.yaml`),
282
+ Object.entries(meta).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join('\n'),
283
+ 'utf8'
284
+ );
285
+ indexed++;
286
+ }
287
+
288
+ clack.log.success(`✅ ${indexed} project(s) indexed in global brain`);
289
+ }
290
+
291
+ // Generate human-readable index
292
+ const indexPath = path.join(os.homedir(), '.bmad-plus', 'brain', 'projects-index.md');
293
+ const existingProjects = [];
294
+ if (fs.existsSync(globalBrainDir)) {
295
+ for (const f of fs.readdirSync(globalBrainDir)) {
296
+ if (!f.endsWith('.yaml')) continue;
297
+ try {
298
+ const content = fs.readFileSync(path.join(globalBrainDir, f), 'utf8');
299
+ const meta = {};
300
+ for (const line of content.split('\n')) {
301
+ const m = line.match(/^(\w+):\s*(.+)$/);
302
+ if (m) {
303
+ try { meta[m[1]] = JSON.parse(m[2]); } catch { meta[m[1]] = m[2]; }
304
+ }
305
+ }
306
+ existingProjects.push(meta);
307
+ } catch {}
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
+ };
package/tools/cli/i18n.js CHANGED
@@ -10,7 +10,7 @@ const LANGUAGES = {
10
10
  flag: '🇬🇧',
11
11
  name: 'English',
12
12
  locale: 'en',
13
- installer_title: ' BMAD+ Installer v0.6.0 ',
13
+ installer_title: ' BMAD+ Installer v0.7.1 ',
14
14
  select_language: 'Select your language',
15
15
  installing_to: 'Installing to',
16
16
  select_packs: 'Which packs to install? (Core is always included)',
@@ -76,13 +76,23 @@ const LANGUAGES = {
76
76
  guide_example_backup: '🗂️ Backup: "/backup create" → ZIP timestamped',
77
77
  guide_example_animated: '🎬 Animated: "/animated build hero.mp4"',
78
78
  guide_example_osint: '🔍 OSINT: "Shadow, investigate John Doe"',
79
+ guide_memory: '🧠 Persistent Brain',
80
+ guide_dev_studio: '🏗️ Dev Studio',
81
+ guide_example_memory_1: '🧠 Memory: "Zecher, scan projects in D:\\DEV"',
82
+ guide_example_memory_2: '🧠 Memory: "Zecher, where were we?"',
83
+ guide_example_memory_3: '🧠 Memory: "Zecher, consolidate memory"',
84
+ guide_example_dev_studio_1: '🏗️ Dev Studio: "Miriam, brainstorm a productivity app"',
85
+ guide_example_dev_studio_2: '🏗️ Dev Studio: "Bezalel, design the architecture"',
86
+ guide_example_dev_studio_3: '🏗️ Dev Studio: "Oholiab, implement story S1"',
87
+ brain_detected: 'Existing brain detected',
88
+ brain_created: 'Global brain created',
79
89
  },
80
90
 
81
91
  fr: {
82
92
  flag: '🇫🇷',
83
93
  name: 'Français',
84
94
  locale: 'fr',
85
- installer_title: ' BMAD+ Installeur v0.6.0 ',
95
+ installer_title: ' BMAD+ Installeur v0.7.1 ',
86
96
  select_language: 'Choisissez votre langue',
87
97
  installing_to: 'Installation dans',
88
98
  select_packs: 'Quels packs installer ? (Core est toujours inclus)',
@@ -146,13 +156,23 @@ const LANGUAGES = {
146
156
  guide_example_backup: '🗂️ Backup : "/backup create" → ZIP horodaté',
147
157
  guide_example_animated: '🎬 Animé : "/animated build hero.mp4"',
148
158
  guide_example_osint: '🔍 OSINT : "Shadow, investigate John Doe"',
159
+ guide_memory: '🧠 Cerveau persistant',
160
+ guide_dev_studio: '🏗️ Dev Studio',
161
+ guide_example_memory_1: '🧠 Mémoire : "Zecher, scanne les projets dans D:\\DEV"',
162
+ guide_example_memory_2: '🧠 Mémoire : "Zecher, où en étions-nous ?"',
163
+ guide_example_memory_3: '🧠 Mémoire : "Zecher, consolide la mémoire"',
164
+ guide_example_dev_studio_1: '🏗️ Dev Studio : "Miriam, brainstorme une app de productivité"',
165
+ guide_example_dev_studio_2: '🏗️ Dev Studio : "Bezalel, conçois l\'architecture"',
166
+ guide_example_dev_studio_3: '🏗️ Dev Studio : "Oholiab, implémente la story S1"',
167
+ brain_detected: 'Cerveau existant détecté',
168
+ brain_created: 'Cerveau global créé',
149
169
  },
150
170
 
151
171
  es: {
152
172
  flag: '🇪🇸',
153
173
  name: 'Español',
154
174
  locale: 'es',
155
- installer_title: ' BMAD+ Instalador v0.6.0 ',
175
+ installer_title: ' BMAD+ Instalador v0.7.1 ',
156
176
  select_language: 'Seleccione su idioma',
157
177
  installing_to: 'Instalando en',
158
178
  select_packs: '¿Qué packs instalar? (Core siempre está incluido)',
@@ -222,7 +242,7 @@ const LANGUAGES = {
222
242
  flag: '🇩🇪',
223
243
  name: 'Deutsch',
224
244
  locale: 'de',
225
- installer_title: ' BMAD+ Installer v0.6.0 ',
245
+ installer_title: ' BMAD+ Installer v0.7.1 ',
226
246
  select_language: 'Wählen Sie Ihre Sprache',
227
247
  installing_to: 'Installiere in',
228
248
  select_packs: 'Welche Packs installieren? (Core ist immer enthalten)',
@@ -292,7 +312,7 @@ const LANGUAGES = {
292
312
  flag: '🇧🇷',
293
313
  name: 'Português (Brasil)',
294
314
  locale: 'pt-BR',
295
- installer_title: ' BMAD+ Instalador v0.6.0 ',
315
+ installer_title: ' BMAD+ Instalador v0.7.1 ',
296
316
  select_language: 'Selecione seu idioma',
297
317
  installing_to: 'Instalando em',
298
318
  select_packs: 'Quais packs instalar? (Core sempre está incluído)',
@@ -362,7 +382,7 @@ const LANGUAGES = {
362
382
  flag: '🇷🇺',
363
383
  name: 'Русский',
364
384
  locale: 'ru',
365
- installer_title: ' BMAD+ Установщик v0.6.0 ',
385
+ installer_title: ' BMAD+ Установщик v0.7.1 ',
366
386
  select_language: 'Выберите язык',
367
387
  installing_to: 'Установка в',
368
388
  select_packs: 'Какие пакеты установить? (Core всегда включён)',
@@ -432,7 +452,7 @@ const LANGUAGES = {
432
452
  flag: '🇨🇳',
433
453
  name: '中文 (简体)',
434
454
  locale: 'zh-CN',
435
- installer_title: ' BMAD+ 安装程序 v0.6.0 ',
455
+ installer_title: ' BMAD+ 安装程序 v0.7.1 ',
436
456
  select_language: '选择您的语言',
437
457
  installing_to: '安装到',
438
458
  select_packs: '安装哪些包?(Core 始终包含)',
@@ -502,7 +522,7 @@ const LANGUAGES = {
502
522
  flag: '🇮🇱',
503
523
  name: 'עברית',
504
524
  locale: 'he',
505
- installer_title: ' BMAD+ מתקין v0.6.0 ',
525
+ installer_title: ' BMAD+ מתקין v0.7.1 ',
506
526
  select_language: 'בחר את השפה שלך',
507
527
  installing_to: 'מתקין ב',
508
528
  select_packs: 'אילו חבילות להתקין? (Core תמיד כלול)',
@@ -572,7 +592,7 @@ const LANGUAGES = {
572
592
  flag: '🇯🇵',
573
593
  name: '日本語',
574
594
  locale: 'ja',
575
- installer_title: ' BMAD+ インストーラー v0.6.0 ',
595
+ installer_title: ' BMAD+ インストーラー v0.7.1 ',
576
596
  select_language: '言語を選択してください',
577
597
  installing_to: 'インストール先',
578
598
  select_packs: 'どのパックをインストールしますか?(Coreは常に含まれます)',
@@ -642,7 +662,7 @@ const LANGUAGES = {
642
662
  flag: '🇮🇹',
643
663
  name: 'Italiano',
644
664
  locale: 'it',
645
- installer_title: ' BMAD+ Installatore v0.6.0 ',
665
+ installer_title: ' BMAD+ Installatore v0.7.1 ',
646
666
  select_language: 'Seleziona la tua lingua',
647
667
  installing_to: 'Installazione in',
648
668
  select_packs: 'Quali pack installare? (Core è sempre incluso)',