bmad-plus 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +108 -30
  2. package/README.md +23 -23
  3. package/SECURITY.md +9 -1
  4. package/osint-agent-package/README.md +13 -0
  5. package/osint-agent-package/SETUP_KEYS.md +13 -0
  6. package/package.json +32 -8
  7. package/readme-international/README.de.md +133 -542
  8. package/readme-international/README.es.md +135 -561
  9. package/readme-international/README.fr.md +134 -558
  10. package/src/bmad-plus/agents/agent-shadow/SKILL.md +6 -0
  11. package/src/bmad-plus/packs/pack-memory/README.md +1 -1
  12. package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +1 -1
  13. package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +1 -1
  14. package/src/bmad-plus/packs/pack-shield/README.md +2 -0
  15. package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -0
  16. package/src/bmad-plus/packs/pack-shield/shared/audit-report-template.md +6 -0
  17. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +4 -0
  18. package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +61 -1
  19. package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +11 -0
  20. package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +455 -137
  21. package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +230 -20
  22. package/tools/build/adapters.config.js +1 -1
  23. package/tools/build/generate-adapters.js +186 -60
  24. package/tools/build/generate.js +49 -13
  25. package/tools/cli/bmad-plus-cli.js +3 -6
  26. package/tools/cli/commands/autoconfig.js +62 -22
  27. package/tools/cli/commands/install.js +620 -356
  28. package/tools/cli/commands/memory.js +40 -13
  29. package/tools/cli/commands/scan.js +94 -30
  30. package/tools/cli/commands/uat.js +50 -5
  31. package/tools/cli/commands/uninstall.js +65 -16
  32. package/tools/cli/commands/update-check.js +13 -4
  33. package/tools/cli/commands/update-policy.js +14 -5
  34. package/tools/cli/commands/update.js +96 -21
  35. package/tools/cli/i18n.js +120 -68
  36. package/tools/cli/lib/ide-config.js +3 -1
  37. package/tools/cli/lib/install-manifest.js +32 -7
  38. package/tools/cli/lib/installation-health.js +33 -1
  39. package/tools/cli/lib/installed-adapters.js +29 -6
  40. package/tools/cli/lib/memory-init.js +19 -4
  41. package/tools/cli/lib/packs.js +17 -28
  42. package/tools/cli/lib/path-hash.js +5 -1
  43. package/tools/cli/lib/python-provision.js +51 -12
  44. package/tools/cli/lib/stack-detect.js +51 -25
  45. package/tools/cli/lib/uat.js +139 -1
  46. package/tools/cli/lib/update-check.js +132 -34
  47. package/tools/cli/lib/update-dispatch.js +2 -2
  48. package/tools/cli/lib/update-policy.js +57 -19
  49. package/tools/cli/lib/update-transaction.js +439 -85
  50. package/tools/build/README.md +0 -78
  51. package/tools/build/check-counts.js +0 -626
  52. package/tools/build/check-install-contract.js +0 -648
  53. package/tools/build/generated-adapters/.codex/AGENTS.md +0 -134
  54. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +0 -139
  55. package/tools/build/generated-adapters/.opencode/AGENTS.md +0 -134
  56. package/tools/build/generated-adapters/AGENTS.md +0 -132
  57. package/tools/build/generated-adapters/CLAUDE.md +0 -135
  58. package/tools/build/generated-adapters/CONVENTIONS.md +0 -134
  59. package/tools/build/generated-adapters/GEMINI.md +0 -139
  60. package/tools/build/generated-adapters/README.md +0 -79
  61. package/tools/maintain/upstream-candidate.js +0 -456
  62. package/tools/release/publication-content.js +0 -904
  63. package/tools/release/supply-chain.js +0 -282
@@ -1,17 +1,42 @@
1
1
  /** Read the manifest fields used by maintenance commands without unsafe coercion. */
2
2
  const fs = require('node:fs');
3
+ const path = require('node:path');
3
4
 
4
5
  function readInstallManifest(manifestPath) {
5
6
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
6
- if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest) ||
7
- typeof manifest.version !== 'string' || !manifest.version.trim() ||
8
- typeof manifest.installed !== 'string' || !Number.isFinite(Date.parse(manifest.installed)) ||
9
- !Array.isArray(manifest.packs) || manifest.packs.length === 0 ||
10
- manifest.packs.some(pack => typeof pack !== 'string' || !/^[a-z0-9-]+$/.test(pack)) ||
11
- (manifest.uiLanguage !== undefined && typeof manifest.uiLanguage !== 'string')) {
7
+ if (
8
+ !manifest ||
9
+ typeof manifest !== 'object' ||
10
+ Array.isArray(manifest) ||
11
+ typeof manifest.version !== 'string' ||
12
+ !manifest.version.trim() ||
13
+ typeof manifest.installed !== 'string' ||
14
+ !Number.isFinite(Date.parse(manifest.installed)) ||
15
+ !Array.isArray(manifest.packs) ||
16
+ manifest.packs.length === 0 ||
17
+ manifest.packs.some((pack) => typeof pack !== 'string' || !/^[a-z0-9-]+$/.test(pack)) ||
18
+ (manifest.uiLanguage !== undefined && typeof manifest.uiLanguage !== 'string')
19
+ ) {
12
20
  throw new Error('Expected version, installed date, and a nonempty array of pack IDs.');
13
21
  }
14
22
  return manifest;
15
23
  }
16
24
 
17
- module.exports = { readInstallManifest };
25
+ /**
26
+ * Guidance for maintenance commands run outside an installed project. A person who
27
+ * opens a terminal in a drive root or a parent folder must learn where to run them.
28
+ */
29
+ function notInstalledMessage(projectDir) {
30
+ return [
31
+ `No BMAD+ installation found in ${projectDir} (missing _bmad/.bmad-plus-install.json).`,
32
+ 'Run this command from your project folder, for example: cd "path/to/your-project"',
33
+ 'or pass the folder explicitly: --directory "path/to/your-project".',
34
+ 'To install BMAD+ in this folder instead: npx bmad-plus@latest install',
35
+ ].join('\n');
36
+ }
37
+
38
+ function hasInstallManifest(projectDir) {
39
+ return fs.existsSync(path.join(projectDir, '_bmad', '.bmad-plus-install.json'));
40
+ }
41
+
42
+ module.exports = { readInstallManifest, notInstalledMessage, hasInstallManifest };
@@ -4,7 +4,13 @@ const path = require('node:path');
4
4
  const semver = require('semver');
5
5
  const { DERIVED } = require('./packs');
6
6
  const { readInstallManifest } = require('./install-manifest');
7
- const { inventoryOf, collectManagedFiles, MANIFEST } = require('./update-transaction');
7
+ const {
8
+ inventoryOf,
9
+ collectManagedFiles,
10
+ inspectProjectLock,
11
+ MANIFEST,
12
+ LOCK,
13
+ } = require('./update-transaction');
8
14
  const { fileHash } = require('./pack-copy');
9
15
  const { contentHash } = require('./installed-adapters');
10
16
  const {
@@ -86,6 +92,32 @@ function collectInstallationHealth({
86
92
  }
87
93
  report.versions.installed = manifest.version;
88
94
  check('manifest.valid', 'pass', 'Manifest found: v' + manifest.version);
95
+ let lock = null;
96
+ try {
97
+ lock = inspectProjectLock(projectDir);
98
+ } catch (error) {
99
+ check('lock.unreadable', 'warning', 'Project lock cannot be inspected: ' + error.message, {
100
+ path: LOCK,
101
+ });
102
+ }
103
+ if (lock) {
104
+ check(
105
+ lock.stale ? 'lock.stale' : 'lock.active',
106
+ lock.stale ? 'error' : 'warning',
107
+ lock.stale
108
+ ? 'Stale project lock blocks install and update: ' +
109
+ lock.reason +
110
+ ' If no BMAD+ install or update is running, delete ' +
111
+ LOCK +
112
+ ' and retry.'
113
+ : 'An install or update holds the project lock (pid ' +
114
+ (lock.pid ?? 'unknown') +
115
+ ', since ' +
116
+ lock.createdAt +
117
+ ').',
118
+ { path: LOCK }
119
+ );
120
+ }
89
121
  check(
90
122
  'version.cli',
91
123
  manifest.version === cliVersion ? 'pass' : 'warning',
@@ -6,7 +6,8 @@ const clack = require('@clack/prompts');
6
6
  const { GENERATED_MARKER, USER_CONFIG_MARKER } = require('../../build/generate-adapters');
7
7
 
8
8
  const INSTALLER_MARKER = 'BMAD+ \u2014 AI Agent Configuration';
9
- const contentHash = content => crypto.createHash('sha256').update(String(content).replace(/\r\n/g, '\n')).digest('hex');
9
+ const contentHash = (content) =>
10
+ crypto.createHash('sha256').update(String(content).replace(/\r\n/g, '\n')).digest('hex');
10
11
 
11
12
  function classifyAdapter({ existing, content, previousHash, update = false, yes = false }) {
12
13
  if (existing === undefined) return { action: 'write', backup: false };
@@ -16,7 +17,13 @@ function classifyAdapter({ existing, content, previousHash, update = false, yes
16
17
  if (installed && existing === content) return { action: 'same', backup: false };
17
18
  const unchanged = installed && previousHash === contentHash(existing);
18
19
  if (adopted || (installed && !unchanged) || (!installed && !legacy && (update || !yes))) {
19
- return { action: 'preserve', backup: false, adopted, installed, reason: adopted ? 'repository-adopted' : 'local-instructions' };
20
+ return {
21
+ action: 'preserve',
22
+ backup: false,
23
+ adopted,
24
+ installed,
25
+ reason: adopted ? 'repository-adopted' : 'local-instructions',
26
+ };
20
27
  }
21
28
  return { action: 'write', backup: legacy || (!installed && yes), legacy };
22
29
  }
@@ -26,14 +33,20 @@ function safeAdapterPath(projectDir, file) {
26
33
  const root = path.resolve(projectDir);
27
34
  const target = path.resolve(root, file);
28
35
  const relative = path.relative(root, target);
29
- if (!relative || relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative)) {
36
+ if (
37
+ !relative ||
38
+ relative === '..' ||
39
+ relative.startsWith('..' + path.sep) ||
40
+ path.isAbsolute(relative)
41
+ ) {
30
42
  throw new Error('Adapter path outside project: ' + file);
31
43
  }
32
44
  let current = root;
33
45
  for (const part of relative.split(path.sep)) {
34
46
  current = path.join(current, part);
35
47
  try {
36
- if (fs.lstatSync(current).isSymbolicLink()) throw new Error('Adapter path is a symbolic link or junction: ' + current);
48
+ if (fs.lstatSync(current).isSymbolicLink())
49
+ throw new Error('Adapter path is a symbolic link or junction: ' + current);
37
50
  } catch (err) {
38
51
  if (err.code !== 'ENOENT') throw err;
39
52
  }
@@ -73,7 +86,11 @@ function writeIDEConfigs({ projectDir, files, yes = false, update = false, adapt
73
86
  let suffix = '.bak';
74
87
  let index = 1;
75
88
  while (fs.existsSync(safeAdapterPath(projectDir, file + suffix))) suffix = `.bak.${index++}`;
76
- fs.copyFileSync(target, safeAdapterPath(projectDir, file + suffix), fs.constants.COPYFILE_EXCL);
89
+ fs.copyFileSync(
90
+ target,
91
+ safeAdapterPath(projectDir, file + suffix),
92
+ fs.constants.COPYFILE_EXCL
93
+ );
77
94
  outcome.backedUp.push(file);
78
95
  clack.log.warn(`${file}: backed up to ${file + suffix} before refreshing.`);
79
96
  }
@@ -86,4 +103,10 @@ function writeIDEConfigs({ projectDir, files, yes = false, update = false, adapt
86
103
  return outcome;
87
104
  }
88
105
 
89
- module.exports = { INSTALLER_MARKER, contentHash, safeAdapterPath, writeIDEConfigs, classifyAdapter };
106
+ module.exports = {
107
+ INSTALLER_MARKER,
108
+ contentHash,
109
+ safeAdapterPath,
110
+ writeIDEConfigs,
111
+ classifyAdapter,
112
+ };
@@ -45,7 +45,9 @@ function resolveBrain(projectDir, homeDir = os.homedir()) {
45
45
  const envBrain = process.env.BMAD_PLUS_BRAIN;
46
46
  if (envBrain) {
47
47
  if (isBrainCandidateDir(envBrain)) return { dir: envBrain, type: 'portfolio' };
48
- clack.log.warn(`BMAD_PLUS_BRAIN is set but is not a directory: ${envBrain} — falling back to auto-detection`);
48
+ clack.log.warn(
49
+ `BMAD_PLUS_BRAIN is set but is not a directory: ${envBrain} — falling back to auto-detection`
50
+ );
49
51
  }
50
52
 
51
53
  let dir = path.resolve(projectDir);
@@ -129,7 +131,14 @@ function ensureGlobalBrain(brainRoot, templateDir, userName, commLang) {
129
131
  * @param {string} [opts.homeDir] - Home directory (injectable for tests; defaults to os.homedir())
130
132
  * @returns {void}
131
133
  */
132
- function initMemory({ projectDir, bmadSrc, userName, commLang, selectedPacks, homeDir = os.homedir() }) {
134
+ function initMemory({
135
+ projectDir,
136
+ bmadSrc,
137
+ userName,
138
+ commLang,
139
+ selectedPacks,
140
+ homeDir = os.homedir(),
141
+ }) {
133
142
  const memoryDir = path.join(projectDir, '.agents', 'memory');
134
143
  const sessionsDir = path.join(memoryDir, 'sessions');
135
144
  const globalBrainRoot = path.join(homeDir, '.bmad-plus', 'brain');
@@ -170,7 +179,11 @@ function initMemory({ projectDir, bmadSrc, userName, commLang, selectedPacks, ho
170
179
  fs.writeFileSync(
171
180
  path.join(memoryDir, '.brain-link'),
172
181
  JSON.stringify(
173
- { linked_brain: linkedBrain.dir, brain_type: linkedBrain.type, linked_at: new Date().toISOString() },
182
+ {
183
+ linked_brain: linkedBrain.dir,
184
+ brain_type: linkedBrain.type,
185
+ linked_at: new Date().toISOString(),
186
+ },
174
187
  null,
175
188
  2
176
189
  ),
@@ -195,7 +208,9 @@ function initMemory({ projectDir, bmadSrc, userName, commLang, selectedPacks, ho
195
208
  };
196
209
  fs.writeFileSync(
197
210
  path.join(globalBrainRoot, 'projects', `${projHash}.yaml`),
198
- Object.entries(projMeta).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join('\n'),
211
+ Object.entries(projMeta)
212
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
213
+ .join('\n'),
199
214
  'utf8'
200
215
  );
201
216
  }
@@ -25,8 +25,6 @@ const PACKS = {
25
25
  "data": [
26
26
  "role-triggers.yaml"
27
27
  ],
28
- "packDir": "pack-core",
29
- "packSrcDir": "packs",
30
28
  "required": true,
31
29
  "desc": "Core agents & skills"
32
30
  },
@@ -38,8 +36,6 @@ const PACKS = {
38
36
  ],
39
37
  "skills": [],
40
38
  "externalPackage": "osint-agent-package",
41
- "packDir": "pack-osint",
42
- "packSrcDir": "packs",
43
39
  "desc": "OSINT & investigation"
44
40
  },
45
41
  "maker": {
@@ -50,16 +46,12 @@ const PACKS = {
50
46
  ],
51
47
  "skills": [],
52
48
  "data": [],
53
- "packDir": "pack-maker",
54
- "packSrcDir": "packs",
55
49
  "desc": "Agent creation toolkit"
56
50
  },
57
51
  "shield": {
58
52
  "name": "Shield",
59
53
  "icon": "m",
60
- "agents": [
61
- "shield-orchestrator"
62
- ],
54
+ "agents": [],
63
55
  "skills": [],
64
56
  "packDir": "pack-shield",
65
57
  "packSrcDir": "packs",
@@ -68,11 +60,7 @@ const PACKS = {
68
60
  "seo": {
69
61
  "name": "SEO",
70
62
  "icon": "k",
71
- "agents": [
72
- "seo-scout",
73
- "seo-chief",
74
- "seo-judge"
75
- ],
63
+ "agents": [],
76
64
  "skills": [],
77
65
  "packDir": "pack-seo",
78
66
  "packSrcDir": "packs",
@@ -81,9 +69,7 @@ const PACKS = {
81
69
  "memory": {
82
70
  "name": "Memory",
83
71
  "icon": "x",
84
- "agents": [
85
- "zecher"
86
- ],
72
+ "agents": [],
87
73
  "skills": [],
88
74
  "packDir": "pack-memory",
89
75
  "packSrcDir": "packs",
@@ -92,9 +78,7 @@ const PACKS = {
92
78
  "dev-studio": {
93
79
  "name": "Dev Studio",
94
80
  "icon": "v",
95
- "agents": [
96
- "dev-studio-orchestrator"
97
- ],
81
+ "agents": [],
98
82
  "skills": [
99
83
  "dev-studio"
100
84
  ],
@@ -105,9 +89,7 @@ const PACKS = {
105
89
  "backup": {
106
90
  "name": "Backup",
107
91
  "icon": "y",
108
- "agents": [
109
- "backup-agent"
110
- ],
92
+ "agents": [],
111
93
  "skills": [],
112
94
  "packDir": "pack-backup",
113
95
  "packSrcDir": "packs",
@@ -116,9 +98,7 @@ const PACKS = {
116
98
  "animated": {
117
99
  "name": "Animated",
118
100
  "icon": "z",
119
- "agents": [
120
- "animated-website-agent"
121
- ],
101
+ "agents": [],
122
102
  "skills": [],
123
103
  "packDir": "pack-animated",
124
104
  "packSrcDir": "packs",
@@ -211,7 +191,7 @@ const DERIVED = {
211
191
  "product": {
212
192
  "code": "bmad-plus",
213
193
  "displayName": "BMAD+",
214
- "version": "0.17.0",
194
+ "version": "0.18.0",
215
195
  "derivedFrom": "BMAD-METHOD v6.6.0"
216
196
  },
217
197
  "packOrder": [
@@ -246,6 +226,7 @@ const DERIVED = {
246
226
  "order": 0,
247
227
  "name": "Core",
248
228
  "displayName": "Core Development",
229
+ "iconEmoji": "⚙️",
249
230
  "required": true,
250
231
  "installerAgentCount": 4,
251
232
  "categoryAgentCount": 0,
@@ -306,6 +287,7 @@ const DERIVED = {
306
287
  "order": 1,
307
288
  "name": "OSINT",
308
289
  "displayName": "OSINT Intelligence",
290
+ "iconEmoji": "🔍",
309
291
  "required": false,
310
292
  "installerAgentCount": 1,
311
293
  "categoryAgentCount": 0,
@@ -328,7 +310,7 @@ const DERIVED = {
328
310
  "id": "agent-shadow",
329
311
  "name": "Shadow",
330
312
  "role": "OSINT",
331
- "description": "Investigation + Scraping + Psychoprofiling"
313
+ "description": "Investigation + Public-source research (governed, legal basis required)"
332
314
  }
333
315
  ],
334
316
  "summary": "Investigation & scraping (55+ Apify actors, 7 APIs).",
@@ -340,6 +322,7 @@ const DERIVED = {
340
322
  "order": 2,
341
323
  "name": "Maker",
342
324
  "displayName": "Agent Creator",
325
+ "iconEmoji": "🧬",
343
326
  "required": false,
344
327
  "installerAgentCount": 1,
345
328
  "categoryAgentCount": 0,
@@ -374,6 +357,7 @@ const DERIVED = {
374
357
  "order": 3,
375
358
  "name": "Shield",
376
359
  "displayName": "Shield — GRC Compliance",
360
+ "iconEmoji": "🛡️",
377
361
  "required": false,
378
362
  "installerAgentCount": 1,
379
363
  "categoryAgentCount": 27,
@@ -416,6 +400,7 @@ const DERIVED = {
416
400
  "order": 4,
417
401
  "name": "SEO",
418
402
  "displayName": "SEO Audit 360",
403
+ "iconEmoji": "🔍",
419
404
  "required": false,
420
405
  "installerAgentCount": 3,
421
406
  "categoryAgentCount": 0,
@@ -467,6 +452,7 @@ const DERIVED = {
467
452
  "order": 5,
468
453
  "name": "Memory",
469
454
  "displayName": "Memory — Persistent Brain",
455
+ "iconEmoji": "🧠",
470
456
  "required": false,
471
457
  "installerAgentCount": 1,
472
458
  "categoryAgentCount": 0,
@@ -503,6 +489,7 @@ const DERIVED = {
503
489
  "order": 6,
504
490
  "name": "Dev Studio",
505
491
  "displayName": "Dev Studio — Full SDLC",
492
+ "iconEmoji": "🏗️",
506
493
  "required": false,
507
494
  "installerAgentCount": 1,
508
495
  "categoryAgentCount": 0,
@@ -585,6 +572,7 @@ const DERIVED = {
585
572
  "order": 7,
586
573
  "name": "Backup",
587
574
  "displayName": "Universal Backup",
575
+ "iconEmoji": "🗂️",
588
576
  "required": false,
589
577
  "installerAgentCount": 1,
590
578
  "categoryAgentCount": 0,
@@ -619,6 +607,7 @@ const DERIVED = {
619
607
  "order": 8,
620
608
  "name": "Animated",
621
609
  "displayName": "Animated Website",
610
+ "iconEmoji": "🎬",
622
611
  "required": false,
623
612
  "installerAgentCount": 1,
624
613
  "categoryAgentCount": 0,
@@ -33,7 +33,11 @@ function normalizePathForHash(p) {
33
33
  * @returns {string} 8-character hex hash
34
34
  */
35
35
  function projectHash(projectDir) {
36
- return crypto.createHash('sha256').update(normalizePathForHash(projectDir)).digest('hex').slice(0, 8);
36
+ return crypto
37
+ .createHash('sha256')
38
+ .update(normalizePathForHash(projectDir))
39
+ .digest('hex')
40
+ .slice(0, 8);
37
41
  }
38
42
 
39
43
  module.exports = { normalizePathForHash, projectHash };
@@ -156,7 +156,13 @@ function venvPython(envDir, platform = process.platform) {
156
156
  * @returns {{ create: Array<{cmd:string,args:string[],label:string}>,
157
157
  * install: Array<{cmd:string,args:string[],label:string}> }}
158
158
  */
159
- function buildEnvCommands({ tool, python, envDir, requirementsPath = null, platform = process.platform }) {
159
+ function buildEnvCommands({
160
+ tool,
161
+ python,
162
+ envDir,
163
+ requirementsPath = null,
164
+ platform = process.platform,
165
+ }) {
160
166
  const envPy = venvPython(envDir, platform);
161
167
  const create = [];
162
168
  const install = [];
@@ -223,13 +229,21 @@ function missingPythonGuidance(platform = process.platform) {
223
229
  const min = `${MIN_PYTHON.major}.${MIN_PYTHON.minor}`;
224
230
  const lines = [`Python >= ${min} was not found on PATH.`];
225
231
  if (platform === 'win32') {
226
- lines.push(`Install it with: winget install Python.Python.3.12 (or from https://www.python.org/downloads/)`);
232
+ lines.push(
233
+ `Install it with: winget install Python.Python.3.12 (or from https://www.python.org/downloads/)`
234
+ );
227
235
  } else if (platform === 'darwin') {
228
- lines.push(`Install it with: brew install python@3.12 (or from https://www.python.org/downloads/)`);
236
+ lines.push(
237
+ `Install it with: brew install python@3.12 (or from https://www.python.org/downloads/)`
238
+ );
229
239
  } else {
230
- lines.push(`Install it with your package manager (e.g. apt install python3.12) or from https://www.python.org/downloads/`);
240
+ lines.push(
241
+ `Install it with your package manager (e.g. apt install python3.12) or from https://www.python.org/downloads/`
242
+ );
231
243
  }
232
- lines.push(`Alternatively install uv (https://docs.astral.sh/uv/) — BMAD+ will use it to fetch Python ${min} automatically.`);
244
+ lines.push(
245
+ `Alternatively install uv (https://docs.astral.sh/uv/) — BMAD+ will use it to fetch Python ${min} automatically.`
246
+ );
233
247
  return lines;
234
248
  }
235
249
 
@@ -248,7 +262,11 @@ function missingPythonGuidance(platform = process.platform) {
248
262
  * @returns {{ ok: boolean, tool: 'python'|null, command: {cmd:string,baseArgs:string[]}|null,
249
263
  * version: object|null, messages: string[] }}
250
264
  */
251
- function detectPython({ runner = defaultRunner, platform = process.platform, minVersion = MIN_PYTHON } = {}) {
265
+ function detectPython({
266
+ runner = defaultRunner,
267
+ platform = process.platform,
268
+ minVersion = MIN_PYTHON,
269
+ } = {}) {
252
270
  const messages = [];
253
271
  let tooOld = null;
254
272
 
@@ -259,7 +277,9 @@ function detectPython({ runner = defaultRunner, platform = process.platform, min
259
277
  const version = parsePythonVersion(`${res.stdout}\n${res.stderr}`);
260
278
  if (!version) continue;
261
279
  if (meetsMinVersion(version, minVersion)) {
262
- messages.push(`Found ${version.raw} via \`${[candidate.cmd, ...candidate.baseArgs].join(' ')}\`.`);
280
+ messages.push(
281
+ `Found ${version.raw} via \`${[candidate.cmd, ...candidate.baseArgs].join(' ')}\`.`
282
+ );
263
283
  return { ok: true, tool: 'python', command: candidate, version, messages };
264
284
  }
265
285
  tooOld = { candidate, version };
@@ -370,14 +390,22 @@ function verifyImports({ envDir, modules, runner = defaultRunner, platform = pro
370
390
  * Verify that the pack's entry script runs inside the env (default: --help).
371
391
  * @returns {{ ok: boolean, messages: string[] }}
372
392
  */
373
- function verifyEntry({ envDir, entryPath, entryArgs = ['--help'], runner = defaultRunner, platform = process.platform }) {
393
+ function verifyEntry({
394
+ envDir,
395
+ entryPath,
396
+ entryArgs = ['--help'],
397
+ runner = defaultRunner,
398
+ platform = process.platform,
399
+ }) {
374
400
  if (!entryPath) return { ok: true, messages: [] };
375
401
  const envPy = venvPython(envDir, platform);
376
402
  const res = runner(envPy, [entryPath, ...entryArgs]);
377
403
  if (!ranOk(res)) {
378
404
  return {
379
405
  ok: false,
380
- messages: [`Entry check failed (${path.basename(entryPath)} ${entryArgs.join(' ')}): ${firstErrorLine(res)}`],
406
+ messages: [
407
+ `Entry check failed (${path.basename(entryPath)} ${entryArgs.join(' ')}): ${firstErrorLine(res)}`,
408
+ ],
381
409
  };
382
410
  }
383
411
  return { ok: true, messages: [`Verified entry: ${path.basename(entryPath)} runs.`] };
@@ -418,7 +446,13 @@ function provisionPack({
418
446
  const messages = [];
419
447
 
420
448
  if (!envDir) {
421
- return { ok: false, tool: null, envDir: null, python: null, messages: ['provisionPack: envDir is required.'] };
449
+ return {
450
+ ok: false,
451
+ tool: null,
452
+ envDir: null,
453
+ python: null,
454
+ messages: ['provisionPack: envDir is required.'],
455
+ };
422
456
  }
423
457
 
424
458
  // 1) provisioner first — uv can operate without a system Python.
@@ -432,7 +466,9 @@ function provisionPack({
432
466
  return { ok: false, tool: prov.tool, envDir, python: null, messages };
433
467
  }
434
468
  if (!py.ok && prov.tool === 'uv') {
435
- messages.push(`No system Python >= ${minVersion.major}.${minVersion.minor} — uv will download a managed interpreter.`);
469
+ messages.push(
470
+ `No system Python >= ${minVersion.major}.${minVersion.minor} — uv will download a managed interpreter.`
471
+ );
436
472
  }
437
473
 
438
474
  // 3) create isolated env
@@ -482,7 +518,10 @@ function provisionPack({
482
518
  function firstErrorLine(res) {
483
519
  if (!res) return 'no result';
484
520
  if (res.error) return res.error;
485
- const text = `${res.stderr}\n${res.stdout}`.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
521
+ const text = `${res.stderr}\n${res.stdout}`
522
+ .split(/\r?\n/)
523
+ .map((l) => l.trim())
524
+ .filter(Boolean);
486
525
  return text.length > 0 ? text[text.length - 1] : `exit code ${res.status}`;
487
526
  }
488
527
 
@@ -10,24 +10,28 @@ const fs = require('node:fs');
10
10
  // ── Project Detection Markers (for scan.js) ──
11
11
 
12
12
  const PROJECT_MARKERS = [
13
- { file: 'package.json', stack: 'Node.js', detect: (dir) => {
14
- try {
15
- const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
16
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
17
- if (deps['next']) return 'Next.js';
18
- if (deps['nuxt']) return 'Nuxt';
19
- if (deps['react']) return 'React';
20
- if (deps['vue']) return 'Vue.js';
21
- if (deps['svelte']) return 'Svelte';
22
- if (deps['express']) return 'Express';
23
- if (deps['fastify']) return 'Fastify';
24
- if (deps['electron']) return 'Electron';
25
- if (deps['tauri']) return 'Tauri';
26
- return 'Node.js';
27
- } catch {
28
- return 'Node.js';
29
- }
30
- }},
13
+ {
14
+ file: 'package.json',
15
+ stack: 'Node.js',
16
+ detect: (dir) => {
17
+ try {
18
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
19
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
20
+ if (deps['next']) return 'Next.js';
21
+ if (deps['nuxt']) return 'Nuxt';
22
+ if (deps['react']) return 'React';
23
+ if (deps['vue']) return 'Vue.js';
24
+ if (deps['svelte']) return 'Svelte';
25
+ if (deps['express']) return 'Express';
26
+ if (deps['fastify']) return 'Fastify';
27
+ if (deps['electron']) return 'Electron';
28
+ if (deps['tauri']) return 'Tauri';
29
+ return 'Node.js';
30
+ } catch {
31
+ return 'Node.js';
32
+ }
33
+ },
34
+ },
31
35
  { file: 'Cargo.toml', stack: 'Rust' },
32
36
  { file: 'pyproject.toml', stack: 'Python' },
33
37
  { file: 'requirements.txt', stack: 'Python' },
@@ -56,7 +60,10 @@ function detectStack(dir) {
56
60
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
57
61
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
58
62
  result.runtime = 'Node.js';
59
- result.language = deps['typescript'] || fs.existsSync(path.join(dir, 'tsconfig.json')) ? 'TypeScript' : 'JavaScript';
63
+ result.language =
64
+ deps['typescript'] || fs.existsSync(path.join(dir, 'tsconfig.json'))
65
+ ? 'TypeScript'
66
+ : 'JavaScript';
60
67
  result.hasTypeScript = result.language === 'TypeScript';
61
68
 
62
69
  // Framework detection
@@ -85,12 +92,31 @@ function detectStack(dir) {
85
92
 
86
93
  // Other languages
87
94
  if (!result.runtime) {
88
- if (fs.existsSync(path.join(dir, 'Cargo.toml'))) { result.language = 'Rust'; result.runtime = 'Rust'; }
89
- else if (fs.existsSync(path.join(dir, 'pyproject.toml')) || fs.existsSync(path.join(dir, 'requirements.txt'))) { result.language = 'Python'; result.runtime = 'Python'; }
90
- else if (fs.existsSync(path.join(dir, 'go.mod'))) { result.language = 'Go'; result.runtime = 'Go'; }
91
- else if (fs.existsSync(path.join(dir, 'composer.json'))) { result.language = 'PHP'; result.runtime = 'PHP'; }
92
- else if (fs.existsSync(path.join(dir, 'Gemfile'))) { result.language = 'Ruby'; result.runtime = 'Ruby'; }
93
- else if (fs.existsSync(path.join(dir, 'pom.xml')) || fs.existsSync(path.join(dir, 'build.gradle'))) { result.language = 'Java'; result.runtime = 'JVM'; }
95
+ if (fs.existsSync(path.join(dir, 'Cargo.toml'))) {
96
+ result.language = 'Rust';
97
+ result.runtime = 'Rust';
98
+ } else if (
99
+ fs.existsSync(path.join(dir, 'pyproject.toml')) ||
100
+ fs.existsSync(path.join(dir, 'requirements.txt'))
101
+ ) {
102
+ result.language = 'Python';
103
+ result.runtime = 'Python';
104
+ } else if (fs.existsSync(path.join(dir, 'go.mod'))) {
105
+ result.language = 'Go';
106
+ result.runtime = 'Go';
107
+ } else if (fs.existsSync(path.join(dir, 'composer.json'))) {
108
+ result.language = 'PHP';
109
+ result.runtime = 'PHP';
110
+ } else if (fs.existsSync(path.join(dir, 'Gemfile'))) {
111
+ result.language = 'Ruby';
112
+ result.runtime = 'Ruby';
113
+ } else if (
114
+ fs.existsSync(path.join(dir, 'pom.xml')) ||
115
+ fs.existsSync(path.join(dir, 'build.gradle'))
116
+ ) {
117
+ result.language = 'Java';
118
+ result.runtime = 'JVM';
119
+ }
94
120
  }
95
121
 
96
122
  return result;