bmad-method 6.2.2 → 6.2.3-next.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 (77) hide show
  1. package/.claude-plugin/marketplace.json +78 -0
  2. package/package.json +8 -8
  3. package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +1 -1
  4. package/src/core-skills/bmad-init/scripts/bmad_init.py +35 -4
  5. package/src/core-skills/bmad-init/scripts/tests/test_bmad_init.py +64 -0
  6. package/tools/{cli → installer}/bmad-cli.js +3 -1
  7. package/tools/{cli/lib → installer}/cli-utils.js +3 -4
  8. package/tools/{cli → installer}/commands/install.js +3 -3
  9. package/tools/{cli → installer}/commands/status.js +4 -4
  10. package/tools/{cli → installer}/commands/uninstall.js +5 -5
  11. package/tools/installer/core/config.js +52 -0
  12. package/tools/{cli/installers/lib → installer}/core/custom-module-cache.js +1 -1
  13. package/tools/installer/core/existing-install.js +127 -0
  14. package/tools/installer/core/install-paths.js +129 -0
  15. package/tools/installer/core/installer.js +1790 -0
  16. package/tools/{cli/installers/lib → installer}/core/manifest-generator.js +3 -3
  17. package/tools/{cli/installers/lib → installer}/core/manifest.js +2 -2
  18. package/tools/{cli/installers/lib/custom/handler.js → installer/custom-handler.js} +1 -1
  19. package/tools/{cli/installers/lib → installer}/ide/_config-driven.js +30 -397
  20. package/tools/{cli/installers/lib → installer}/ide/manager.js +1 -53
  21. package/tools/installer/ide/platform-codes.js +37 -0
  22. package/tools/installer/ide/platform-codes.yaml +190 -0
  23. package/tools/{cli/installers/lib → installer}/ide/shared/module-injections.js +1 -1
  24. package/tools/{cli/installers/lib → installer}/message-loader.js +2 -2
  25. package/tools/installer/modules/custom-modules.js +197 -0
  26. package/tools/installer/modules/external-manager.js +323 -0
  27. package/tools/{cli/installers/lib/core/config-collector.js → installer/modules/official-modules.js} +714 -43
  28. package/tools/{cli/lib → installer}/ui.js +65 -299
  29. package/tools/javascript-conventions.md +5 -0
  30. package/tools/bmad-npx-wrapper.js +0 -38
  31. package/tools/cli/installers/lib/core/dependency-resolver.js +0 -743
  32. package/tools/cli/installers/lib/core/detector.js +0 -223
  33. package/tools/cli/installers/lib/core/ide-config-manager.js +0 -157
  34. package/tools/cli/installers/lib/core/installer.js +0 -3002
  35. package/tools/cli/installers/lib/ide/_base-ide.js +0 -657
  36. package/tools/cli/installers/lib/ide/platform-codes.js +0 -100
  37. package/tools/cli/installers/lib/ide/platform-codes.yaml +0 -341
  38. package/tools/cli/installers/lib/modules/external-manager.js +0 -136
  39. package/tools/cli/installers/lib/modules/manager.js +0 -928
  40. package/tools/cli/lib/config.js +0 -213
  41. package/tools/cli/lib/platform-codes.js +0 -116
  42. package/tools/lib/xml-utils.js +0 -13
  43. /package/tools/{cli → installer}/README.md +0 -0
  44. /package/tools/{cli → installer}/external-official-modules.yaml +0 -0
  45. /package/tools/{cli/lib → installer}/file-ops.js +0 -0
  46. /package/tools/{cli/installers/lib → installer}/ide/shared/agent-command-generator.js +0 -0
  47. /package/tools/{cli/installers/lib → installer}/ide/shared/bmad-artifacts.js +0 -0
  48. /package/tools/{cli/installers/lib → installer}/ide/shared/path-utils.js +0 -0
  49. /package/tools/{cli/installers/lib → installer}/ide/shared/skill-manifest.js +0 -0
  50. /package/tools/{cli/installers/lib → installer}/ide/templates/agent-command-template.md +0 -0
  51. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/antigravity.md +0 -0
  52. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-agent.md +0 -0
  53. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-task.md +0 -0
  54. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-tool.md +0 -0
  55. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/default-workflow.md +0 -0
  56. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-agent.toml +0 -0
  57. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-task.toml +0 -0
  58. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-tool.toml +0 -0
  59. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-workflow-yaml.toml +0 -0
  60. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/gemini-workflow.toml +0 -0
  61. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-agent.md +0 -0
  62. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-task.md +0 -0
  63. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-tool.md +0 -0
  64. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/kiro-workflow.md +0 -0
  65. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-agent.md +0 -0
  66. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-task.md +0 -0
  67. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-tool.md +0 -0
  68. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-workflow-yaml.md +0 -0
  69. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/opencode-workflow.md +0 -0
  70. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/rovodev.md +0 -0
  71. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/trae.md +0 -0
  72. /package/tools/{cli/installers/lib → installer}/ide/templates/combined/windsurf-workflow.md +0 -0
  73. /package/tools/{cli/installers/lib → installer}/ide/templates/split/.gitkeep +0 -0
  74. /package/tools/{cli/installers → installer}/install-messages.yaml +0 -0
  75. /package/tools/{cli/lib → installer}/project-root.js +0 -0
  76. /package/tools/{cli/lib → installer}/prompts.js +0 -0
  77. /package/tools/{cli/lib → installer}/yaml-format.js +0 -0
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "bmad-method",
3
+ "owner": {
4
+ "name": "Brian (BMad) Madison"
5
+ },
6
+ "description": "Breakthrough Method of Agile AI-driven Development — a full-lifecycle framework with agents and workflows for analysis, planning, architecture, and implementation.",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/bmad-code-org/BMAD-METHOD",
9
+ "repository": "https://github.com/bmad-code-org/BMAD-METHOD",
10
+ "keywords": ["bmad", "agile", "ai", "orchestrator", "development", "methodology", "agents"],
11
+ "plugins": [
12
+ {
13
+ "name": "bmad-pro-skills",
14
+ "source": "./",
15
+ "description": "Next level skills for power users — advanced prompting techniques, agent management, and more.",
16
+ "version": "6.3.0",
17
+ "author": {
18
+ "name": "Brian (BMad) Madison"
19
+ },
20
+ "skills": [
21
+ "./src/core-skills/bmad-help",
22
+ "./src/core-skills/bmad-init",
23
+ "./src/core-skills/bmad-brainstorming",
24
+ "./src/core-skills/bmad-distillator",
25
+ "./src/core-skills/bmad-party-mode",
26
+ "./src/core-skills/bmad-shard-doc",
27
+ "./src/core-skills/bmad-advanced-elicitation",
28
+ "./src/core-skills/bmad-editorial-review-prose",
29
+ "./src/core-skills/bmad-editorial-review-structure",
30
+ "./src/core-skills/bmad-index-docs",
31
+ "./src/core-skills/bmad-review-adversarial-general",
32
+ "./src/core-skills/bmad-review-edge-case-hunter"
33
+ ]
34
+ },
35
+ {
36
+ "name": "bmad-method-lifecycle",
37
+ "source": "./",
38
+ "description": "Full-lifecycle AI development framework — agents and workflows for product analysis, planning, architecture, and implementation.",
39
+ "version": "6.3.0",
40
+ "author": {
41
+ "name": "Brian (BMad) Madison"
42
+ },
43
+ "skills": [
44
+ "./src/bmm-skills/1-analysis/bmad-product-brief",
45
+ "./src/bmm-skills/1-analysis/bmad-agent-analyst",
46
+ "./src/bmm-skills/1-analysis/bmad-agent-tech-writer",
47
+ "./src/bmm-skills/1-analysis/bmad-document-project",
48
+ "./src/bmm-skills/1-analysis/research/bmad-domain-research",
49
+ "./src/bmm-skills/1-analysis/research/bmad-market-research",
50
+ "./src/bmm-skills/1-analysis/research/bmad-technical-research",
51
+ "./src/bmm-skills/2-plan-workflows/bmad-agent-pm",
52
+ "./src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer",
53
+ "./src/bmm-skills/2-plan-workflows/bmad-create-prd",
54
+ "./src/bmm-skills/2-plan-workflows/bmad-edit-prd",
55
+ "./src/bmm-skills/2-plan-workflows/bmad-validate-prd",
56
+ "./src/bmm-skills/2-plan-workflows/bmad-create-ux-design",
57
+ "./src/bmm-skills/3-solutioning/bmad-agent-architect",
58
+ "./src/bmm-skills/3-solutioning/bmad-create-architecture",
59
+ "./src/bmm-skills/3-solutioning/bmad-check-implementation-readiness",
60
+ "./src/bmm-skills/3-solutioning/bmad-create-epics-and-stories",
61
+ "./src/bmm-skills/3-solutioning/bmad-generate-project-context",
62
+ "./src/bmm-skills/4-implementation/bmad-agent-dev",
63
+ "./src/bmm-skills/4-implementation/bmad-agent-sm",
64
+ "./src/bmm-skills/4-implementation/bmad-agent-qa",
65
+ "./src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev",
66
+ "./src/bmm-skills/4-implementation/bmad-dev-story",
67
+ "./src/bmm-skills/4-implementation/bmad-quick-dev",
68
+ "./src/bmm-skills/4-implementation/bmad-sprint-planning",
69
+ "./src/bmm-skills/4-implementation/bmad-sprint-status",
70
+ "./src/bmm-skills/4-implementation/bmad-code-review",
71
+ "./src/bmm-skills/4-implementation/bmad-create-story",
72
+ "./src/bmm-skills/4-implementation/bmad-correct-course",
73
+ "./src/bmm-skills/4-implementation/bmad-retrospective",
74
+ "./src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests"
75
+ ]
76
+ }
77
+ ]
78
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.2.2",
4
+ "version": "6.2.3-next.1",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -18,14 +18,14 @@
18
18
  },
19
19
  "license": "MIT",
20
20
  "author": "Brian (BMad) Madison",
21
- "main": "tools/cli/bmad-cli.js",
21
+ "main": "tools/installer/bmad-cli.js",
22
22
  "bin": {
23
- "bmad": "tools/bmad-npx-wrapper.js",
24
- "bmad-method": "tools/bmad-npx-wrapper.js"
23
+ "bmad": "tools/installer/bmad-cli.js",
24
+ "bmad-method": "tools/installer/bmad-cli.js"
25
25
  },
26
26
  "scripts": {
27
- "bmad:install": "node tools/cli/bmad-cli.js install",
28
- "bmad:uninstall": "node tools/cli/bmad-cli.js uninstall",
27
+ "bmad:install": "node tools/installer/bmad-cli.js install",
28
+ "bmad:uninstall": "node tools/installer/bmad-cli.js uninstall",
29
29
  "docs:build": "node tools/build-docs.mjs",
30
30
  "docs:dev": "astro dev --root website",
31
31
  "docs:fix-links": "node tools/fix-doc-links.js",
@@ -34,13 +34,13 @@
34
34
  "format:check": "prettier --check \"**/*.{js,cjs,mjs,json,yaml}\"",
35
35
  "format:fix": "prettier --write \"**/*.{js,cjs,mjs,json,yaml}\"",
36
36
  "format:fix:staged": "prettier --write",
37
- "install:bmad": "node tools/cli/bmad-cli.js install",
37
+ "install:bmad": "node tools/installer/bmad-cli.js install",
38
38
  "lint": "eslint . --ext .js,.cjs,.mjs,.yaml --max-warnings=0",
39
39
  "lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
40
40
  "lint:md": "markdownlint-cli2 \"**/*.md\"",
41
41
  "prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
42
42
  "quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run validate:refs && npm run validate:skills",
43
- "rebundle": "node tools/cli/bundlers/bundle-web.js rebundle",
43
+ "rebundle": "node tools/installer/bundlers/bundle-web.js rebundle",
44
44
  "test": "npm run test:refs && npm run test:install && npm run lint && npm run lint:md && npm run format:check",
45
45
  "test:install": "node test/test-installation-components.js",
46
46
  "test:refs": "node test/test-file-refs-csv.js",
@@ -172,7 +172,7 @@ parts: 1
172
172
  - Deferred: CI/CD integration, telemetry for module authors, air-gapped enterprise install, zip bundle integrity verification (checksums/signing), deeper non-technical platform integrations
173
173
 
174
174
  ## Current Installer (migration context)
175
- - Entry: `tools/cli/bmad-cli.js` (Commander.js) → `tools/cli/installers/lib/core/installer.js`
175
+ - Entry: `tools/installer/bmad-cli.js` (Commander.js) → `tools/installer/core/installer.js`
176
176
  - Platforms: `platform-codes.yaml` (~20 platforms with target dirs, legacy dirs, template types, special flags)
177
177
  - Manifests: CSV files (skill/workflow/agent-manifest.csv) are current source of truth, not JSON
178
178
  - External modules: `external-official-modules.yaml` (CIS, GDS, TEA, WDS) from npm with semver
@@ -166,9 +166,27 @@ def resolve_project_root_placeholder(value, project_root):
166
166
  """Replace {project-root} placeholder with actual path."""
167
167
  if not value or not isinstance(value, str):
168
168
  return value
169
- if '{project-root}' in value:
170
- return value.replace('{project-root}', str(project_root))
171
- return value
169
+ if '{project-root}' not in value:
170
+ return value
171
+
172
+ # Strip the {project-root} token to inspect what remains, so we can
173
+ # correctly handle absolute paths stored as "{project-root}//absolute/path"
174
+ # (produced by the "{project-root}/{value}" template applied to an absolute value).
175
+ suffix = value.replace('{project-root}', '', 1)
176
+
177
+ # Strip the one path separator that follows the token (if any)
178
+ if suffix.startswith('/') or suffix.startswith('\\'):
179
+ remainder = suffix[1:]
180
+ else:
181
+ remainder = suffix
182
+
183
+ if os.path.isabs(remainder):
184
+ # The original value was an absolute path stored with a {project-root}/ prefix.
185
+ # Return the absolute path directly — no joining needed.
186
+ return remainder
187
+
188
+ # Relative path: join with project root and normalize to resolve any .. segments.
189
+ return os.path.normpath(os.path.join(str(project_root), remainder))
172
190
 
173
191
 
174
192
  def parse_var_specs(vars_string):
@@ -222,9 +240,22 @@ def apply_result_template(var_def, raw_value, context):
222
240
  if not result_template:
223
241
  return raw_value
224
242
 
243
+ # If the user supplied an absolute path and the template would prefix it with
244
+ # "{project-root}/", skip the template entirely to avoid producing a broken path
245
+ # like "/my/project//absolute/path".
246
+ if isinstance(raw_value, str) and os.path.isabs(raw_value):
247
+ return raw_value
248
+
225
249
  ctx = dict(context)
226
250
  ctx['value'] = raw_value
227
- return expand_template(result_template, ctx)
251
+ result = expand_template(result_template, ctx)
252
+
253
+ # Normalize the resulting path to resolve any ".." segments (e.g. when the user
254
+ # entered a relative path such as "../../outside-dir").
255
+ if isinstance(result, str) and '{' not in result and os.path.isabs(result):
256
+ result = os.path.normpath(result)
257
+
258
+ return result
228
259
 
229
260
 
230
261
  # =============================================================================
@@ -110,6 +110,37 @@ class TestResolveProjectRootPlaceholder(unittest.TestCase):
110
110
  def test_non_string(self):
111
111
  self.assertEqual(resolve_project_root_placeholder(42, Path('/test')), 42)
112
112
 
113
+ def test_absolute_path_stored_with_prefix(self):
114
+ """Absolute output_folder entered by user is stored as '{project-root}//abs/path'
115
+ by the '{project-root}/{value}' template. It must resolve to '/abs/path', not
116
+ '/project//abs/path'."""
117
+ result = resolve_project_root_placeholder(
118
+ '{project-root}//Users/me/outside', Path('/Users/me/myproject')
119
+ )
120
+ self.assertEqual(result, '/Users/me/outside')
121
+
122
+ def test_relative_path_with_traversal_is_normalized(self):
123
+ """A relative path like '../../sibling' produces '{project-root}/../../sibling'
124
+ after the template. It must resolve to the normalized absolute path, not the
125
+ un-normalized string '/project/../../sibling'."""
126
+ result = resolve_project_root_placeholder(
127
+ '{project-root}/../../sibling', Path('/Users/me/myproject')
128
+ )
129
+ self.assertEqual(result, '/Users/sibling')
130
+
131
+ def test_relative_path_one_level_up(self):
132
+ result = resolve_project_root_placeholder(
133
+ '{project-root}/../outside-outputs', Path('/project/root')
134
+ )
135
+ self.assertEqual(result, '/project/outside-outputs')
136
+
137
+ def test_standard_relative_path_unchanged(self):
138
+ """Normal in-project relative paths continue to work correctly."""
139
+ result = resolve_project_root_placeholder(
140
+ '{project-root}/_bmad-output', Path('/project/root')
141
+ )
142
+ self.assertEqual(result, '/project/root/_bmad-output')
143
+
113
144
 
114
145
  class TestExpandTemplate(unittest.TestCase):
115
146
 
@@ -147,6 +178,39 @@ class TestApplyResultTemplate(unittest.TestCase):
147
178
  result = apply_result_template(var_def, 'English', {})
148
179
  self.assertEqual(result, 'English')
149
180
 
181
+ def test_absolute_value_skips_project_root_template(self):
182
+ """When the user enters an absolute path, the '{project-root}/{value}' template
183
+ must not be applied — doing so would produce '/project//absolute/path'."""
184
+ var_def = {'result': '{project-root}/{value}'}
185
+ result = apply_result_template(
186
+ var_def, '/Users/me/shared-outputs', {'project-root': '/Users/me/myproject'}
187
+ )
188
+ self.assertEqual(result, '/Users/me/shared-outputs')
189
+
190
+ def test_relative_traversal_value_is_normalized(self):
191
+ """A relative path like '../../outside' combined with the project-root template
192
+ must produce a clean normalized absolute path, not '/project/../../outside'."""
193
+ var_def = {'result': '{project-root}/{value}'}
194
+ result = apply_result_template(
195
+ var_def, '../../outside-dir', {'project-root': '/Users/me/myproject'}
196
+ )
197
+ self.assertEqual(result, '/Users/outside-dir')
198
+
199
+ def test_relative_one_level_up_is_normalized(self):
200
+ var_def = {'result': '{project-root}/{value}'}
201
+ result = apply_result_template(
202
+ var_def, '../sibling-outputs', {'project-root': '/project/root'}
203
+ )
204
+ self.assertEqual(result, '/project/sibling-outputs')
205
+
206
+ def test_normal_relative_value_unchanged(self):
207
+ """Standard in-project relative paths still produce the expected joined path."""
208
+ var_def = {'result': '{project-root}/{value}'}
209
+ result = apply_result_template(
210
+ var_def, '_bmad-output', {'project-root': '/project/root'}
211
+ )
212
+ self.assertEqual(result, '/project/root/_bmad-output')
213
+
150
214
 
151
215
  class TestLoadModuleYaml(unittest.TestCase):
152
216
 
@@ -1,9 +1,11 @@
1
+ #!/usr/bin/env node
2
+
1
3
  const { program } = require('commander');
2
4
  const path = require('node:path');
3
5
  const fs = require('node:fs');
4
6
  const { execSync } = require('node:child_process');
5
7
  const semver = require('semver');
6
- const prompts = require('./lib/prompts');
8
+ const prompts = require('./prompts');
7
9
 
8
10
  // The installer flow uses many sequential @clack/prompts, each adding keypress
9
11
  // listeners to stdin. Raise the limit to avoid spurious EventEmitter warnings.
@@ -8,7 +8,7 @@ const CLIUtils = {
8
8
  */
9
9
  getVersion() {
10
10
  try {
11
- const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
11
+ const packageJson = require(path.join(__dirname, '..', '..', 'package.json'));
12
12
  return packageJson.version || 'Unknown';
13
13
  } catch {
14
14
  return 'Unknown';
@@ -16,10 +16,9 @@ const CLIUtils = {
16
16
  },
17
17
 
18
18
  /**
19
- * Display BMAD logo using @clack intro + box
20
- * @param {boolean} _clearScreen - Deprecated, ignored (no longer clears screen)
19
+ * Display BMAD logo and version using @clack intro + box
21
20
  */
22
- async displayLogo(_clearScreen = true) {
21
+ async displayLogo() {
23
22
  const version = this.getVersion();
24
23
  const color = await prompts.getColor();
25
24
 
@@ -1,7 +1,7 @@
1
1
  const path = require('node:path');
2
- const prompts = require('../lib/prompts');
3
- const { Installer } = require('../installers/lib/core/installer');
4
- const { UI } = require('../lib/ui');
2
+ const prompts = require('../prompts');
3
+ const { Installer } = require('../core/installer');
4
+ const { UI } = require('../ui');
5
5
 
6
6
  const installer = new Installer();
7
7
  const ui = new UI();
@@ -1,8 +1,8 @@
1
1
  const path = require('node:path');
2
- const prompts = require('../lib/prompts');
3
- const { Installer } = require('../installers/lib/core/installer');
4
- const { Manifest } = require('../installers/lib/core/manifest');
5
- const { UI } = require('../lib/ui');
2
+ const prompts = require('../prompts');
3
+ const { Installer } = require('../core/installer');
4
+ const { Manifest } = require('../core/manifest');
5
+ const { UI } = require('../ui');
6
6
 
7
7
  const installer = new Installer();
8
8
  const manifest = new Manifest();
@@ -1,7 +1,7 @@
1
1
  const path = require('node:path');
2
2
  const fs = require('fs-extra');
3
- const prompts = require('../lib/prompts');
4
- const { Installer } = require('../installers/lib/core/installer');
3
+ const prompts = require('../prompts');
4
+ const { Installer } = require('../core/installer');
5
5
 
6
6
  const installer = new Installer();
7
7
 
@@ -62,9 +62,9 @@ module.exports = {
62
62
  }
63
63
 
64
64
  const existingInstall = await installer.getStatus(projectDir);
65
- const version = existingInstall.version || 'unknown';
66
- const modules = (existingInstall.modules || []).map((m) => m.id || m.name).join(', ');
67
- const ides = (existingInstall.ides || []).join(', ');
65
+ const version = existingInstall.installed ? existingInstall.version : 'unknown';
66
+ const modules = existingInstall.moduleIds.join(', ');
67
+ const ides = existingInstall.ides.join(', ');
68
68
 
69
69
  const outputFolder = await installer.getOutputFolder(projectDir);
70
70
 
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Clean install configuration built from user input.
3
+ * User input comes from either UI answers or headless CLI flags.
4
+ */
5
+ class Config {
6
+ constructor({ directory, modules, ides, skipPrompts, verbose, actionType, coreConfig, moduleConfigs, quickUpdate }) {
7
+ this.directory = directory;
8
+ this.modules = Object.freeze([...modules]);
9
+ this.ides = Object.freeze([...ides]);
10
+ this.skipPrompts = skipPrompts;
11
+ this.verbose = verbose;
12
+ this.actionType = actionType;
13
+ this.coreConfig = coreConfig;
14
+ this.moduleConfigs = moduleConfigs;
15
+ this._quickUpdate = quickUpdate;
16
+ Object.freeze(this);
17
+ }
18
+
19
+ /**
20
+ * Build a clean install config from raw user input.
21
+ * @param {Object} userInput - UI answers or CLI flags
22
+ * @returns {Config}
23
+ */
24
+ static build(userInput) {
25
+ const modules = [...(userInput.modules || [])];
26
+ if (userInput.installCore && !modules.includes('core')) {
27
+ modules.unshift('core');
28
+ }
29
+
30
+ return new Config({
31
+ directory: userInput.directory,
32
+ modules,
33
+ ides: userInput.skipIde ? [] : [...(userInput.ides || [])],
34
+ skipPrompts: userInput.skipPrompts || false,
35
+ verbose: userInput.verbose || false,
36
+ actionType: userInput.actionType,
37
+ coreConfig: userInput.coreConfig || {},
38
+ moduleConfigs: userInput.moduleConfigs || null,
39
+ quickUpdate: userInput._quickUpdate || false,
40
+ });
41
+ }
42
+
43
+ hasCoreConfig() {
44
+ return this.coreConfig && Object.keys(this.coreConfig).length > 0;
45
+ }
46
+
47
+ isQuickUpdate() {
48
+ return this._quickUpdate;
49
+ }
50
+ }
51
+
52
+ module.exports = { Config };
@@ -7,7 +7,7 @@
7
7
  const fs = require('fs-extra');
8
8
  const path = require('node:path');
9
9
  const crypto = require('node:crypto');
10
- const prompts = require('../../../lib/prompts');
10
+ const prompts = require('../prompts');
11
11
 
12
12
  class CustomModuleCache {
13
13
  constructor(bmadDir) {
@@ -0,0 +1,127 @@
1
+ const path = require('node:path');
2
+ const fs = require('fs-extra');
3
+ const yaml = require('yaml');
4
+ const { Manifest } = require('./manifest');
5
+
6
+ /**
7
+ * Immutable snapshot of an existing BMAD installation.
8
+ * Pure query object — no filesystem operations after construction.
9
+ */
10
+ class ExistingInstall {
11
+ #version;
12
+
13
+ constructor({ installed, version, hasCore, modules, ides, customModules }) {
14
+ this.installed = installed;
15
+ this.#version = version;
16
+ this.hasCore = hasCore;
17
+ this.modules = Object.freeze(modules.map((m) => Object.freeze({ ...m })));
18
+ this.moduleIds = Object.freeze(this.modules.map((m) => m.id));
19
+ this.ides = Object.freeze([...ides]);
20
+ this.customModules = Object.freeze([...customModules]);
21
+ Object.freeze(this);
22
+ }
23
+
24
+ get version() {
25
+ if (!this.installed) {
26
+ throw new Error('version is not available when nothing is installed');
27
+ }
28
+ return this.#version;
29
+ }
30
+
31
+ static empty() {
32
+ return new ExistingInstall({
33
+ installed: false,
34
+ version: null,
35
+ hasCore: false,
36
+ modules: [],
37
+ ides: [],
38
+ customModules: [],
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Scan a bmad directory and return an immutable snapshot of what's installed.
44
+ * @param {string} bmadDir - Path to bmad directory
45
+ * @returns {Promise<ExistingInstall>}
46
+ */
47
+ static async detect(bmadDir) {
48
+ if (!(await fs.pathExists(bmadDir))) {
49
+ return ExistingInstall.empty();
50
+ }
51
+
52
+ let version = null;
53
+ let hasCore = false;
54
+ const modules = [];
55
+ let ides = [];
56
+ let customModules = [];
57
+
58
+ const manifest = new Manifest();
59
+ const manifestData = await manifest.read(bmadDir);
60
+ if (manifestData) {
61
+ version = manifestData.version;
62
+ if (manifestData.customModules) {
63
+ customModules = manifestData.customModules;
64
+ }
65
+ if (manifestData.ides) {
66
+ ides = manifestData.ides.filter((ide) => ide && typeof ide === 'string');
67
+ }
68
+ }
69
+
70
+ const corePath = path.join(bmadDir, 'core');
71
+ if (await fs.pathExists(corePath)) {
72
+ hasCore = true;
73
+
74
+ if (!version) {
75
+ const coreConfigPath = path.join(corePath, 'config.yaml');
76
+ if (await fs.pathExists(coreConfigPath)) {
77
+ try {
78
+ const configContent = await fs.readFile(coreConfigPath, 'utf8');
79
+ const config = yaml.parse(configContent);
80
+ if (config.version) {
81
+ version = config.version;
82
+ }
83
+ } catch {
84
+ // Ignore config read errors
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ if (manifestData && manifestData.modules && manifestData.modules.length > 0) {
91
+ for (const moduleId of manifestData.modules) {
92
+ const modulePath = path.join(bmadDir, moduleId);
93
+ const moduleConfigPath = path.join(modulePath, 'config.yaml');
94
+
95
+ const moduleInfo = {
96
+ id: moduleId,
97
+ path: modulePath,
98
+ version: 'unknown',
99
+ };
100
+
101
+ if (await fs.pathExists(moduleConfigPath)) {
102
+ try {
103
+ const configContent = await fs.readFile(moduleConfigPath, 'utf8');
104
+ const config = yaml.parse(configContent);
105
+ moduleInfo.version = config.version || 'unknown';
106
+ moduleInfo.name = config.name || moduleId;
107
+ moduleInfo.description = config.description;
108
+ } catch {
109
+ // Ignore config read errors
110
+ }
111
+ }
112
+
113
+ modules.push(moduleInfo);
114
+ }
115
+ }
116
+
117
+ const installed = hasCore || modules.length > 0 || !!manifestData;
118
+
119
+ if (!installed) {
120
+ return ExistingInstall.empty();
121
+ }
122
+
123
+ return new ExistingInstall({ installed, version, hasCore, modules, ides, customModules });
124
+ }
125
+ }
126
+
127
+ module.exports = { ExistingInstall };
@@ -0,0 +1,129 @@
1
+ const path = require('node:path');
2
+ const fs = require('fs-extra');
3
+ const { getProjectRoot } = require('../project-root');
4
+ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
5
+
6
+ class InstallPaths {
7
+ static async create(config) {
8
+ const srcDir = getProjectRoot();
9
+ await assertReadableDir(srcDir, 'BMAD source root');
10
+
11
+ const pkgPath = path.join(srcDir, 'package.json');
12
+ await assertReadableFile(pkgPath, 'package.json');
13
+ const version = require(pkgPath).version;
14
+
15
+ const projectRoot = path.resolve(config.directory);
16
+ await ensureWritableDir(projectRoot, 'project root');
17
+
18
+ const bmadDir = path.join(projectRoot, BMAD_FOLDER_NAME);
19
+ const isUpdate = await fs.pathExists(bmadDir);
20
+
21
+ const configDir = path.join(bmadDir, '_config');
22
+ const agentsDir = path.join(configDir, 'agents');
23
+ const customCacheDir = path.join(configDir, 'custom');
24
+ const coreDir = path.join(bmadDir, 'core');
25
+
26
+ for (const [dir, label] of [
27
+ [bmadDir, 'bmad directory'],
28
+ [configDir, 'config directory'],
29
+ [agentsDir, 'agents config directory'],
30
+ [customCacheDir, 'custom modules cache'],
31
+ [coreDir, 'core module directory'],
32
+ ]) {
33
+ await ensureWritableDir(dir, label);
34
+ }
35
+
36
+ return new InstallPaths({
37
+ srcDir,
38
+ version,
39
+ projectRoot,
40
+ bmadDir,
41
+ configDir,
42
+ agentsDir,
43
+ customCacheDir,
44
+ coreDir,
45
+ isUpdate,
46
+ });
47
+ }
48
+
49
+ constructor(props) {
50
+ Object.assign(this, props);
51
+ Object.freeze(this);
52
+ }
53
+
54
+ manifestFile() {
55
+ return path.join(this.configDir, 'manifest.yaml');
56
+ }
57
+ agentManifest() {
58
+ return path.join(this.configDir, 'agent-manifest.csv');
59
+ }
60
+ filesManifest() {
61
+ return path.join(this.configDir, 'files-manifest.csv');
62
+ }
63
+ helpCatalog() {
64
+ return path.join(this.configDir, 'bmad-help.csv');
65
+ }
66
+ moduleDir(name) {
67
+ return path.join(this.bmadDir, name);
68
+ }
69
+ moduleConfig(name) {
70
+ return path.join(this.bmadDir, name, 'config.yaml');
71
+ }
72
+ }
73
+
74
+ async function assertReadableDir(dirPath, label) {
75
+ const stat = await fs.stat(dirPath).catch(() => null);
76
+ if (!stat) {
77
+ throw new Error(`${label} does not exist: ${dirPath}`);
78
+ }
79
+ if (!stat.isDirectory()) {
80
+ throw new Error(`${label} is not a directory: ${dirPath}`);
81
+ }
82
+ try {
83
+ await fs.access(dirPath, fs.constants.R_OK);
84
+ } catch {
85
+ throw new Error(`${label} is not readable: ${dirPath}`);
86
+ }
87
+ }
88
+
89
+ async function assertReadableFile(filePath, label) {
90
+ const stat = await fs.stat(filePath).catch(() => null);
91
+ if (!stat) {
92
+ throw new Error(`${label} does not exist: ${filePath}`);
93
+ }
94
+ if (!stat.isFile()) {
95
+ throw new Error(`${label} is not a file: ${filePath}`);
96
+ }
97
+ try {
98
+ await fs.access(filePath, fs.constants.R_OK);
99
+ } catch {
100
+ throw new Error(`${label} is not readable: ${filePath}`);
101
+ }
102
+ }
103
+
104
+ async function ensureWritableDir(dirPath, label) {
105
+ const stat = await fs.stat(dirPath).catch(() => null);
106
+ if (stat && !stat.isDirectory()) {
107
+ throw new Error(`${label} exists but is not a directory: ${dirPath}`);
108
+ }
109
+
110
+ try {
111
+ await fs.ensureDir(dirPath);
112
+ } catch (error) {
113
+ if (error.code === 'EACCES') {
114
+ throw new Error(`${label}: permission denied creating directory: ${dirPath}`);
115
+ }
116
+ if (error.code === 'ENOSPC') {
117
+ throw new Error(`${label}: no space left on device: ${dirPath}`);
118
+ }
119
+ throw new Error(`${label}: cannot create directory: ${dirPath} (${error.message})`);
120
+ }
121
+
122
+ try {
123
+ await fs.access(dirPath, fs.constants.R_OK | fs.constants.W_OK);
124
+ } catch {
125
+ throw new Error(`${label} is not writable: ${dirPath}`);
126
+ }
127
+ }
128
+
129
+ module.exports = { InstallPaths };