bmad-plus 0.12.2 → 0.14.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 (127) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +96 -461
  3. package/package.json +12 -5
  4. package/readme-international/README.de.md +42 -9
  5. package/readme-international/README.es.md +43 -10
  6. package/readme-international/README.fr.md +42 -9
  7. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +5 -3
  8. package/src/bmad-plus/module.yaml +270 -220
  9. package/src/bmad-plus/packs/pack-dev-studio/README.md +133 -141
  10. package/src/bmad-plus/packs/pack-dev-studio/SKILL.md +49 -0
  11. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +35 -60
  12. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +59 -59
  13. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +55 -93
  14. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +58 -93
  15. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +55 -132
  16. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +63 -78
  17. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +54 -69
  18. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +54 -93
  19. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +32 -60
  20. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +67 -71
  21. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +61 -90
  22. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +56 -78
  23. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +55 -88
  24. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +20 -153
  25. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +20 -173
  26. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +14 -220
  27. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +20 -329
  28. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +15 -314
  29. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +15 -355
  30. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +15 -375
  31. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +14 -357
  32. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +13 -78
  33. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +52 -65
  34. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +14 -81
  35. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +14 -31
  36. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +14 -45
  37. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +13 -128
  38. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +61 -87
  39. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +55 -298
  40. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +52 -426
  41. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +48 -69
  42. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +22 -80
  43. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +60 -482
  44. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +50 -184
  45. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +57 -173
  46. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +56 -108
  47. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +54 -1509
  48. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +54 -296
  49. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +41 -283
  50. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +58 -18
  51. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +103 -72
  52. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +55 -27
  53. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +34 -60
  54. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +46 -87
  55. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/steps/step-01-init.md +10 -0
  56. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +30 -60
  57. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +57 -27
  58. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +47 -138
  59. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +48 -34
  60. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +51 -68
  61. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +46 -3
  62. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +68 -109
  63. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +53 -174
  64. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +39 -53
  65. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +45 -83
  66. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +45 -176
  67. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +45 -63
  68. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +53 -124
  69. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +44 -100
  70. package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +56 -116
  71. package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +49 -12
  72. package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +51 -328
  73. package/src/bmad-plus/packs/pack-dev-studio/shared/catalog.json +489 -0
  74. package/src/bmad-plus/packs/pack-dev-studio/shared/execution.md +69 -0
  75. package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +39 -32
  76. package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +85 -14
  77. package/src/bmad-plus/packs/pack-memory/README.md +6 -0
  78. package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +12 -5
  79. package/src/bmad-plus/packs/pack-seo/SKILL.md +27 -1
  80. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +8 -8
  81. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +1 -2
  82. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +0 -1
  83. package/src/bmad-plus/packs/pack-seo/seo-chief.md +16 -1
  84. package/src/bmad-plus/packs/pack-seo/seo-judge.md +12 -0
  85. package/src/bmad-plus/packs/pack-seo/seo-scout.md +12 -0
  86. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +4 -4
  87. package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +5 -3
  88. package/src/bmad-plus/skills/dev-studio/SKILL.md +19 -0
  89. package/tools/bmad-plus-npx.js +4 -2
  90. package/tools/build/adapters.config.js +60 -51
  91. package/tools/build/check-counts.js +52 -54
  92. package/tools/build/check-install-contract.js +450 -0
  93. package/tools/build/generate-adapters.js +252 -56
  94. package/tools/build/generate.js +364 -14
  95. package/tools/build/generated-adapters/.codex/AGENTS.md +20 -7
  96. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +20 -7
  97. package/tools/build/generated-adapters/.opencode/AGENTS.md +20 -7
  98. package/tools/build/generated-adapters/AGENTS.md +20 -7
  99. package/tools/build/generated-adapters/CLAUDE.md +20 -7
  100. package/tools/build/generated-adapters/CONVENTIONS.md +20 -7
  101. package/tools/build/generated-adapters/GEMINI.md +20 -7
  102. package/tools/build/module.template.yaml +82 -0
  103. package/tools/build/pack-delivery.js +78 -0
  104. package/tools/cli/bmad-plus-cli.js +16 -1
  105. package/tools/cli/commands/doctor.js +49 -217
  106. package/tools/cli/commands/install.js +109 -164
  107. package/tools/cli/commands/studio.js +68 -0
  108. package/tools/cli/commands/uninstall.js +173 -65
  109. package/tools/cli/commands/update-check.js +31 -0
  110. package/tools/cli/commands/update-policy.js +39 -0
  111. package/tools/cli/commands/update.js +102 -113
  112. package/tools/cli/i18n.js +60 -0
  113. package/tools/cli/lib/ide-config.js +4 -261
  114. package/tools/cli/lib/install-manifest.js +17 -0
  115. package/tools/cli/lib/installation-health.js +360 -0
  116. package/tools/cli/lib/installed-adapters.js +89 -0
  117. package/tools/cli/lib/npm-runner.js +177 -0
  118. package/tools/cli/lib/pack-copy.js +62 -66
  119. package/tools/cli/lib/packs.js +546 -3
  120. package/tools/cli/lib/python-health.js +233 -0
  121. package/tools/cli/lib/python-provision.js +2 -2
  122. package/tools/cli/lib/studio.js +310 -0
  123. package/tools/cli/lib/update-check.js +153 -0
  124. package/tools/cli/lib/update-dispatch.js +182 -0
  125. package/tools/cli/lib/update-policy.js +90 -0
  126. package/tools/cli/lib/update-transaction.js +334 -0
  127. package/tools/release/publication-content.js +901 -0
@@ -0,0 +1,68 @@
1
+ /** Inspect installed Dev Studio routes and prepare actual host instructions. */
2
+ const path = require('node:path');
3
+ const { installedPack, validatePack, prepare } = require('../lib/studio');
4
+
5
+ module.exports = {
6
+ command: 'studio <action> [workflow]',
7
+ description: 'List Dev Studio workflows or prepare read-only execution context',
8
+ options: [
9
+ ['-d, --directory <path>', 'Installed project directory'],
10
+ ['--request <text>', 'Task scope for the host assistant'],
11
+ [
12
+ '--input <file>',
13
+ 'Project-relative input file (repeatable)',
14
+ (file, files) => [...files, file],
15
+ [],
16
+ ],
17
+ ['--json', 'Print machine-readable context and evidence'],
18
+ ],
19
+ action: (action, workflow, options = {}) => {
20
+ try {
21
+ const projectDir = path.resolve(options.directory || process.cwd());
22
+ let result;
23
+ if (action === 'list' && !workflow) {
24
+ const { catalog, resources } = validatePack(installedPack(projectDir));
25
+ result = {
26
+ schemaVersion: 1,
27
+ execution: 'host-managed',
28
+ ...catalog,
29
+ validatedResources: resources.length,
30
+ };
31
+ } else if (action === 'prepare' && workflow) {
32
+ result = prepare({ projectDir, workflow, request: options.request, inputs: options.input });
33
+ } else throw new Error('Use studio list or studio prepare WORKFLOW.');
34
+ process.exitCode = result.status === 'needs-input' ? 2 : 0;
35
+ if (options.json) console.log(JSON.stringify(result, null, 2));
36
+ else if (action === 'list')
37
+ console.log(
38
+ result.workflows.map((w) => `${w.id} (${w.agent}; input: ${w.input})`).join('\n')
39
+ );
40
+ else
41
+ console.log(
42
+ [
43
+ `${result.workflow}: ${result.status} — prepared for the host, not executed.`,
44
+ `Project: ${result.projectDir}`,
45
+ `Request: ${result.request || '(none supplied)'}`,
46
+ `Resolved configuration:\n${JSON.stringify(result.config.values, null, 2)}`,
47
+ `Defaulted fields: ${result.config.defaultsUsed.join(', ') || '(none)'}`,
48
+ `Proposed report: ${result.outputPath}`,
49
+ ...result.missingInputs,
50
+ ...result.instructions.map(
51
+ (item) => `\n--- ${item.path} (${item.sha256}) ---\n${item.content}`
52
+ ),
53
+ ...result.inputs.map(
54
+ (item) => `\n--- Project input: ${item.path} (${item.sha256}) ---\n${item.content}`
55
+ ),
56
+ result.previousReport
57
+ ? `\n--- Continue existing report ---\n${result.previousReport.content}`
58
+ : '',
59
+ ].join('\n')
60
+ );
61
+ } catch (error) {
62
+ process.exitCode = 1;
63
+ if (options.json)
64
+ console.log(JSON.stringify({ schemaVersion: 1, status: 'error', error: error.message }));
65
+ else console.error('Dev Studio: ' + error.message);
66
+ }
67
+ },
68
+ };
@@ -1,101 +1,209 @@
1
1
  /**
2
2
  * BMAD+ Uninstall Command
3
- * Removes BMAD+ agents, skills, and configs from the current project
4
- * Internationalized — uses i18n system
5
- *
3
+ * Removes identifiable BMAD+ files while preserving project memory and edits.
6
4
  * Author: Laurent Rochetta
7
5
  */
8
6
 
9
7
  const path = require('node:path');
10
8
  const fs = require('node:fs');
11
- const fsExtra = require('fs-extra');
12
9
  const clack = require('@clack/prompts');
13
10
  const pc = require('picocolors');
14
11
  const { t } = require('../i18n');
12
+ const { PACKS, DERIVED } = require('../lib/packs');
13
+ const { readInstallManifest } = require('../lib/install-manifest');
14
+ const { generateUserFiles, USER_CONFIG_MARKER } = require('../../build/generate-adapters');
15
+ const { contentHash } = require('../lib/installed-adapters');
16
+
17
+ // Never follow a junction/symlink in a path used for deletion, including parents.
18
+ function safeStat(projectDir, target) {
19
+ const relative = path.relative(projectDir, target);
20
+ if (!relative || relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative)) {
21
+ throw new Error('Path outside the project: ' + target);
22
+ }
23
+ let current = projectDir;
24
+ let stat;
25
+ for (const part of relative.split(path.sep)) {
26
+ current = path.join(current, part);
27
+ try {
28
+ stat = fs.lstatSync(current);
29
+ } catch (err) {
30
+ if (err.code === 'ENOENT') return undefined;
31
+ throw err;
32
+ }
33
+ if (stat.isSymbolicLink()) {
34
+ const error = new Error('Symbolic link or junction: ' + current);
35
+ error.code = 'BMAD_UNSAFE_LINK';
36
+ throw error;
37
+ }
38
+ }
39
+ return stat;
40
+ }
41
+
42
+ function removalPlan(projectDir, manifest, manifestPath) {
43
+ const packageRoot = path.join(__dirname, '..', '..', '..');
44
+ const sourceRoot = path.join(packageRoot, 'src', 'bmad-plus');
45
+ const files = new Map();
46
+ const directories = new Set();
47
+ const targetPath = (...parts) => path.join(projectDir, ...parts);
48
+
49
+ // Old manifests contain pack IDs, not file hashes. Match shipped files byte for
50
+ // byte; anything unknown or modified stays, including files inside a pack.
51
+ function matchSource(source, target) {
52
+ const targetStat = safeStat(projectDir, target);
53
+ if (!targetStat || !fs.existsSync(source)) return;
54
+ const sourceStat = fs.lstatSync(source);
55
+ if (sourceStat.isDirectory() && targetStat.isDirectory()) {
56
+ directories.add(target);
57
+ for (const name of fs.readdirSync(source)) matchSource(path.join(source, name), path.join(target, name));
58
+ } else if (sourceStat.isFile() && targetStat.isFile()) {
59
+ const expected = fs.readFileSync(source);
60
+ if (fs.readFileSync(target).equals(expected)) files.set(target, expected);
61
+ }
62
+ }
63
+
64
+ for (const relative of ['.agents', '.agents/skills', '.agents/data', '_bmad', '_bmad-output']) {
65
+ const target = targetPath(relative);
66
+ if (safeStat(projectDir, target)?.isDirectory()) directories.add(target);
67
+ }
68
+
69
+ for (const packId of manifest.packs) {
70
+ if (!Object.hasOwn(PACKS, packId)) throw new Error('Unknown installed pack: ' + packId);
71
+ 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'));
77
+ }
78
+ for (const name of ['module.yaml', 'module-help.csv']) matchSource(path.join(sourceRoot, name), targetPath('_bmad', name));
79
+ const renderOptions = {
80
+ packs: manifest.packs,
81
+ userName: typeof manifest.user === 'string' ? manifest.user : 'Developer',
82
+ language: typeof manifest.language === 'string' ? manifest.language : 'English',
83
+ };
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 : {};
87
+ // Only registry-declared paths can become deletion candidates, even when the
88
+ // manifest contains arbitrary hash keys. Keep previously owned tool targets.
89
+ const claimed = new Set(Object.keys(hashes).filter(file => expectedFiles.has(file)));
90
+ if (Array.isArray(manifest.ides)) {
91
+ const tools = manifest.ides.filter(id => DERIVED.targets.adapters.some(adapter => adapter.tool === id));
92
+ if (tools.length) {
93
+ for (const entry of generateUserFiles(DERIVED, { ...renderOptions, tools })) {
94
+ claimed.add(entry.file);
95
+ expectedFiles.set(entry.file, entry.content);
96
+ }
97
+ }
98
+ }
99
+ const legacyFallback = !Array.isArray(manifest.ides) && Object.keys(hashes).length === 0;
100
+ for (const [file, rendered] of expectedFiles) {
101
+ if (!legacyFallback && !claimed.has(file)) continue;
102
+ const target = targetPath(file);
103
+ let stat;
104
+ try {
105
+ stat = safeStat(projectDir, target);
106
+ } catch (error) {
107
+ // A link with no ownership evidence is user content, not an uninstall target.
108
+ if (legacyFallback && error.code === 'BMAD_UNSAFE_LINK') continue;
109
+ throw error;
110
+ }
111
+ if (stat?.isFile()) {
112
+ const content = fs.readFileSync(target);
113
+ const text = content.toString('utf8');
114
+ const expectedHash = hashes[file];
115
+ const matches = expectedHash ? contentHash(text) === expectedHash : text === rendered;
116
+ if (text.includes(USER_CONFIG_MARKER) && matches) {
117
+ files.set(target, content);
118
+ let parent = path.dirname(target);
119
+ while (parent !== projectDir) {
120
+ directories.add(parent);
121
+ parent = path.dirname(parent);
122
+ }
123
+ }
124
+ }
125
+ }
126
+ // Keep config.yaml: it contains the user's settings and has no ownership hash.
127
+ files.set(manifestPath, fs.readFileSync(manifestPath));
128
+ return { files, directories };
129
+ }
15
130
 
16
131
  module.exports = {
17
132
  command: 'uninstall',
18
133
  description: 'Remove BMAD+ from your project',
19
134
  options: [
135
+ ['-y, --yes', 'Confirm removal without prompting'],
20
136
  ['-l, --lang <code>', 'Language code: en, fr, es, de, pt-br, ru, zh, he, ja, it'],
21
137
  ],
22
138
  action: async (options) => {
23
- const projectDir = process.cwd();
24
-
139
+ const projectDir = fs.realpathSync(process.cwd());
140
+ let i = t(options.lang || 'en');
25
141
  clack.intro(pc.bgRed(pc.white(' BMAD+ Uninstaller ')));
26
142
 
27
- // Check if installed
28
143
  const manifestPath = path.join(projectDir, '_bmad', '.bmad-plus-install.json');
29
- if (!fs.existsSync(manifestPath)) {
30
- clack.log.warn('BMAD+ is not installed in this directory.');
31
- clack.outro('Nothing to remove.');
144
+ let manifest;
145
+ try {
146
+ if (!safeStat(projectDir, manifestPath)) {
147
+ clack.log.warn(i.uninstall_not_installed);
148
+ return;
149
+ }
150
+ manifest = readInstallManifest(manifestPath);
151
+ } catch (err) {
152
+ clack.log.error(i.manifest_invalid(err.message));
153
+ clack.outro(pc.red(i.manifest_repair));
154
+ process.exitCode = 1;
32
155
  return;
33
156
  }
34
-
35
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
36
-
37
- // Use --lang flag, or fall back to manifest language
38
- let lang = options.lang || manifest.uiLanguage || 'en';
39
- const i = t(lang);
40
-
41
- clack.log.info(`BMAD+ v${manifest.version} (${i.installed_on || 'installed'} ${manifest.installed.split('T')[0]})`);
42
- clack.log.info(`${i.selected_packs}: ${(manifest.packs || ['core']).join(', ')}`);
43
-
44
- const confirm = await clack.confirm({
45
- message: i.uninstall_confirm || 'Remove BMAD+ from this project?',
46
- });
47
-
48
- if (!confirm || clack.isCancel(confirm)) {
49
- clack.cancel(i.cancelled);
157
+ i = t(options.lang || manifest.uiLanguage || 'en');
158
+ if (!options.yes && !process.stdin.isTTY) {
159
+ clack.log.error(i.noninteractive_requires_yes('uninstall'));
160
+ process.exitCode = 1;
50
161
  return;
51
162
  }
52
163
 
53
- const spinner = clack.spinner();
54
- spinner.start(i.uninstall_removing || 'Removing...');
55
-
56
- let removed = 0;
57
-
58
- // Remove .agents/skills/ (BMAD+ agents & skills)
59
- const agentsDir = path.join(projectDir, '.agents');
60
- if (fs.existsSync(agentsDir)) {
61
- fsExtra.removeSync(agentsDir);
62
- removed++;
63
- }
64
-
65
- // Remove _bmad/ config
66
- const bmadDir = path.join(projectDir, '_bmad');
67
- if (fs.existsSync(bmadDir)) {
68
- fsExtra.removeSync(bmadDir);
69
- removed++;
164
+ let plan;
165
+ try {
166
+ plan = removalPlan(projectDir, manifest, manifestPath);
167
+ } catch (err) {
168
+ clack.log.error(i.uninstall_aborted(err.message));
169
+ process.exitCode = 1;
170
+ return;
70
171
  }
71
-
72
- // Remove _bmad-output/ (only if empty)
73
- const outputDir = path.join(projectDir, '_bmad-output');
74
- if (fs.existsSync(outputDir)) {
75
- const contents = fs.readdirSync(outputDir, { recursive: true }).filter(f => !f.startsWith('.'));
76
- if (contents.length === 0) {
77
- fsExtra.removeSync(outputDir);
78
- removed++;
79
- } else {
80
- clack.log.info(i.uninstall_output_kept || '📁 _bmad-output/ kept (contains files)');
172
+ clack.log.info('BMAD+ v' + manifest.version + ' (' + i.installed_on + ' ' + manifest.installed.split('T')[0] + ')');
173
+ clack.log.info(i.selected_packs + ': ' + manifest.packs.join(', '));
174
+ if (!options.yes) {
175
+ const confirm = await clack.confirm({ message: i.uninstall_confirm });
176
+ if (!confirm || clack.isCancel(confirm)) {
177
+ clack.cancel(i.cancelled);
178
+ return;
81
179
  }
82
180
  }
83
181
 
84
- // Remove IDE configs (only BMAD+-generated ones)
85
- for (const configFile of ['CLAUDE.md', 'GEMINI.md', 'AGENTS.md', 'OPENCODE.md']) {
86
- const p = path.join(projectDir, configFile);
87
- if (fs.existsSync(p)) {
88
- const content = fs.readFileSync(p, 'utf8');
89
- // NODE-05: only remove a config BMAD+ actually generated (it carries the
90
- // marker below). A hand-authored file that merely mentions "BMAD+" is kept.
91
- if (content.includes('BMAD+ AI Agent Configuration')) {
92
- fs.unlinkSync(p);
182
+ const spinner = clack.spinner();
183
+ spinner.start(i.uninstall_removing);
184
+ let removed = 0;
185
+ try {
186
+ // Revalidate after the prompt, before deleting any file.
187
+ for (const target of [...plan.files.keys(), ...plan.directories]) safeStat(projectDir, target);
188
+ for (const [target, expected] of plan.files) {
189
+ if (safeStat(projectDir, target)?.isFile() && fs.readFileSync(target).equals(expected)) {
190
+ fs.unlinkSync(target);
93
191
  removed++;
94
192
  }
95
193
  }
194
+ // No recursive deletion: hidden files, nested user content, and memory stay.
195
+ for (const target of [...plan.directories].sort((a, b) => b.length - a.length)) {
196
+ if (safeStat(projectDir, target)?.isDirectory() && fs.readdirSync(target).length === 0) {
197
+ fs.rmdirSync(target);
198
+ removed++;
199
+ }
200
+ }
201
+ } catch (err) {
202
+ spinner.stop(i.uninstall_aborted(err.message));
203
+ process.exitCode = 1;
204
+ return;
96
205
  }
97
-
98
- spinner.stop(i.uninstall_done ? i.uninstall_done(removed) : `✅ BMAD+ removed (${removed} items)`);
99
- clack.outro(pc.green(i.guide_ready ? '👋 Done!' : 'Done!'));
206
+ spinner.stop(i.uninstall_done(removed));
207
+ clack.outro(i.uninstall_preserved);
100
208
  },
101
209
  };
@@ -0,0 +1,31 @@
1
+ /** Report release status without applying updates. */
2
+ const path = require('node:path');
3
+ const { checkForUpdate } = require('../lib/update-check');
4
+
5
+ module.exports = {
6
+ command: 'update-check',
7
+ description: 'Check the published BMAD+ release without applying it',
8
+ options: [
9
+ ['-d, --directory <path>', 'Project directory (default: current directory)'],
10
+ ['--refresh', 'Bypass the release cache and retry backoff'],
11
+ ['--offline', 'Report local state without network access'],
12
+ ['--json', 'Print machine-readable JSON'],
13
+ ],
14
+ action: async (options = {}) => {
15
+ const result = await checkForUpdate({
16
+ projectDir: path.resolve(options.directory || process.cwd()),
17
+ runningVersion: require('../../../package.json').version,
18
+ refresh: Boolean(options.refresh), offline: Boolean(options.offline),
19
+ checkReadiness: ({ projectDir }) => {
20
+ const { evaluateUpdateReadiness } = require('../lib/update-transaction');
21
+ return evaluateUpdateReadiness({ projectDir });
22
+ },
23
+ });
24
+ if (result.status.startsWith('invalid-')) process.exitCode = 1;
25
+ if (options.json) console.log(JSON.stringify(result));
26
+ 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)' : ''}`);
29
+ }
30
+ },
31
+ };
@@ -0,0 +1,39 @@
1
+ /** Show or explicitly configure the local update policy; never updates packages. */
2
+ const path = require('node:path');
3
+ const { readUpdatePolicy, writeUpdatePolicy } = require('../lib/update-policy');
4
+
5
+ module.exports = {
6
+ command: 'update-policy',
7
+ description: 'Show or configure the project update policy',
8
+ options: [
9
+ ['-d, --directory <path>', 'Project directory (default: current directory)'],
10
+ ['--mode <mode>', 'off, notify, or auto (auto requires --range)'],
11
+ ['--range <range>', 'Explicit semantic-version range for automatic updates'],
12
+ ['--channel <tag>', 'npm release tag (default: latest)'],
13
+ ['--allow-prerelease', 'Explicitly allow prerelease targets'],
14
+ ['--stable-only', 'Disable prerelease targets'],
15
+ ['--json', 'Print machine-readable JSON'],
16
+ ],
17
+ action: async (options = {}) => {
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.');
21
+ const projectDir = path.resolve(options.directory || process.cwd());
22
+ const policy = readUpdatePolicy(projectDir);
23
+ const changing = options.mode !== undefined || options.range !== undefined ||
24
+ options.channel !== undefined || options.allowPrerelease || options.stableOnly;
25
+ if (options.mode !== undefined) policy.mode = options.mode;
26
+ if (options.range !== undefined) policy.allowedRange = options.range;
27
+ if (options.channel !== undefined) policy.channel = options.channel;
28
+ if (options.allowPrerelease) policy.allowPrerelease = true;
29
+ if (options.stableOnly) policy.allowPrerelease = false;
30
+ const saved = changing ? writeUpdatePolicy(projectDir, policy) : policy;
31
+ 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}`);
33
+ } catch (error) {
34
+ process.exitCode = 1;
35
+ if (options.json) console.log(JSON.stringify({ error: error.message }));
36
+ else console.error(error.message);
37
+ }
38
+ },
39
+ };
@@ -1,134 +1,123 @@
1
1
  /**
2
- * BMAD+ Update Command
3
- * Updates agents and skills while preserving user config
4
- *
2
+ * BMAD+ Update Command — planned updates with recoverable backups.
5
3
  * Author: Laurent Rochetta
6
4
  */
7
-
8
5
  const path = require('node:path');
9
6
  const fs = require('node:fs');
10
- const fsExtra = require('fs-extra');
7
+ const semver = require('semver');
11
8
  const clack = require('@clack/prompts');
12
9
  const pc = require('picocolors');
13
10
  const { t } = require('../i18n');
14
- const { PACKS } = require('../lib/packs');
15
- const { copyPackFiles } = require('../lib/pack-copy');
16
-
17
- // Pack definitions imported from shared module: require('../lib/packs').PACKS
11
+ const { safeTarget } = require('../../build/generate-adapters');
12
+ const { readInstallManifest } = require('../lib/install-manifest');
13
+ const { isExactVersion } = require('../lib/update-policy');
14
+ const { planUpdate, applyPlan, restoreUpdate, MANIFEST } = require('../lib/update-transaction');
15
+
16
+ function authorizeAuto(projectDir, version) {
17
+ const { readUpdatePolicy, authorizeTarget } = require('../lib/update-policy');
18
+ const decision = authorizeTarget(readUpdatePolicy(projectDir), version);
19
+ if (!decision.allowed) throw new Error('Automatic update refused: ' + decision.reason);
20
+ }
21
+
22
+ async function confirmAction(options, message, lang = 'en') {
23
+ if (options.yes || options.auto) return true;
24
+ const i = t(lang);
25
+ if (!process.stdin.isTTY) throw new Error(i.noninteractive_requires_yes('update'));
26
+ const confirmed = await clack.confirm({ message });
27
+ if (!confirmed || clack.isCancel(confirmed)) {
28
+ clack.cancel(i.cancelled);
29
+ return false;
30
+ }
31
+ return true;
32
+ }
18
33
 
19
34
  module.exports = {
20
35
  command: 'update',
21
- description: 'Update BMAD+ agents and skills (preserves config)',
36
+ description: 'Update BMAD+ agents and skills (preserves local changes)',
22
37
  options: [
23
38
  ['-d, --directory <path>', 'Project directory (default: current directory)'],
39
+ ['-y, --yes', 'Confirm update without prompting'],
24
40
  ['-l, --lang <code>', 'Language code: en, fr, es, de, pt-br, ru, zh, he, ja, it'],
41
+ ['--latest', 'Resolve and run the exact latest compatible published package'],
42
+ ['--expected-version <version>', 'Require this exact executing package version'],
43
+ ['--auto', 'Apply only when automatic update policy and ownership allow it'],
44
+ ['--restore <receipt-id>', 'Restore files from an update receipt'],
25
45
  ],
26
46
  action: async (options) => {
27
47
  const projectDir = path.resolve(options.directory || process.cwd());
28
- const bmadSrc = path.join(__dirname, '..', '..', '..', 'src', 'bmad-plus');
29
- const packageJson = require('../../../package.json');
30
-
31
- clack.intro(pc.bgMagenta(pc.white(` BMAD+ Updater v${packageJson.version} `)));
32
-
33
- // Check if installed
34
- const manifestPath = path.join(projectDir, '_bmad', '.bmad-plus-install.json');
35
- if (!fs.existsSync(manifestPath)) {
36
- clack.log.error('BMAD+ is not installed in this directory.');
37
- clack.log.info('Run `npx bmad-plus install` first.');
38
- clack.outro(pc.red('Update aborted.'));
39
- return;
40
- }
41
-
42
- let manifest;
48
+ const version = require('../../../package.json').version;
49
+ let spinner;
43
50
  try {
44
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
45
- } catch (err) {
46
- clack.log.error(`Install manifest is unreadable or corrupt: ${err.message}`);
47
- clack.log.info('Re-run `npx bmad-plus install` to repair the installation.');
48
- clack.outro(pc.red('Update aborted.'));
49
- return;
51
+ if (options.restore && (options.latest || options.expectedVersion)) {
52
+ throw new Error('--restore cannot be combined with --latest or --expected-version.');
53
+ }
54
+ // The dispatcher resolves the target package; this local package may be older.
55
+ if (options.latest) {
56
+ const result = await require('../lib/update-dispatch').runLatestUpdate({
57
+ projectDir, auto: Boolean(options.auto), yes: Boolean(options.yes),
58
+ });
59
+ clack.log.info(result.reason || result.status);
60
+ if (result.output) clack.log.info(result.output);
61
+ if (result.targetVersion) clack.log.info('Target: v' + result.targetVersion);
62
+ if (result.reloadInstructions) clack.log.info('Reload your agent instructions or start a new session.');
63
+ return;
64
+ }
65
+
66
+ clack.intro(pc.bgMagenta(pc.white(' BMAD+ Updater v' + version + ' ')));
67
+ if (options.restore) {
68
+ if (options.auto) throw new Error('Recovery requires manual approval.');
69
+ if (!await confirmAction(options, 'Restore the files saved in receipt ' + options.restore + '?', options.lang)) return;
70
+ const restored = restoreUpdate({ projectDir, receiptId: options.restore });
71
+ clack.log.success('Restored BMAD+ v' + restored.restoredVersion);
72
+ clack.log.info('Receipt: ' + restored.receiptPath);
73
+ clack.outro('Reload your agent instructions or start a new session.');
74
+ return;
75
+ }
76
+
77
+ if (options.auto && !options.expectedVersion) throw new Error('Automatic update requires --expected-version.');
78
+ if (options.expectedVersion && options.expectedVersion !== version) {
79
+ throw new Error('Executing package version ' + version + ' does not match expected version ' + options.expectedVersion + '.');
80
+ }
81
+ const manifestPath = safeTarget(projectDir, MANIFEST);
82
+ if (!fs.existsSync(manifestPath)) throw new Error('BMAD+ is not installed in this directory. Run bmad-plus install first.');
83
+ let manifest;
84
+ try { manifest = readInstallManifest(manifestPath); } catch (error) {
85
+ throw new Error('Install manifest is unreadable or corrupt: ' + error.message, { cause: error });
86
+ }
87
+ if (!isExactVersion(version) || !isExactVersion(manifest.version)) throw new Error('Update versions must be strict semantic versions.');
88
+ if (semver.lt(version, manifest.version)) throw new Error('Downgrade refused: ' + manifest.version + ' -> ' + version);
89
+ const lang = options.lang || manifest.uiLanguage || 'en';
90
+ clack.log.info('Installed: v' + manifest.version + ' -> Executing package: v' + version);
91
+ if (manifest.version === version) {
92
+ clack.log.success('The installed version matches this executing package. Use --latest to check the registry.');
93
+ clack.outro(pc.green('Nothing to update.'));
94
+ return;
95
+ }
96
+ const plan = planUpdate({ projectDir, manifest, version, auto: Boolean(options.auto) });
97
+ if (options.auto) {
98
+ authorizeAuto(projectDir, version);
99
+ const fresh = await require('../lib/update-check').checkForUpdate({ projectDir, runningVersion: version, refresh: true });
100
+ if (fresh.source !== 'registry' || fresh.stale || !fresh.versionEligible || fresh.targetVersion !== options.expectedVersion) {
101
+ throw new Error('Automatic update requires a fresh, eligible registry target matching --expected-version.');
102
+ }
103
+ }
104
+
105
+ clack.log.info('Planned changes: ' + (plan.files.length - 1) + ' files; preserved conflicts: ' + plan.conflicts.length + '.');
106
+ if (plan.legacy) clack.log.warn('Legacy installation: original pack files will be backed up before replacement because ownership hashes are unavailable. Review the receipt to recover local edits.');
107
+ for (const conflict of plan.conflicts) clack.log.warn(conflict.file + ': preserved (' + conflict.reason + ').');
108
+ if (!await confirmAction(options, 'Apply this update from v' + manifest.version + ' to v' + version + '?', lang)) return;
109
+
110
+ spinner = clack.spinner();
111
+ spinner.start('Updating managed files...');
112
+ const result = applyPlan(plan, { reauthorize: () => authorizeAuto(projectDir, version) });
113
+ spinner.stop(result.changedFiles.length + ' files updated; ' + result.conflicts.length + ' local conflicts preserved.');
114
+ spinner = null;
115
+ clack.log.info('Receipt and backups: ' + result.receiptPath);
116
+ clack.outro(pc.green('BMAD+ v' + version + (result.conflicts.length ? ' applied partially.' : ' installed.') + ' Reload your agent instructions or start a new session.'));
117
+ } catch (error) {
118
+ if (spinner) spinner.stop('Update failed.');
119
+ clack.log.error(error.message);
120
+ process.exitCode = 1;
50
121
  }
51
- if (!manifest || typeof manifest !== 'object') {
52
- clack.log.error('Install manifest is malformed (not an object).');
53
- clack.log.info('Re-run `npx bmad-plus install` to repair the installation.');
54
- clack.outro(pc.red('Update aborted.'));
55
- return;
56
- }
57
- const lang = options.lang || manifest.uiLanguage || 'en';
58
- const i = t(lang);
59
-
60
- clack.log.info(`📦 Installed: v${manifest.version} → Available: v${packageJson.version}`);
61
-
62
- if (manifest.version === packageJson.version) {
63
- clack.log.success(i.update_current || '✅ Already up to date!');
64
- clack.outro(pc.green('Nothing to update.'));
65
- return;
66
- }
67
-
68
- // Guard against a missing/malformed `packs` field (NODE-04): coerce to an array.
69
- const selectedPacks = Array.isArray(manifest.packs) && manifest.packs.length > 0
70
- ? manifest.packs
71
- : ['core'];
72
- clack.log.info(`${i.selected_packs}: ${selectedPacks.join(', ')}`);
73
-
74
- const confirm = await clack.confirm({
75
- message: i.update_confirm || `Update from v${manifest.version} to v${packageJson.version}?`,
76
- });
77
-
78
- if (!confirm || clack.isCancel(confirm)) {
79
- clack.cancel(i.cancelled);
80
- return;
81
- }
82
-
83
- const spinner = clack.spinner();
84
- spinner.start(i.update_updating || 'Updating agents and skills...');
85
-
86
- const targetAgentsDir = path.join(projectDir, '.agents', 'skills');
87
- const targetDataDir = path.join(projectDir, '.agents', 'data');
88
-
89
- fsExtra.ensureDirSync(targetAgentsDir);
90
- fsExtra.ensureDirSync(targetDataDir);
91
-
92
- let updated = 0;
93
-
94
- const projectRoot = path.join(bmadSrc, '..', '..');
95
-
96
- for (const packId of selectedPacks) {
97
- const pack = PACKS[packId];
98
- if (!pack) continue;
99
-
100
- const result = copyPackFiles({
101
- bmadSrc,
102
- targetAgentsDir,
103
- targetDataDir,
104
- projectRoot,
105
- pack,
106
- });
107
- updated += result.copiedAgents + result.copiedSkills + result.copiedFiles;
108
- }
109
-
110
- // Update module config (always)
111
- const moduleYaml = path.join(bmadSrc, 'module.yaml');
112
- const targetBmadDir = path.join(projectDir, '_bmad');
113
- if (fs.existsSync(moduleYaml)) {
114
- fsExtra.copySync(moduleYaml, path.join(targetBmadDir, 'module.yaml'));
115
- updated++;
116
- }
117
-
118
- const helpCsv = path.join(bmadSrc, 'module-help.csv');
119
- if (fs.existsSync(helpCsv)) {
120
- fsExtra.copySync(helpCsv, path.join(targetBmadDir, 'module-help.csv'));
121
- updated++;
122
- }
123
-
124
- // Update manifest version (preserve everything else)
125
- manifest.version = packageJson.version;
126
- manifest.lastUpdated = new Date().toISOString();
127
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
128
-
129
- spinner.stop(i.update_done ? i.update_done(updated) : `✅ ${updated} files updated to v${packageJson.version}`);
130
-
131
- clack.log.info('📋 Config preserved: config.yaml, IDE configs, output directories');
132
- clack.outro(pc.green(i.update_ready || `BMAD+ v${packageJson.version} is ready! 🚀`));
133
122
  },
134
123
  };