bmad-plus 0.16.0 → 0.17.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +114 -29
  2. package/README.md +45 -27
  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 +134 -537
  8. package/readme-international/README.es.md +136 -556
  9. package/readme-international/README.fr.md +135 -553
  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 +18 -4
  19. package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +95 -19
  20. package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +90 -10
  21. package/src/bmad-plus/skills/bmad-plus-uat/templates/README.md +63 -0
  22. package/src/bmad-plus/skills/bmad-plus-uat/templates/example-uat-spec.json +95 -0
  23. package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-release-gate.mjs +127 -0
  24. package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-spec-present.mjs +77 -0
  25. package/tools/build/adapters.config.js +1 -1
  26. package/tools/build/generate-adapters.js +186 -60
  27. package/tools/build/generate.js +49 -13
  28. package/tools/cli/bmad-plus-cli.js +3 -6
  29. package/tools/cli/commands/autoconfig.js +66 -24
  30. package/tools/cli/commands/install.js +620 -356
  31. package/tools/cli/commands/memory.js +40 -13
  32. package/tools/cli/commands/scan.js +94 -30
  33. package/tools/cli/commands/uat.js +21 -5
  34. package/tools/cli/commands/uninstall.js +65 -16
  35. package/tools/cli/commands/update-check.js +8 -3
  36. package/tools/cli/commands/update-policy.js +14 -5
  37. package/tools/cli/commands/update.js +89 -20
  38. package/tools/cli/i18n.js +128 -66
  39. package/tools/cli/lib/ide-config.js +3 -1
  40. package/tools/cli/lib/install-manifest.js +13 -6
  41. package/tools/cli/lib/installation-health.js +33 -1
  42. package/tools/cli/lib/installed-adapters.js +29 -6
  43. package/tools/cli/lib/memory-init.js +19 -4
  44. package/tools/cli/lib/packs.js +17 -28
  45. package/tools/cli/lib/path-hash.js +5 -1
  46. package/tools/cli/lib/python-provision.js +51 -12
  47. package/tools/cli/lib/stack-detect.js +51 -25
  48. package/tools/cli/lib/uat.js +18 -0
  49. package/tools/cli/lib/update-check.js +132 -34
  50. package/tools/cli/lib/update-dispatch.js +2 -2
  51. package/tools/cli/lib/update-policy.js +57 -19
  52. package/tools/cli/lib/update-transaction.js +439 -85
  53. package/tools/build/README.md +0 -78
  54. package/tools/build/check-counts.js +0 -626
  55. package/tools/build/check-install-contract.js +0 -648
  56. package/tools/build/generated-adapters/.codex/AGENTS.md +0 -134
  57. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +0 -139
  58. package/tools/build/generated-adapters/.opencode/AGENTS.md +0 -134
  59. package/tools/build/generated-adapters/AGENTS.md +0 -132
  60. package/tools/build/generated-adapters/CLAUDE.md +0 -135
  61. package/tools/build/generated-adapters/CONVENTIONS.md +0 -134
  62. package/tools/build/generated-adapters/GEMINI.md +0 -139
  63. package/tools/build/generated-adapters/README.md +0 -79
  64. package/tools/maintain/upstream-candidate.js +0 -456
  65. package/tools/release/publication-content.js +0 -903
  66. package/tools/release/supply-chain.js +0 -282
@@ -31,8 +31,10 @@ function countEntries(filePath) {
31
31
  function countSessions(dir) {
32
32
  if (!fs.existsSync(dir)) return 0;
33
33
  try {
34
- return fs.readdirSync(dir).filter(f => f.endsWith('.md')).length;
35
- } catch { return 0; }
34
+ return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).length;
35
+ } catch {
36
+ return 0;
37
+ }
36
38
  }
37
39
 
38
40
  module.exports = {
@@ -67,11 +69,21 @@ module.exports = {
67
69
  const sessions = countSessions(path.join(memoryDir, 'sessions'));
68
70
  const brainLink = fs.existsSync(path.join(memoryDir, '.brain-link'));
69
71
 
70
- clack.log.info(` decisions.md ${decisions.exists ? pc.green('✓') : pc.red('✗')} ${decisions.count} entries ${pc.dim(decisions.lastModified || '')}`);
71
- clack.log.info(` lessons.md ${lessons.exists ? pc.green('✓') : pc.red('✗')} ${lessons.count} entries ${pc.dim(lessons.lastModified || '')}`);
72
- clack.log.info(` patterns.md ${patterns.exists ? pc.green('✓') : pc.red('✗')} ${patterns.count} entries ${pc.dim(patterns.lastModified || '')}`);
73
- clack.log.info(` context.md ${context.exists ? pc.green('✓') : pc.red('✗')} ${pc.dim(context.lastModified || '')}`);
74
- clack.log.info(` sessions/ ${sessions > 0 ? pc.green('✓') : pc.dim('·')} ${sessions} handoff(s)`);
72
+ clack.log.info(
73
+ ` decisions.md ${decisions.exists ? pc.green('✓') : pc.red('✗')} ${decisions.count} entries ${pc.dim(decisions.lastModified || '')}`
74
+ );
75
+ clack.log.info(
76
+ ` lessons.md ${lessons.exists ? pc.green('✓') : pc.red('✗')} ${lessons.count} entries ${pc.dim(lessons.lastModified || '')}`
77
+ );
78
+ clack.log.info(
79
+ ` patterns.md ${patterns.exists ? pc.green('✓') : pc.red('✗')} ${patterns.count} entries ${pc.dim(patterns.lastModified || '')}`
80
+ );
81
+ clack.log.info(
82
+ ` context.md ${context.exists ? pc.green('✓') : pc.red('✗')} ${pc.dim(context.lastModified || '')}`
83
+ );
84
+ clack.log.info(
85
+ ` sessions/ ${sessions > 0 ? pc.green('✓') : pc.dim('·')} ${sessions} handoff(s)`
86
+ );
75
87
 
76
88
  if (brainLink) {
77
89
  try {
@@ -100,15 +112,21 @@ module.exports = {
100
112
  const projectsDir = path.join(globalBrainDir, 'projects');
101
113
  let projectCount = 0;
102
114
  if (fs.existsSync(projectsDir)) {
103
- projectCount = fs.readdirSync(projectsDir).filter(f => f.endsWith('.yaml')).length;
115
+ projectCount = fs.readdirSync(projectsDir).filter((f) => f.endsWith('.yaml')).length;
104
116
  }
105
117
 
106
118
  const indexExists = fs.existsSync(path.join(globalBrainDir, 'projects-index.md'));
107
119
 
108
120
  clack.log.info(` identity.yaml ${identity ? pc.green('✓') : pc.yellow('✗ missing')}`);
109
- clack.log.info(` decisions.md ${gDecisions.exists ? pc.green('✓') : pc.dim('·')} ${gDecisions.count} entries`);
110
- clack.log.info(` lessons.md ${gLessons.exists ? pc.green('✓') : pc.dim('·')} ${gLessons.count} entries`);
111
- clack.log.info(` patterns.md ${gPatterns.exists ? pc.green('✓') : pc.dim('·')} ${gPatterns.count} entries`);
121
+ clack.log.info(
122
+ ` decisions.md ${gDecisions.exists ? pc.green('✓') : pc.dim('·')} ${gDecisions.count} entries`
123
+ );
124
+ clack.log.info(
125
+ ` lessons.md ${gLessons.exists ? pc.green('✓') : pc.dim('·')} ${gLessons.count} entries`
126
+ );
127
+ clack.log.info(
128
+ ` patterns.md ${gPatterns.exists ? pc.green('✓') : pc.dim('·')} ${gPatterns.count} entries`
129
+ );
112
130
  clack.log.info(` projects/ ${pc.green('✓')} ${projectCount} project(s) indexed`);
113
131
  clack.log.info(` projects-index ${indexExists ? pc.green('✓') : pc.dim('·')}`);
114
132
  } else {
@@ -160,7 +178,10 @@ module.exports = {
160
178
  return;
161
179
  }
162
180
 
163
- const exportPath = path.join(projectDir, `bmad-brain-export-${new Date().toISOString().slice(0, 10)}.md`);
181
+ const exportPath = path.join(
182
+ projectDir,
183
+ `bmad-brain-export-${new Date().toISOString().slice(0, 10)}.md`
184
+ );
164
185
  const sections = [];
165
186
 
166
187
  sections.push('# BMAD+ Brain Export', `> Exported: ${new Date().toISOString()}`, '');
@@ -178,7 +199,13 @@ module.exports = {
178
199
  sections.push('## Projects', '');
179
200
  for (const f of fs.readdirSync(projectsDir)) {
180
201
  if (f.endsWith('.yaml')) {
181
- sections.push(`### ${f}`, '```yaml', fs.readFileSync(path.join(projectsDir, f), 'utf8').trim(), '```', '');
202
+ sections.push(
203
+ `### ${f}`,
204
+ '```yaml',
205
+ fs.readFileSync(path.join(projectsDir, f), 'utf8').trim(),
206
+ '```',
207
+ ''
208
+ );
182
209
  }
183
210
  }
184
211
  }
@@ -16,12 +16,32 @@ const { normalizePathForHash, projectHash } = require('../lib/path-hash');
16
16
 
17
17
  // Directories to skip during scanning
18
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',
19
+ 'node_modules',
20
+ '.git',
21
+ 'vendor',
22
+ '__pycache__',
23
+ 'dist',
24
+ 'build',
25
+ '.next',
26
+ '.nuxt',
27
+ '.svelte-kit',
28
+ 'target',
29
+ '.venv',
30
+ 'venv',
31
+ '.cache',
32
+ '.output',
33
+ 'coverage',
34
+ '.turbo',
35
+ '.angular',
36
+ '$RECYCLE.BIN',
37
+ 'System Volume Information',
38
+ 'Windows',
39
+ 'Program Files',
40
+ 'Program Files (x86)',
41
+ 'ProgramData',
42
+ 'AppData',
43
+ 'Recovery',
44
+ 'PerfLogs',
25
45
  ]);
26
46
 
27
47
  function getProjectStatus(dir, activeDays = 30, pausedDays = 180) {
@@ -31,15 +51,21 @@ function getProjectStatus(dir, activeDays = 30, pausedDays = 180) {
31
51
  if (daysSince < activeDays) return 'active';
32
52
  if (daysSince < pausedDays) return 'paused';
33
53
  return 'archived';
34
- } catch { return 'unknown'; }
54
+ } catch {
55
+ return 'unknown';
56
+ }
35
57
  }
36
58
 
37
59
  function getStatusIcon(status) {
38
60
  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('?');
61
+ case 'active':
62
+ return pc.green('●');
63
+ case 'paused':
64
+ return pc.yellow('◐');
65
+ case 'archived':
66
+ return pc.dim('○');
67
+ default:
68
+ return pc.dim('?');
43
69
  }
44
70
  }
45
71
 
@@ -57,8 +83,7 @@ function getProjectName(dir) {
57
83
  }
58
84
 
59
85
  function hasBmadInstalled(dir) {
60
- return fs.existsSync(path.join(dir, '.agents')) ||
61
- fs.existsSync(path.join(dir, '_bmad'));
86
+ return fs.existsSync(path.join(dir, '.agents')) || fs.existsSync(path.join(dir, '_bmad'));
62
87
  }
63
88
 
64
89
  function scanDirectory(rootDir, maxDepth = 4, currentDepth = 0, activeDays = 30, pausedDays = 180) {
@@ -138,7 +163,9 @@ function indexProject(project, globalBrainDir) {
138
163
  };
139
164
  fs.writeFileSync(
140
165
  path.join(globalBrainDir, `${hash}.yaml`),
141
- Object.entries(meta).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join('\n'),
166
+ Object.entries(meta)
167
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
168
+ .join('\n'),
142
169
  'utf8'
143
170
  );
144
171
  }
@@ -164,8 +191,16 @@ module.exports = {
164
191
  options: [
165
192
  ['-d, --directory <path>', 'Directory to scan (default: current directory)'],
166
193
  ['--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'],
194
+ [
195
+ '--active-days <n>',
196
+ 'Days since last modified to consider a project "active" (default: 30)',
197
+ '30',
198
+ ],
199
+ [
200
+ '--paused-days <n>',
201
+ 'Days since last modified to consider a project "paused" (default: 180)',
202
+ '180',
203
+ ],
169
204
  ['-y, --yes', 'Index all projects without prompting'],
170
205
  ['-l, --lang <code>', 'Language code: en, fr, es, de, pt-br, ru, zh, he, ja, it'],
171
206
  ],
@@ -173,13 +208,31 @@ module.exports = {
173
208
  const scanDir = path.resolve(options.directory || process.cwd());
174
209
 
175
210
  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; })();
211
+ const maxDepth =
212
+ !isNaN(rawDepth) && rawDepth > 0
213
+ ? rawDepth
214
+ : (() => {
215
+ clack.log.warn(`Invalid --depth "${options.depth}", defaulting to 4`);
216
+ return 4;
217
+ })();
177
218
 
178
219
  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; })();
220
+ const activeDays =
221
+ !isNaN(rawActive) && rawActive > 0
222
+ ? rawActive
223
+ : (() => {
224
+ clack.log.warn(`Invalid --active-days "${options.activeDays}", defaulting to 30`);
225
+ return 30;
226
+ })();
180
227
 
181
228
  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; })();
229
+ const pausedDays =
230
+ !isNaN(rawPaused) && rawPaused > 0
231
+ ? rawPaused
232
+ : (() => {
233
+ clack.log.warn(`Invalid --paused-days "${options.pausedDays}", defaulting to 180`);
234
+ return 180;
235
+ })();
183
236
 
184
237
  clack.intro(pc.bgMagenta(pc.white(' 🧠 BMAD+ Project Scanner ')));
185
238
 
@@ -205,15 +258,21 @@ module.exports = {
205
258
  spinner.stop(`Found ${pc.bold(projects.length)} project(s)`);
206
259
 
207
260
  // 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;
261
+ const activeCount = projects.filter((p) => p.status === 'active').length;
262
+ const pausedCount = projects.filter((p) => p.status === 'paused').length;
263
+ const archivedCount = projects.filter((p) => p.status === 'archived').length;
211
264
 
212
265
  clack.log.info('');
213
266
  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)`)}`);
267
+ clack.log.info(
268
+ ` ${pc.green('●')} active modified < ${activeDays} days ago ${pc.dim(`(${activeCount} found)`)}`
269
+ );
270
+ clack.log.info(
271
+ ` ${pc.yellow('◐')} paused modified ${activeDays}–${pausedDays} days ago ${pc.dim(`(${pausedCount} found)`)}`
272
+ );
273
+ clack.log.info(
274
+ ` ${pc.dim('○')} archived modified > ${pausedDays} days ago ${pc.dim(`(${archivedCount} found)`)}`
275
+ );
217
276
  clack.log.info('');
218
277
 
219
278
  // Display table
@@ -249,7 +308,7 @@ module.exports = {
249
308
  options: [
250
309
  { value: 'all', label: `✅ Index all ${projects.length} projects` },
251
310
  { value: 'select', label: '✏️ Select which to index' },
252
- { value: 'none', label: '⏭️ Skip — don\'t index anything' },
311
+ { value: 'none', label: "⏭️ Skip — don't index anything" },
253
312
  ],
254
313
  });
255
314
 
@@ -279,7 +338,7 @@ module.exports = {
279
338
  return;
280
339
  }
281
340
 
282
- toIndex = selected.map(i => projects[i]);
341
+ toIndex = selected.map((i) => projects[i]);
283
342
  }
284
343
 
285
344
  const indexed = indexProjects(toIndex, globalBrainDir);
@@ -298,7 +357,11 @@ module.exports = {
298
357
  for (const line of content.split('\n')) {
299
358
  const m = line.match(/^(\w+):\s*(.+)$/);
300
359
  if (m) {
301
- try { meta[m[1]] = JSON.parse(m[2]); } catch { meta[m[1]] = m[2]; }
360
+ try {
361
+ meta[m[1]] = JSON.parse(m[2]);
362
+ } catch {
363
+ meta[m[1]] = m[2];
364
+ }
302
365
  }
303
366
  }
304
367
  existingProjects.push(meta);
@@ -321,8 +384,9 @@ module.exports = {
321
384
  '',
322
385
  '| Status | Name | Stack | BMAD+ | Path |',
323
386
  '|--------|------|-------|-------|------|',
324
- ...existingProjects.map(p =>
325
- `| ${p.status || '?'} | ${p.name || '?'} | ${p.stack || '?'} | ${p.bmad_installed ? '✓' : '·'} | \`${p.path || '?'}\` |`
387
+ ...existingProjects.map(
388
+ (p) =>
389
+ `| ${p.status || '?'} | ${p.name || '?'} | ${p.stack || '?'} | ${p.bmad_installed ? '✓' : '·'} | \`${p.path || '?'}\` |`
326
390
  ),
327
391
  '',
328
392
  ];
@@ -17,11 +17,27 @@ function fail(message) {
17
17
 
18
18
  function resolveSpec(target, paths) {
19
19
  if (!target) fail('an id or a spec file is required');
20
- const file = target.endsWith('.json')
21
- ? path.resolve(target)
22
- : path.join(paths.specs, `${target}.json`);
23
- if (!fs.existsSync(file)) fail(`no spec at ${file}`);
24
- return uat.loadSpec(file);
20
+ if (target.endsWith('.json')) {
21
+ const file = path.resolve(target);
22
+ if (!fs.existsSync(file)) fail(`no spec at ${file}`);
23
+ return uat.loadSpec(file);
24
+ }
25
+ const direct = path.join(paths.specs, `${target}.json`);
26
+ if (fs.existsSync(direct)) return uat.loadSpec(direct);
27
+ // A recipe is named by its id, not by its file: a copied template keeps its own name
28
+ // (the shipped example-uat-spec.json holds the id example-1.0.0).
29
+ if (fs.existsSync(paths.specs)) {
30
+ for (const name of fs.readdirSync(paths.specs).filter((entry) => entry.endsWith('.json'))) {
31
+ try {
32
+ const loaded = uat.loadSpec(path.join(paths.specs, name));
33
+ if (loaded.spec.id === target) return loaded;
34
+ } catch {
35
+ // an unreadable file cannot be the recipe asked for
36
+ }
37
+ }
38
+ }
39
+ fail(`no spec with the id "${target}" in ${paths.specs}`);
40
+ return null;
25
41
  }
26
42
 
27
43
  function readStdin() {
@@ -17,7 +17,12 @@ const { contentHash } = require('../lib/installed-adapters');
17
17
  // Never follow a junction/symlink in a path used for deletion, including parents.
18
18
  function safeStat(projectDir, target) {
19
19
  const relative = path.relative(projectDir, target);
20
- if (!relative || relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative)) {
20
+ if (
21
+ !relative ||
22
+ relative === '..' ||
23
+ relative.startsWith('..' + path.sep) ||
24
+ path.isAbsolute(relative)
25
+ ) {
21
26
  throw new Error('Path outside the project: ' + target);
22
27
  }
23
28
  let current = projectDir;
@@ -54,7 +59,8 @@ function removalPlan(projectDir, manifest, manifestPath) {
54
59
  const sourceStat = fs.lstatSync(source);
55
60
  if (sourceStat.isDirectory() && targetStat.isDirectory()) {
56
61
  directories.add(target);
57
- for (const name of fs.readdirSync(source)) matchSource(path.join(source, name), path.join(target, name));
62
+ for (const name of fs.readdirSync(source))
63
+ matchSource(path.join(source, name), path.join(target, name));
58
64
  } else if (sourceStat.isFile() && targetStat.isFile()) {
59
65
  const expected = fs.readFileSync(source);
60
66
  if (fs.readFileSync(target).equals(expected)) files.set(target, expected);
@@ -69,26 +75,46 @@ function removalPlan(projectDir, manifest, manifestPath) {
69
75
  for (const packId of manifest.packs) {
70
76
  if (!Object.hasOwn(PACKS, packId)) throw new Error('Unknown installed pack: ' + packId);
71
77
  const pack = PACKS[packId];
72
- for (const name of pack.agents || []) matchSource(path.join(sourceRoot, 'agents', name), targetPath('.agents', 'skills', name));
73
- for (const name of pack.skills || []) matchSource(path.join(sourceRoot, 'skills', name), targetPath('.agents', 'skills', name));
74
- for (const name of pack.data || []) matchSource(path.join(sourceRoot, 'data', name), targetPath('.agents', 'data', name));
75
- if (pack.packDir) matchSource(path.join(sourceRoot, pack.packSrcDir || 'agents', pack.packDir), targetPath('.agents', 'skills', pack.packDir));
76
- if (pack.externalPackage) matchSource(path.join(packageRoot, pack.externalPackage, 'skills'), targetPath('.agents', 'skills'));
78
+ for (const name of pack.agents || [])
79
+ matchSource(path.join(sourceRoot, 'agents', name), targetPath('.agents', 'skills', name));
80
+ for (const name of pack.skills || [])
81
+ matchSource(path.join(sourceRoot, 'skills', name), targetPath('.agents', 'skills', name));
82
+ for (const name of pack.data || [])
83
+ matchSource(path.join(sourceRoot, 'data', name), targetPath('.agents', 'data', name));
84
+ if (pack.packDir)
85
+ matchSource(
86
+ path.join(sourceRoot, pack.packSrcDir || 'agents', pack.packDir),
87
+ targetPath('.agents', 'skills', pack.packDir)
88
+ );
89
+ if (pack.externalPackage)
90
+ matchSource(
91
+ path.join(packageRoot, pack.externalPackage, 'skills'),
92
+ targetPath('.agents', 'skills')
93
+ );
77
94
  }
78
- for (const name of ['module.yaml', 'module-help.csv']) matchSource(path.join(sourceRoot, name), targetPath('_bmad', name));
95
+ for (const name of ['module.yaml', 'module-help.csv'])
96
+ matchSource(path.join(sourceRoot, name), targetPath('_bmad', name));
79
97
  const renderOptions = {
80
98
  packs: manifest.packs,
81
99
  userName: typeof manifest.user === 'string' ? manifest.user : 'Developer',
82
100
  language: typeof manifest.language === 'string' ? manifest.language : 'English',
83
101
  };
84
- const expectedFiles = new Map(generateUserFiles(DERIVED, renderOptions).map(entry => [entry.file, entry.content]));
85
- const hashes = manifest.adapterHashes && typeof manifest.adapterHashes === 'object' && !Array.isArray(manifest.adapterHashes)
86
- ? manifest.adapterHashes : {};
102
+ const expectedFiles = new Map(
103
+ generateUserFiles(DERIVED, renderOptions).map((entry) => [entry.file, entry.content])
104
+ );
105
+ const hashes =
106
+ manifest.adapterHashes &&
107
+ typeof manifest.adapterHashes === 'object' &&
108
+ !Array.isArray(manifest.adapterHashes)
109
+ ? manifest.adapterHashes
110
+ : {};
87
111
  // Only registry-declared paths can become deletion candidates, even when the
88
112
  // manifest contains arbitrary hash keys. Keep previously owned tool targets.
89
- const claimed = new Set(Object.keys(hashes).filter(file => expectedFiles.has(file)));
113
+ const claimed = new Set(Object.keys(hashes).filter((file) => expectedFiles.has(file)));
90
114
  if (Array.isArray(manifest.ides)) {
91
- const tools = manifest.ides.filter(id => DERIVED.targets.adapters.some(adapter => adapter.tool === id));
115
+ const tools = manifest.ides.filter((id) =>
116
+ DERIVED.targets.adapters.some((adapter) => adapter.tool === id)
117
+ );
92
118
  if (tools.length) {
93
119
  for (const entry of generateUserFiles(DERIVED, { ...renderOptions, tools })) {
94
120
  claimed.add(entry.file);
@@ -132,13 +158,27 @@ module.exports = {
132
158
  command: 'uninstall',
133
159
  description: 'Remove BMAD+ from your project',
134
160
  options: [
161
+ ['-d, --directory <path>', 'Project directory (default: current directory)'],
135
162
  ['-y, --yes', 'Confirm removal without prompting'],
136
163
  ['-l, --lang <code>', 'Language code: en, fr, es, de, pt-br, ru, zh, he, ja, it'],
137
164
  ],
138
165
  action: async (options) => {
139
- const projectDir = fs.realpathSync(process.cwd());
140
166
  let i = t(options.lang || 'en');
141
167
  clack.intro(pc.bgRed(pc.white(' BMAD+ Uninstaller ')));
168
+ let projectDir;
169
+ try {
170
+ projectDir = fs.realpathSync(path.resolve(options.directory || process.cwd()));
171
+ } catch (err) {
172
+ clack.log.error(
173
+ 'Project directory not found: ' +
174
+ (options.directory || process.cwd()) +
175
+ ' (' +
176
+ err.code +
177
+ ')'
178
+ );
179
+ process.exitCode = 1;
180
+ return;
181
+ }
142
182
 
143
183
  const manifestPath = path.join(projectDir, '_bmad', '.bmad-plus-install.json');
144
184
  let manifest;
@@ -169,7 +209,15 @@ module.exports = {
169
209
  process.exitCode = 1;
170
210
  return;
171
211
  }
172
- clack.log.info('BMAD+ v' + manifest.version + ' (' + i.installed_on + ' ' + manifest.installed.split('T')[0] + ')');
212
+ clack.log.info(
213
+ 'BMAD+ v' +
214
+ manifest.version +
215
+ ' (' +
216
+ i.installed_on +
217
+ ' ' +
218
+ manifest.installed.split('T')[0] +
219
+ ')'
220
+ );
173
221
  clack.log.info(i.selected_packs + ': ' + manifest.packs.join(', '));
174
222
  if (!options.yes) {
175
223
  const confirm = await clack.confirm({ message: i.uninstall_confirm });
@@ -184,7 +232,8 @@ module.exports = {
184
232
  let removed = 0;
185
233
  try {
186
234
  // Revalidate after the prompt, before deleting any file.
187
- for (const target of [...plan.files.keys(), ...plan.directories]) safeStat(projectDir, target);
235
+ for (const target of [...plan.files.keys(), ...plan.directories])
236
+ safeStat(projectDir, target);
188
237
  for (const [target, expected] of plan.files) {
189
238
  if (safeStat(projectDir, target)?.isFile() && fs.readFileSync(target).equals(expected)) {
190
239
  fs.unlinkSync(target);
@@ -15,7 +15,8 @@ module.exports = {
15
15
  const result = await checkForUpdate({
16
16
  projectDir: path.resolve(options.directory || process.cwd()),
17
17
  runningVersion: require('../../../package.json').version,
18
- refresh: Boolean(options.refresh), offline: Boolean(options.offline),
18
+ refresh: Boolean(options.refresh),
19
+ offline: Boolean(options.offline),
19
20
  checkReadiness: ({ projectDir }) => {
20
21
  const { evaluateUpdateReadiness } = require('../lib/update-transaction');
21
22
  return evaluateUpdateReadiness({ projectDir });
@@ -24,8 +25,12 @@ module.exports = {
24
25
  if (result.status.startsWith('invalid-')) process.exitCode = 1;
25
26
  if (options.json) console.log(JSON.stringify(result));
26
27
  else {
27
- console.log(`Installed: ${result.installedVersion || 'unknown'}; running: ${result.runningVersion}; channel ${result.channel || 'unknown'}: ${result.targetVersion || 'unknown'}`);
28
- console.log(`${result.status}: ${result.reason}${result.stale ? ' (cached result is stale)' : ''}`);
28
+ console.log(
29
+ `Installed: ${result.installedVersion || 'unknown'}; running: ${result.runningVersion}; channel ${result.channel || 'unknown'}: ${result.targetVersion || 'unknown'}`
30
+ );
31
+ console.log(
32
+ `${result.status}: ${result.reason}${result.stale ? ' (cached result is stale)' : ''}`
33
+ );
29
34
  }
30
35
  },
31
36
  };
@@ -16,12 +16,18 @@ module.exports = {
16
16
  ],
17
17
  action: async (options = {}) => {
18
18
  try {
19
- if (options.allowPrerelease && options.stableOnly) throw new Error('Choose --allow-prerelease or --stable-only.');
20
- if (options.mode === 'auto' && !options.range) throw new Error('--mode auto requires an explicit --range.');
19
+ if (options.allowPrerelease && options.stableOnly)
20
+ throw new Error('Choose --allow-prerelease or --stable-only.');
21
+ if (options.mode === 'auto' && !options.range)
22
+ throw new Error('--mode auto requires an explicit --range.');
21
23
  const projectDir = path.resolve(options.directory || process.cwd());
22
24
  const policy = readUpdatePolicy(projectDir);
23
- const changing = options.mode !== undefined || options.range !== undefined ||
24
- options.channel !== undefined || options.allowPrerelease || options.stableOnly;
25
+ const changing =
26
+ options.mode !== undefined ||
27
+ options.range !== undefined ||
28
+ options.channel !== undefined ||
29
+ options.allowPrerelease ||
30
+ options.stableOnly;
25
31
  if (options.mode !== undefined) policy.mode = options.mode;
26
32
  if (options.range !== undefined) policy.allowedRange = options.range;
27
33
  if (options.channel !== undefined) policy.channel = options.channel;
@@ -29,7 +35,10 @@ module.exports = {
29
35
  if (options.stableOnly) policy.allowPrerelease = false;
30
36
  const saved = changing ? writeUpdatePolicy(projectDir, policy) : policy;
31
37
  if (options.json) console.log(JSON.stringify({ policy: saved, changed: Boolean(changing) }));
32
- else console.log(`Update policy${changing ? ' saved' : ''}: ${saved.mode}; channel=${saved.channel}; range=${saved.allowedRange || 'none'}; prereleases=${saved.allowPrerelease}`);
38
+ else
39
+ console.log(
40
+ `Update policy${changing ? ' saved' : ''}: ${saved.mode}; channel=${saved.channel}; range=${saved.allowedRange || 'none'}; prereleases=${saved.allowPrerelease}`
41
+ );
33
42
  } catch (error) {
34
43
  process.exitCode = 1;
35
44
  if (options.json) console.log(JSON.stringify({ error: error.message }));