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.
- package/CHANGELOG.md +114 -29
- package/README.md +45 -27
- package/SECURITY.md +9 -1
- package/osint-agent-package/README.md +13 -0
- package/osint-agent-package/SETUP_KEYS.md +13 -0
- package/package.json +32 -8
- package/readme-international/README.de.md +134 -537
- package/readme-international/README.es.md +136 -556
- package/readme-international/README.fr.md +135 -553
- package/src/bmad-plus/agents/agent-shadow/SKILL.md +6 -0
- package/src/bmad-plus/packs/pack-memory/README.md +1 -1
- package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +1 -1
- package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +1 -1
- package/src/bmad-plus/packs/pack-shield/README.md +2 -0
- package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -0
- package/src/bmad-plus/packs/pack-shield/shared/audit-report-template.md +6 -0
- package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +4 -0
- package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +18 -4
- package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +95 -19
- package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +90 -10
- package/src/bmad-plus/skills/bmad-plus-uat/templates/README.md +63 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/example-uat-spec.json +95 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-release-gate.mjs +127 -0
- package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-spec-present.mjs +77 -0
- package/tools/build/adapters.config.js +1 -1
- package/tools/build/generate-adapters.js +186 -60
- package/tools/build/generate.js +49 -13
- package/tools/cli/bmad-plus-cli.js +3 -6
- package/tools/cli/commands/autoconfig.js +66 -24
- package/tools/cli/commands/install.js +620 -356
- package/tools/cli/commands/memory.js +40 -13
- package/tools/cli/commands/scan.js +94 -30
- package/tools/cli/commands/uat.js +21 -5
- package/tools/cli/commands/uninstall.js +65 -16
- package/tools/cli/commands/update-check.js +8 -3
- package/tools/cli/commands/update-policy.js +14 -5
- package/tools/cli/commands/update.js +89 -20
- package/tools/cli/i18n.js +128 -66
- package/tools/cli/lib/ide-config.js +3 -1
- package/tools/cli/lib/install-manifest.js +13 -6
- package/tools/cli/lib/installation-health.js +33 -1
- package/tools/cli/lib/installed-adapters.js +29 -6
- package/tools/cli/lib/memory-init.js +19 -4
- package/tools/cli/lib/packs.js +17 -28
- package/tools/cli/lib/path-hash.js +5 -1
- package/tools/cli/lib/python-provision.js +51 -12
- package/tools/cli/lib/stack-detect.js +51 -25
- package/tools/cli/lib/uat.js +18 -0
- package/tools/cli/lib/update-check.js +132 -34
- package/tools/cli/lib/update-dispatch.js +2 -2
- package/tools/cli/lib/update-policy.js +57 -19
- package/tools/cli/lib/update-transaction.js +439 -85
- package/tools/build/README.md +0 -78
- package/tools/build/check-counts.js +0 -626
- package/tools/build/check-install-contract.js +0 -648
- package/tools/build/generated-adapters/.codex/AGENTS.md +0 -134
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +0 -139
- package/tools/build/generated-adapters/.opencode/AGENTS.md +0 -134
- package/tools/build/generated-adapters/AGENTS.md +0 -132
- package/tools/build/generated-adapters/CLAUDE.md +0 -135
- package/tools/build/generated-adapters/CONVENTIONS.md +0 -134
- package/tools/build/generated-adapters/GEMINI.md +0 -139
- package/tools/build/generated-adapters/README.md +0 -79
- package/tools/maintain/upstream-candidate.js +0 -456
- package/tools/release/publication-content.js +0 -903
- package/tools/release/supply-chain.js +0 -282
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
const path = require('node:path');
|
|
10
10
|
const fs = require('node:fs');
|
|
11
|
+
const { isDeepStrictEqual } = require('node:util');
|
|
11
12
|
const fsExtra = require('fs-extra');
|
|
13
|
+
const semver = require('semver');
|
|
12
14
|
const clack = require('@clack/prompts');
|
|
13
15
|
const pc = require('picocolors');
|
|
14
|
-
const { t, getLanguageOptions, getCommLanguageOptions } = require('../i18n');
|
|
16
|
+
const { t, LANGUAGES, getLanguageOptions, getCommLanguageOptions } = require('../i18n');
|
|
15
17
|
const { PACKS, DERIVED } = require('../lib/packs');
|
|
16
18
|
const { copyPackFiles, copyManagedFile } = require('../lib/pack-copy');
|
|
17
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
acquireProjectLock,
|
|
21
|
+
atomicWrite,
|
|
22
|
+
inventoryOf,
|
|
23
|
+
collectManagedFiles,
|
|
24
|
+
} = require('../lib/update-transaction');
|
|
25
|
+
const { isExactVersion } = require('../lib/update-policy');
|
|
18
26
|
const { initMemory } = require('../lib/memory-init');
|
|
19
27
|
const { generateUserFiles, IDE_CONFIGS } = require('../../build/generate-adapters');
|
|
20
28
|
const { writeIDEConfigs, safeAdapterPath } = require('../lib/installed-adapters');
|
|
@@ -32,7 +40,10 @@ module.exports = {
|
|
|
32
40
|
['-d, --directory <path>', 'Installation directory (default: current directory)'],
|
|
33
41
|
['-p, --packs <packs>', 'Comma-separated pack IDs: core,osint,all (default: interactive)'],
|
|
34
42
|
['-y, --yes', 'Accept all defaults, skip prompts'],
|
|
35
|
-
[
|
|
43
|
+
[
|
|
44
|
+
'-l, --lang <code>',
|
|
45
|
+
'Language code: en, fr, es, de, pt-br, ru, zh, he, ja, it (overrides auto-detection)',
|
|
46
|
+
],
|
|
36
47
|
['--tools <tools>', 'Comma-separated IDE IDs, all, or none (default: auto-detect)'],
|
|
37
48
|
['--mode <mode>', 'Execution mode: manual, autopilot, hybrid (default: manual)'],
|
|
38
49
|
['--provision-python', 'Provision isolated environments for selected Python packs'],
|
|
@@ -50,7 +61,9 @@ module.exports = {
|
|
|
50
61
|
const projectDir = path.resolve(options.directory || process.cwd());
|
|
51
62
|
const bmadSrc = path.join(__dirname, '..', '..', '..', 'src', 'bmad-plus');
|
|
52
63
|
const previousManifestPath = safeAdapterPath(projectDir, '_bmad/.bmad-plus-install.json');
|
|
53
|
-
const previousManifestBytes = fs.existsSync(previousManifestPath)
|
|
64
|
+
const previousManifestBytes = fs.existsSync(previousManifestPath)
|
|
65
|
+
? fs.readFileSync(previousManifestPath)
|
|
66
|
+
: null;
|
|
54
67
|
let previousManifest = null;
|
|
55
68
|
let manifestError;
|
|
56
69
|
if (fs.existsSync(previousManifestPath)) {
|
|
@@ -60,10 +73,27 @@ module.exports = {
|
|
|
60
73
|
manifestError = error;
|
|
61
74
|
}
|
|
62
75
|
}
|
|
63
|
-
if (options.mode && !['manual', 'autopilot', 'hybrid'].includes(options.mode))
|
|
76
|
+
if (options.mode && !['manual', 'autopilot', 'hybrid'].includes(options.mode))
|
|
77
|
+
throw new Error('Unknown execution mode: ' + options.mode);
|
|
78
|
+
const pkgJson = require('../../../package.json');
|
|
79
|
+
|
|
80
|
+
// A version change must go through `update`: it plans, backs up, and records
|
|
81
|
+
// a receipt. Install only adds packs/tools to (or repairs) the same version.
|
|
82
|
+
const versionConflict = installedVersionConflict(previousManifest?.version, pkgJson.version);
|
|
83
|
+
if (versionConflict) {
|
|
84
|
+
clack.log.error(versionConflict);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const existingConfig = readExistingConfig(projectDir);
|
|
89
|
+
const previousPacks = (previousManifest?.packs || []).filter((id) => Object.hasOwn(PACKS, id));
|
|
90
|
+
const previousTools = (
|
|
91
|
+
Array.isArray(previousManifest?.ides) ? previousManifest.ides : []
|
|
92
|
+
).filter((id) => Object.hasOwn(IDE_CONFIGS, id));
|
|
93
|
+
const requestedPacks = parseRequestedPacks(options.packs);
|
|
94
|
+
const requestedTools = parseRequestedTools(options.tools);
|
|
64
95
|
|
|
65
96
|
// ── Step 0: Language Selection ──
|
|
66
|
-
const pkgJson = require('../../../package.json');
|
|
67
97
|
clack.intro(pc.bgCyan(pc.black(` BMAD+ Installer v${pkgJson.version} `)));
|
|
68
98
|
clack.log.info(pc.dim('✨ Created by Laurent Rochetta — github.com/lrochetta/BMAD-PLUS'));
|
|
69
99
|
|
|
@@ -93,26 +123,21 @@ module.exports = {
|
|
|
93
123
|
clack.log.info(`${i.installing_to}: ${pc.cyan(projectDir)}`);
|
|
94
124
|
|
|
95
125
|
// ── Step 1: Pack Selection ──
|
|
96
|
-
|
|
126
|
+
// Core is always included; a re-install adds to the packs already installed.
|
|
127
|
+
let chosenPacks = requestedPacks || [];
|
|
97
128
|
|
|
98
|
-
if (options.
|
|
99
|
-
const requested = options.packs.split(',').map(p => p.trim());
|
|
100
|
-
if (requested.includes('all')) {
|
|
101
|
-
selectedPacks = Object.keys(PACKS).filter(k => !PACKS[k].disabled);
|
|
102
|
-
} else {
|
|
103
|
-
selectedPacks = [...new Set(['core', ...requested.filter(p => PACKS[p] && !PACKS[p].disabled)])];
|
|
104
|
-
}
|
|
105
|
-
} else if (!options.yes) {
|
|
129
|
+
if (!requestedPacks && !options.yes) {
|
|
106
130
|
const packChoice = await clack.multiselect({
|
|
107
131
|
message: i.select_packs,
|
|
108
132
|
options: Object.entries(PACKS)
|
|
109
133
|
.filter(([, p]) => !p.required)
|
|
110
134
|
.map(([key, pack]) => ({
|
|
111
135
|
value: key,
|
|
112
|
-
label:
|
|
113
|
-
hint: pack.disabled ? i.soon :
|
|
136
|
+
label: packLabel(key),
|
|
137
|
+
hint: pack.disabled ? i.soon : pack.desc || pack.description || '',
|
|
114
138
|
disabled: pack.disabled,
|
|
115
139
|
})),
|
|
140
|
+
initialValues: previousPacks.filter((id) => !PACKS[id].required),
|
|
116
141
|
required: false,
|
|
117
142
|
});
|
|
118
143
|
|
|
@@ -121,22 +146,19 @@ module.exports = {
|
|
|
121
146
|
throw new Error(i.cancelled);
|
|
122
147
|
}
|
|
123
148
|
|
|
124
|
-
|
|
149
|
+
chosenPacks = packChoice;
|
|
125
150
|
}
|
|
151
|
+
const selectedPacks = DERIVED.packOrder.filter(
|
|
152
|
+
(id) => id === 'core' || previousPacks.includes(id) || chosenPacks.includes(id)
|
|
153
|
+
);
|
|
126
154
|
|
|
127
|
-
clack.log.success(`${i.selected_packs}: ${selectedPacks.map(
|
|
155
|
+
clack.log.success(`${i.selected_packs}: ${selectedPacks.map(packLabel).join(', ')}`);
|
|
128
156
|
|
|
129
157
|
// ── Step 2: IDE Detection ──
|
|
130
158
|
let detectedIDEs = [];
|
|
131
159
|
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
detectedIDEs = [];
|
|
135
|
-
} else if (options.tools === 'all') {
|
|
136
|
-
detectedIDEs = Object.keys(IDE_CONFIGS);
|
|
137
|
-
} else {
|
|
138
|
-
detectedIDEs = options.tools.split(',').map(t => t.trim()).filter(t => IDE_CONFIGS[t]);
|
|
139
|
-
}
|
|
160
|
+
if (requestedTools) {
|
|
161
|
+
detectedIDEs = requestedTools;
|
|
140
162
|
} else {
|
|
141
163
|
// Auto-detect
|
|
142
164
|
for (const [id, ide] of Object.entries(IDE_CONFIGS)) {
|
|
@@ -164,50 +186,76 @@ module.exports = {
|
|
|
164
186
|
}
|
|
165
187
|
}
|
|
166
188
|
|
|
167
|
-
//
|
|
168
|
-
if (detectedIDEs.length === 0 && options.yes) {
|
|
169
|
-
|
|
189
|
+
// --yes never guesses: without a detected tool, write no adapter.
|
|
190
|
+
if (detectedIDEs.length === 0 && options.yes && previousTools.length === 0) {
|
|
191
|
+
clack.log.info(
|
|
192
|
+
pc.dim(
|
|
193
|
+
'No AI tool detected; no instruction adapter written. Re-run with --tools <ids> or --tools all to add one.'
|
|
194
|
+
)
|
|
195
|
+
);
|
|
170
196
|
}
|
|
171
197
|
}
|
|
198
|
+
// Keep refreshing adapters this installation already owns, unless skipped.
|
|
199
|
+
const skipTools = options.tools === 'none' || options.tools === 'skip';
|
|
200
|
+
if (!skipTools) detectedIDEs = [...new Set([...previousTools, ...detectedIDEs])];
|
|
172
201
|
|
|
173
202
|
if (detectedIDEs.length > 0) {
|
|
174
|
-
clack.log.info(
|
|
175
|
-
|
|
203
|
+
clack.log.info(
|
|
204
|
+
`${i.detected_ides}: ${detectedIDEs.map((id) => IDE_CONFIGS[id].name).join(', ')}`
|
|
205
|
+
);
|
|
206
|
+
} else if (skipTools) {
|
|
176
207
|
clack.log.info(pc.dim('⏭️ IDE config skipped (--tools none) — existing configs preserved'));
|
|
177
208
|
}
|
|
178
209
|
|
|
179
210
|
// ── Step 3: User Config ──
|
|
180
|
-
|
|
181
|
-
let
|
|
182
|
-
|
|
211
|
+
// Existing settings are the defaults; unattended installs never replace them.
|
|
212
|
+
let userName =
|
|
213
|
+
typeof existingConfig?.user_name === 'string' && existingConfig.user_name
|
|
214
|
+
? existingConfig.user_name
|
|
215
|
+
: process.env.USER || process.env.USERNAME || 'Developer';
|
|
216
|
+
let commLang =
|
|
217
|
+
typeof existingConfig?.communication_language === 'string' &&
|
|
218
|
+
existingConfig.communication_language
|
|
219
|
+
? existingConfig.communication_language
|
|
220
|
+
: communicationLanguage(options.lang || (options.yes ? undefined : lang));
|
|
221
|
+
let execMode = options.mode || existingConfig?.execution_mode || 'manual';
|
|
222
|
+
if (!['manual', 'autopilot', 'hybrid'].includes(execMode)) execMode = 'manual';
|
|
183
223
|
let uatEnvironment = '';
|
|
224
|
+
const explicitSettings = {};
|
|
225
|
+
if (options.mode) explicitSettings.execution_mode = options.mode;
|
|
184
226
|
|
|
185
227
|
if (!options.yes) {
|
|
186
228
|
const userConfig = await clack.group({
|
|
187
|
-
userName: () =>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
229
|
+
userName: () =>
|
|
230
|
+
clack.text({
|
|
231
|
+
message: i.enter_name,
|
|
232
|
+
placeholder: userName,
|
|
233
|
+
defaultValue: userName,
|
|
234
|
+
}),
|
|
235
|
+
commLang: () =>
|
|
236
|
+
clack.select({
|
|
237
|
+
message: i.comm_language,
|
|
238
|
+
options: getCommLanguageOptions(),
|
|
239
|
+
initialValue: commLang,
|
|
240
|
+
}),
|
|
241
|
+
uatEnvironment: () =>
|
|
242
|
+
clack.text({
|
|
243
|
+
message:
|
|
244
|
+
i.uat_environment ||
|
|
245
|
+
'Test environment for human acceptance recipes (URL, blank if none)',
|
|
246
|
+
placeholder: 'https://demo.example.com',
|
|
247
|
+
defaultValue: '',
|
|
248
|
+
}),
|
|
249
|
+
execMode: () =>
|
|
250
|
+
clack.select({
|
|
251
|
+
message: i.exec_mode,
|
|
252
|
+
options: [
|
|
253
|
+
{ value: 'manual', label: i.exec_manual },
|
|
254
|
+
{ value: 'autopilot', label: i.exec_autopilot },
|
|
255
|
+
{ value: 'hybrid', label: i.exec_hybrid },
|
|
256
|
+
],
|
|
257
|
+
initialValue: execMode,
|
|
258
|
+
}),
|
|
211
259
|
});
|
|
212
260
|
|
|
213
261
|
if (clack.isCancel(userConfig)) {
|
|
@@ -220,343 +268,559 @@ module.exports = {
|
|
|
220
268
|
const fb = process.env.USER || process.env.USERNAME || 'Developer';
|
|
221
269
|
const { name: validatedName, warnings } = validateUserName(rawName, fb);
|
|
222
270
|
for (const w of warnings) clack.log.warn(w);
|
|
271
|
+
// Answers given in the wizard are explicit and replace existing settings.
|
|
272
|
+
if (userConfig.commLang !== commLang)
|
|
273
|
+
explicitSettings.document_output_language = userConfig.commLang;
|
|
223
274
|
userName = validatedName;
|
|
224
275
|
commLang = userConfig.commLang;
|
|
225
276
|
execMode = options.mode || userConfig.execMode;
|
|
226
277
|
uatEnvironment = String(userConfig.uatEnvironment || '').trim();
|
|
278
|
+
Object.assign(explicitSettings, {
|
|
279
|
+
user_name: userName,
|
|
280
|
+
communication_language: commLang,
|
|
281
|
+
execution_mode: execMode,
|
|
282
|
+
});
|
|
283
|
+
if (uatEnvironment)
|
|
284
|
+
explicitSettings.uat = { environment: { name: 'TEST', url: uatEnvironment } };
|
|
227
285
|
}
|
|
228
286
|
|
|
229
287
|
const releaseLock = acquireProjectLock(projectDir);
|
|
230
288
|
try {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
289
|
+
const { safeTarget } = require('../../build/generate-adapters');
|
|
290
|
+
const plannedFiles = collectManagedFiles({ projectDir, packs: selectedPacks });
|
|
291
|
+
if (detectedIDEs.length)
|
|
292
|
+
plannedFiles.push(
|
|
293
|
+
...generateUserFiles(DERIVED, {
|
|
294
|
+
packs: selectedPacks,
|
|
295
|
+
tools: detectedIDEs,
|
|
296
|
+
userName,
|
|
297
|
+
language: commLang,
|
|
298
|
+
})
|
|
299
|
+
);
|
|
300
|
+
for (const { file } of plannedFiles) safeTarget(projectDir, file);
|
|
301
|
+
safeTarget(projectDir, '_bmad/config.yaml');
|
|
302
|
+
safeTarget(projectDir, '_bmad/.bmad-plus-install.json');
|
|
303
|
+
for (const directory of [
|
|
304
|
+
'.agents/skills',
|
|
305
|
+
'.agents/data',
|
|
306
|
+
'_bmad',
|
|
307
|
+
'_bmad-output/discovery',
|
|
308
|
+
'_bmad-output/build',
|
|
309
|
+
'docs',
|
|
310
|
+
]) {
|
|
311
|
+
safeTarget(projectDir, directory + '/.bmad-preflight');
|
|
312
|
+
}
|
|
313
|
+
const currentManifestBytes = fs.existsSync(previousManifestPath)
|
|
314
|
+
? fs.readFileSync(previousManifestPath)
|
|
315
|
+
: null;
|
|
316
|
+
if (
|
|
317
|
+
previousManifestBytes === null
|
|
318
|
+
? currentManifestBytes !== null
|
|
319
|
+
: currentManifestBytes === null || !previousManifestBytes.equals(currentManifestBytes)
|
|
320
|
+
) {
|
|
321
|
+
throw new Error('Installation changed while preparing install. Please retry.');
|
|
322
|
+
}
|
|
323
|
+
// Preserve the unreadable manifest before repair; do not trust its ownership
|
|
324
|
+
// hashes. The adapter writer will retain locally edited user instructions.
|
|
325
|
+
if (manifestError) {
|
|
326
|
+
const backupBase = '_bmad/.bmad-plus-install.json.corrupt.bak';
|
|
327
|
+
let backupFile = backupBase;
|
|
328
|
+
let index = 1;
|
|
329
|
+
while (fs.existsSync(safeAdapterPath(projectDir, backupFile)))
|
|
330
|
+
backupFile = `${backupBase}.${index++}`;
|
|
331
|
+
const backupPath = safeAdapterPath(projectDir, backupFile);
|
|
332
|
+
fs.copyFileSync(
|
|
333
|
+
safeAdapterPath(projectDir, '_bmad/.bmad-plus-install.json'),
|
|
334
|
+
backupPath,
|
|
335
|
+
fs.constants.COPYFILE_EXCL
|
|
336
|
+
);
|
|
337
|
+
clack.log.warn(`${i.manifest_invalid(manifestError.message)}\nBackup: ${backupPath}`);
|
|
338
|
+
}
|
|
258
339
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
340
|
+
// ── Step 4: Install Files ──
|
|
341
|
+
const spinner = clack.spinner();
|
|
342
|
+
spinner.start(i.installing_files);
|
|
343
|
+
|
|
344
|
+
const targetAgentsDir = path.join(projectDir, '.agents', 'skills');
|
|
345
|
+
const targetDataDir = path.join(projectDir, '.agents', 'data');
|
|
346
|
+
const targetBmadDir = path.join(projectDir, '_bmad');
|
|
347
|
+
|
|
348
|
+
// Create directories
|
|
349
|
+
fsExtra.ensureDirSync(targetAgentsDir);
|
|
350
|
+
fsExtra.ensureDirSync(targetDataDir);
|
|
351
|
+
fsExtra.ensureDirSync(targetBmadDir);
|
|
352
|
+
|
|
353
|
+
let copiedAgents = 0;
|
|
354
|
+
let copiedSkills = 0;
|
|
355
|
+
let copiedFiles = 0;
|
|
356
|
+
const previousInventory = inventoryOf(previousManifest || {}) || {};
|
|
357
|
+
const fileInventory = { ...previousInventory };
|
|
358
|
+
let inventoryComplete = true;
|
|
359
|
+
const onFileConflict = (file) => {
|
|
360
|
+
if (!previousInventory[file]) inventoryComplete = false;
|
|
361
|
+
clack.log.warn(`${file}: locally changed or unowned content preserved.`);
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const projectRoot = path.join(bmadSrc, '..', '..');
|
|
365
|
+
|
|
366
|
+
for (const packId of selectedPacks) {
|
|
367
|
+
const pack = PACKS[packId];
|
|
368
|
+
if (!pack || pack.disabled) continue;
|
|
369
|
+
|
|
370
|
+
const result = copyPackFiles({
|
|
371
|
+
bmadSrc,
|
|
372
|
+
targetAgentsDir,
|
|
373
|
+
targetDataDir,
|
|
374
|
+
projectRoot,
|
|
375
|
+
pack,
|
|
376
|
+
projectDir,
|
|
377
|
+
inventory: fileInventory,
|
|
378
|
+
previousInventory,
|
|
379
|
+
onConflict: onFileConflict,
|
|
380
|
+
});
|
|
381
|
+
copiedAgents += result.copiedAgents;
|
|
382
|
+
copiedSkills += result.copiedSkills;
|
|
383
|
+
copiedFiles += result.copiedFiles;
|
|
384
|
+
|
|
385
|
+
// Memory pack: initialize brain with existing brain detection.
|
|
386
|
+
// Non-fatal: a read-only or missing home dir (containers, CI) must not
|
|
387
|
+
// abort the install mid-flight and leave a manifest-less partial tree.
|
|
388
|
+
if (packId === 'memory' && pack.packDir) {
|
|
389
|
+
try {
|
|
390
|
+
initMemory({ projectDir, bmadSrc, userName, commLang, selectedPacks });
|
|
391
|
+
} catch (err) {
|
|
392
|
+
clack.log.warn(
|
|
393
|
+
`Memory/brain initialization failed (non-fatal, install continues): ${err.message}`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
309
396
|
}
|
|
310
397
|
}
|
|
311
|
-
}
|
|
312
398
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
399
|
+
// ── Python provisioning (opt-in) — for packs whose runtime needs Python ──
|
|
400
|
+
// Off by default so `install` never spawns Python unless asked. Closes the
|
|
401
|
+
// "SEO pack ships prompts that call scripts never installed" gap (PROD-02).
|
|
402
|
+
if (options.provisionPython) {
|
|
403
|
+
const pythonPacks = selectedPacks.filter((p) => PYTHON_PACKS[p]);
|
|
404
|
+
for (const packId of pythonPacks) {
|
|
405
|
+
const cfg = PYTHON_PACKS[packId];
|
|
406
|
+
const requirementsPath = path.join(projectRoot, ...cfg.requirements);
|
|
407
|
+
if (!fs.existsSync(requirementsPath)) {
|
|
408
|
+
clack.log.warn(
|
|
409
|
+
`Python provisioning skipped for ${packId}: ${requirementsPath} not found.`
|
|
410
|
+
);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const pySpinner = clack.spinner();
|
|
414
|
+
pySpinner.start(`Provisioning Python runtime for ${PACKS[packId].name}…`);
|
|
415
|
+
const res = provisionPack({
|
|
416
|
+
envDir: path.join(projectDir, '.bmad', 'venv', packId),
|
|
417
|
+
requirementsPath,
|
|
418
|
+
verifyModules: cfg.verifyModules,
|
|
419
|
+
});
|
|
420
|
+
pySpinner.stop(
|
|
421
|
+
res.ok
|
|
422
|
+
? `${PACKS[packId].name}: Python runtime ready (${res.tool}).`
|
|
423
|
+
: `${PACKS[packId].name}: Python not provisioned — ${res.messages[res.messages.length - 1] || 'see guidance'}`
|
|
424
|
+
);
|
|
425
|
+
if (!res.ok) {
|
|
426
|
+
clack.log.warn(
|
|
427
|
+
`Run \`${PACKS[packId].name}\` Python tools after installing Python ≥3.11; the pack's prompts still work, only the local scripts need the runtime.`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
339
430
|
}
|
|
340
431
|
}
|
|
341
|
-
}
|
|
342
432
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
433
|
+
// Copy module config
|
|
434
|
+
const moduleYaml = path.join(bmadSrc, 'module.yaml');
|
|
435
|
+
if (fs.existsSync(moduleYaml)) {
|
|
436
|
+
copyManagedFile({
|
|
437
|
+
source: moduleYaml,
|
|
438
|
+
target: path.join(targetBmadDir, 'module.yaml'),
|
|
439
|
+
projectDir,
|
|
440
|
+
inventory: fileInventory,
|
|
441
|
+
previousInventory,
|
|
442
|
+
onConflict: onFileConflict,
|
|
443
|
+
});
|
|
444
|
+
copiedFiles++;
|
|
445
|
+
}
|
|
350
446
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
447
|
+
const helpCsv = path.join(bmadSrc, 'module-help.csv');
|
|
448
|
+
if (fs.existsSync(helpCsv)) {
|
|
449
|
+
copyManagedFile({
|
|
450
|
+
source: helpCsv,
|
|
451
|
+
target: path.join(targetBmadDir, 'module-help.csv'),
|
|
452
|
+
projectDir,
|
|
453
|
+
inventory: fileInventory,
|
|
454
|
+
previousInventory,
|
|
455
|
+
onConflict: onFileConflict,
|
|
456
|
+
});
|
|
457
|
+
copiedFiles++;
|
|
458
|
+
}
|
|
357
459
|
|
|
358
|
-
|
|
460
|
+
spinner.stop(i.installed_summary(copiedAgents, copiedSkills, copiedFiles));
|
|
461
|
+
|
|
462
|
+
// ── Step 5: Generate IDE Configs ──
|
|
463
|
+
let adapterHashes = previousManifest?.adapterHashes || {};
|
|
464
|
+
if (detectedIDEs.length > 0) {
|
|
465
|
+
const ideSpinner = clack.spinner();
|
|
466
|
+
ideSpinner.start(i.configuring_ides);
|
|
467
|
+
|
|
468
|
+
const outcome = writeIDEConfigs({
|
|
469
|
+
projectDir,
|
|
470
|
+
files: generateUserFiles(DERIVED, {
|
|
471
|
+
packs: selectedPacks,
|
|
472
|
+
userName,
|
|
473
|
+
language: commLang,
|
|
474
|
+
tools: detectedIDEs,
|
|
475
|
+
}),
|
|
476
|
+
adapterHashes,
|
|
477
|
+
yes: options.yes,
|
|
478
|
+
});
|
|
479
|
+
adapterHashes = { ...adapterHashes, ...outcome.adapterHashes };
|
|
359
480
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (detectedIDEs.length > 0) {
|
|
363
|
-
const ideSpinner = clack.spinner();
|
|
364
|
-
ideSpinner.start(i.configuring_ides);
|
|
481
|
+
ideSpinner.stop(i.ide_configured(detectedIDEs.length));
|
|
482
|
+
}
|
|
365
483
|
|
|
366
|
-
|
|
484
|
+
// ── Step 6: Create config.yaml ──
|
|
485
|
+
const configYaml = generateConfigYaml(
|
|
486
|
+
userName,
|
|
487
|
+
commLang,
|
|
367
488
|
projectDir,
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
fsExtra.ensureDirSync(path.join(outputDir, 'discovery'));
|
|
385
|
-
fsExtra.ensureDirSync(path.join(outputDir, 'build'));
|
|
386
|
-
fsExtra.ensureDirSync(path.join(projectDir, 'docs'));
|
|
387
|
-
|
|
388
|
-
// ── Step 8: Write install manifest ──
|
|
389
|
-
const pkgVersion = require('../../../package.json').version;
|
|
390
|
-
const manifest = {
|
|
391
|
-
version: pkgVersion,
|
|
392
|
-
uiLanguage: lang,
|
|
393
|
-
installed: new Date().toISOString(),
|
|
394
|
-
packs: selectedPacks,
|
|
395
|
-
ides: detectedIDEs,
|
|
396
|
-
adapterHashes,
|
|
397
|
-
fileInventory: { schemaVersion: 1, complete: inventoryComplete, files: fileInventory },
|
|
398
|
-
executionMode: execMode,
|
|
399
|
-
user: userName,
|
|
400
|
-
language: commLang,
|
|
401
|
-
};
|
|
402
|
-
fs.writeFileSync(
|
|
403
|
-
path.join(targetBmadDir, '.bmad-plus-install.json'),
|
|
404
|
-
JSON.stringify(manifest, null, 2),
|
|
405
|
-
'utf8'
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
// ── Summary — Contextual Getting Started ──
|
|
409
|
-
const agentGuide = [
|
|
410
|
-
i.guide_who,
|
|
411
|
-
'',
|
|
412
|
-
` ${i.guide_idea.padEnd(28)} → "Atlas, [...]"`,
|
|
413
|
-
` ${i.guide_prd.padEnd(28)} → "Atlas, create PRD"`,
|
|
414
|
-
` ${i.guide_arch.padEnd(28)} → "Forge, propose architecture"`,
|
|
415
|
-
` ${i.guide_code.padEnd(28)} → "Forge, implement story [X]"`,
|
|
416
|
-
` ${i.guide_test.padEnd(28)} → "Sentinel, review module [X]"`,
|
|
417
|
-
` ${i.guide_sprint.padEnd(28)} → "Nexus, create epics"`,
|
|
418
|
-
` ${i.guide_auto.padEnd(28)} → "autopilot"`,
|
|
419
|
-
];
|
|
420
|
-
|
|
421
|
-
if (selectedPacks.includes('osint')) {
|
|
422
|
-
agentGuide.push(` ${i.guide_osint.padEnd(28)} → "Shadow, investigate [name]"`);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (selectedPacks.includes('maker')) {
|
|
426
|
-
agentGuide.push(` ${i.guide_maker.padEnd(28)} → "Maker, create agent [desc]"`);
|
|
427
|
-
}
|
|
489
|
+
execMode,
|
|
490
|
+
uatEnvironment
|
|
491
|
+
);
|
|
492
|
+
const configPath = path.join(targetBmadDir, 'config.yaml');
|
|
493
|
+
if (!fs.existsSync(configPath)) {
|
|
494
|
+
atomicWrite(projectDir, '_bmad/config.yaml', configYaml);
|
|
495
|
+
} else {
|
|
496
|
+
const merged = mergeConfigYaml(
|
|
497
|
+
fs.readFileSync(configPath, 'utf8'),
|
|
498
|
+
configYaml,
|
|
499
|
+
explicitSettings
|
|
500
|
+
);
|
|
501
|
+
if (merged === null)
|
|
502
|
+
clack.log.warn('_bmad/config.yaml could not be parsed; it was left unchanged.');
|
|
503
|
+
else if (merged.changed) atomicWrite(projectDir, '_bmad/config.yaml', merged.content);
|
|
504
|
+
}
|
|
428
505
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
506
|
+
// ── Step 7: Create output directories ──
|
|
507
|
+
const outputDir = path.join(projectDir, '_bmad-output');
|
|
508
|
+
fsExtra.ensureDirSync(path.join(outputDir, 'discovery'));
|
|
509
|
+
fsExtra.ensureDirSync(path.join(outputDir, 'build'));
|
|
510
|
+
fsExtra.ensureDirSync(path.join(projectDir, 'docs'));
|
|
511
|
+
|
|
512
|
+
// ── Step 8: Write install manifest ──
|
|
513
|
+
const manifest = {
|
|
514
|
+
version: pkgJson.version,
|
|
515
|
+
uiLanguage: lang,
|
|
516
|
+
installed: previousManifest?.installed || new Date().toISOString(),
|
|
517
|
+
...(previousManifest ? { lastInstalled: new Date().toISOString() } : {}),
|
|
518
|
+
packs: selectedPacks,
|
|
519
|
+
ides: [...new Set([...previousTools, ...detectedIDEs])],
|
|
520
|
+
adapterHashes,
|
|
521
|
+
fileInventory: { schemaVersion: 1, complete: inventoryComplete, files: fileInventory },
|
|
522
|
+
executionMode: execMode,
|
|
523
|
+
user: userName,
|
|
524
|
+
language: commLang,
|
|
525
|
+
};
|
|
526
|
+
atomicWrite(projectDir, '_bmad/.bmad-plus-install.json', JSON.stringify(manifest, null, 2));
|
|
527
|
+
|
|
528
|
+
// ── Summary — Contextual Getting Started ──
|
|
529
|
+
const agentGuide = [
|
|
530
|
+
i.guide_who,
|
|
531
|
+
'',
|
|
532
|
+
` ${i.guide_idea.padEnd(28)} → "Atlas, [...]"`,
|
|
533
|
+
` ${i.guide_prd.padEnd(28)} → "Atlas, create PRD"`,
|
|
534
|
+
` ${i.guide_arch.padEnd(28)} → "Forge, propose architecture"`,
|
|
535
|
+
` ${i.guide_code.padEnd(28)} → "Forge, implement story [X]"`,
|
|
536
|
+
` ${i.guide_test.padEnd(28)} → "Sentinel, review module [X]"`,
|
|
537
|
+
` ${i.guide_sprint.padEnd(28)} → "Nexus, create epics"`,
|
|
538
|
+
` ${i.guide_auto.padEnd(28)} → "autopilot"`,
|
|
539
|
+
];
|
|
540
|
+
|
|
541
|
+
if (selectedPacks.includes('osint')) {
|
|
542
|
+
agentGuide.push(` ${i.guide_osint.padEnd(28)} → "Shadow, investigate [name]"`);
|
|
543
|
+
}
|
|
432
544
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
545
|
+
if (selectedPacks.includes('maker')) {
|
|
546
|
+
agentGuide.push(` ${i.guide_maker.padEnd(28)} → "Maker, create agent [desc]"`);
|
|
547
|
+
}
|
|
436
548
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
549
|
+
if (selectedPacks.includes('seo')) {
|
|
550
|
+
agentGuide.push(` ${i.guide_seo.padEnd(28)} → "/seo audit <url>"`);
|
|
551
|
+
}
|
|
440
552
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
553
|
+
if (selectedPacks.includes('backup')) {
|
|
554
|
+
agentGuide.push(` ${i.guide_backup.padEnd(28)} → "/backup create"`);
|
|
555
|
+
}
|
|
444
556
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
557
|
+
if (selectedPacks.includes('animated')) {
|
|
558
|
+
agentGuide.push(` ${i.guide_animated.padEnd(28)} → "/animated build <video>"`);
|
|
559
|
+
}
|
|
448
560
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
561
|
+
if (selectedPacks.includes('shield')) {
|
|
562
|
+
agentGuide.push(
|
|
563
|
+
` ${(i.guide_shield || '🛡️ GRC Compliance').padEnd(28)} → "Shield, audit my SaaS for GDPR"`
|
|
564
|
+
);
|
|
565
|
+
}
|
|
452
566
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
' 3. Sentinel (tests → review)',
|
|
459
|
-
'',
|
|
460
|
-
i.guide_or_auto,
|
|
461
|
-
'',
|
|
462
|
-
`${i.guide_output}: _bmad-output/discovery/ & _bmad-output/build/`,
|
|
463
|
-
'',
|
|
464
|
-
'─'.repeat(50),
|
|
465
|
-
'',
|
|
466
|
-
`📦 ${i.guide_cli_title || 'CLI Commands'}:`,
|
|
467
|
-
` npx bmad-plus install ${i.guide_cli_install || '— Install agents & skills'}`,
|
|
468
|
-
` npx bmad-plus update ${i.guide_cli_update || '— Update agents (keeps config)'}`,
|
|
469
|
-
` npx bmad-plus doctor ${i.guide_cli_doctor || '— Check installation health'}`,
|
|
470
|
-
` npx bmad-plus uninstall ${i.guide_cli_uninstall || '— Remove BMAD+ from project'}`,
|
|
471
|
-
);
|
|
567
|
+
if (selectedPacks.includes('memory')) {
|
|
568
|
+
agentGuide.push(
|
|
569
|
+
` ${(i.guide_memory || '🧠 Memory Brain').padEnd(28)} → "Zecher, scan projects in [path]"`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
472
572
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (selectedPacks.includes('backup')) {
|
|
479
|
-
examples.push(` ${i.guide_example_backup || '🗂️ Backup: "/backup create" → ZIP timestamped'}`);
|
|
480
|
-
}
|
|
481
|
-
if (selectedPacks.includes('animated')) {
|
|
482
|
-
examples.push(` ${i.guide_example_animated || '🎬 Animated: "/animated build hero.mp4"'}`);
|
|
483
|
-
}
|
|
484
|
-
if (selectedPacks.includes('osint')) {
|
|
485
|
-
examples.push(` ${i.guide_example_osint || '🔍 OSINT: "Shadow, investigate John Doe"'}`);
|
|
486
|
-
}
|
|
487
|
-
if (selectedPacks.includes('shield')) {
|
|
488
|
-
examples.push(
|
|
489
|
-
` ${i.guide_example_shield_1 || '🛡️ GRC: "Shield, audit my app for GDPR compliance"'}`,
|
|
490
|
-
` ${i.guide_example_shield_2 || '🛡️ GRC: "Shield, gap analysis ISO 27001 vs NIST CSF"'}`,
|
|
491
|
-
` ${i.guide_example_shield_3 || '🛡️ GRC: "Shield, generate SOC 2 evidence checklist"'}`,
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
if (selectedPacks.includes('dev-studio')) {
|
|
495
|
-
examples.push(
|
|
496
|
-
` ${i.guide_example_dev_studio_1 || '🏗️ Dev Studio: "Miriam, brainstorm a productivity app"'}`,
|
|
497
|
-
` ${i.guide_example_dev_studio_2 || '🏗️ Dev Studio: "Bezalel, design the architecture"'}`,
|
|
498
|
-
` ${i.guide_example_dev_studio_3 || '🏗️ Dev Studio: "Oholiab, implement story S1"'}`,
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
if (selectedPacks.includes('memory')) {
|
|
502
|
-
examples.push(
|
|
503
|
-
` ${i.guide_example_memory_1 || '🧠 Memory: "Zecher, scan projects in D:\\travail\\DEV"'}`,
|
|
504
|
-
` ${i.guide_example_memory_2 || '🧠 Memory: "Zecher, where were we?"'}`,
|
|
505
|
-
` ${i.guide_example_memory_3 || '🧠 Memory: "Zecher, consolidate memory"'}`,
|
|
506
|
-
);
|
|
507
|
-
}
|
|
573
|
+
if (selectedPacks.includes('dev-studio')) {
|
|
574
|
+
agentGuide.push(
|
|
575
|
+
` ${(i.guide_dev_studio || '🏗️ Dev Studio').padEnd(28)} → "Miriam, brainstorm my app idea"`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
508
578
|
|
|
509
|
-
if (examples.length > 0) {
|
|
510
579
|
agentGuide.push(
|
|
511
580
|
'',
|
|
512
|
-
|
|
513
|
-
|
|
581
|
+
i.guide_workflow,
|
|
582
|
+
' 1. Atlas (idea → brief → PRD)',
|
|
583
|
+
' 2. Forge (architecture → code)',
|
|
584
|
+
' 3. Sentinel (tests → review)',
|
|
585
|
+
'',
|
|
586
|
+
i.guide_or_auto,
|
|
587
|
+
'',
|
|
588
|
+
`${i.guide_output}: _bmad-output/discovery/ & _bmad-output/build/`,
|
|
589
|
+
'',
|
|
590
|
+
'─'.repeat(50),
|
|
591
|
+
'',
|
|
592
|
+
`📦 ${i.guide_cli_title || 'CLI Commands'}:`,
|
|
593
|
+
` npx bmad-plus install ${i.guide_cli_install || '— Install agents & skills'}`,
|
|
594
|
+
` npx bmad-plus update ${i.guide_cli_update || '— Update agents (keeps config)'}`,
|
|
595
|
+
` npx bmad-plus doctor ${i.guide_cli_doctor || '— Check installation health'}`,
|
|
596
|
+
` npx bmad-plus uninstall ${i.guide_cli_uninstall || '— Remove BMAD+ from project'}`
|
|
514
597
|
);
|
|
515
|
-
}
|
|
516
598
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
'
|
|
520
|
-
|
|
521
|
-
|
|
599
|
+
// Add pack-specific examples
|
|
600
|
+
const examples = [];
|
|
601
|
+
if (selectedPacks.includes('seo')) {
|
|
602
|
+
examples.push(` ${i.guide_example_seo || '🔍 SEO: "/seo audit https://example.com"'}`);
|
|
603
|
+
}
|
|
604
|
+
if (selectedPacks.includes('backup')) {
|
|
605
|
+
examples.push(
|
|
606
|
+
` ${i.guide_example_backup || '🗂️ Backup: "/backup create" → ZIP timestamped'}`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
if (selectedPacks.includes('animated')) {
|
|
610
|
+
examples.push(` ${i.guide_example_animated || '🎬 Animated: "/animated build hero.mp4"'}`);
|
|
611
|
+
}
|
|
612
|
+
if (selectedPacks.includes('osint')) {
|
|
613
|
+
examples.push(` ${i.guide_example_osint || '🔍 OSINT: "Shadow, investigate John Doe"'}`);
|
|
614
|
+
}
|
|
615
|
+
if (selectedPacks.includes('shield')) {
|
|
616
|
+
examples.push(
|
|
617
|
+
` ${i.guide_example_shield_1 || '🛡️ GRC: "Shield, audit my app for GDPR compliance"'}`,
|
|
618
|
+
` ${i.guide_example_shield_2 || '🛡️ GRC: "Shield, gap analysis ISO 27001 vs NIST CSF"'}`,
|
|
619
|
+
` ${i.guide_example_shield_3 || '🛡️ GRC: "Shield, generate SOC 2 evidence checklist"'}`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
if (selectedPacks.includes('dev-studio')) {
|
|
623
|
+
examples.push(
|
|
624
|
+
` ${i.guide_example_dev_studio_1 || '🏗️ Dev Studio: "Miriam, brainstorm a productivity app"'}`,
|
|
625
|
+
` ${i.guide_example_dev_studio_2 || '🏗️ Dev Studio: "Bezalel, design the architecture"'}`,
|
|
626
|
+
` ${i.guide_example_dev_studio_3 || '🏗️ Dev Studio: "Oholiab, implement story S1"'}`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
if (selectedPacks.includes('memory')) {
|
|
630
|
+
examples.push(
|
|
631
|
+
` ${i.guide_example_memory_1 || '🧠 Memory: "Zecher, scan projects in ~/projects"'}`,
|
|
632
|
+
` ${i.guide_example_memory_2 || '🧠 Memory: "Zecher, where were we?"'}`,
|
|
633
|
+
` ${i.guide_example_memory_3 || '🧠 Memory: "Zecher, consolidate memory"'}`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (examples.length > 0) {
|
|
638
|
+
agentGuide.push('', `💡 ${i.guide_examples_title || 'Quick Examples'}:`, ...examples);
|
|
639
|
+
}
|
|
522
640
|
|
|
523
|
-
|
|
641
|
+
agentGuide.push('', '---', i.guide_credits);
|
|
524
642
|
|
|
525
|
-
|
|
643
|
+
clack.note(agentGuide.join('\n'), i.guide_title);
|
|
644
|
+
|
|
645
|
+
clack.outro(pc.green(i.guide_ready));
|
|
526
646
|
} finally {
|
|
527
647
|
releaseLock();
|
|
528
648
|
}
|
|
529
649
|
},
|
|
530
650
|
// Exported for functional testing (not part of the public API)
|
|
531
|
-
_internal: {
|
|
651
|
+
_internal: {
|
|
652
|
+
writeIDEConfigs,
|
|
653
|
+
generateConfigYaml,
|
|
654
|
+
mergeConfigYaml,
|
|
655
|
+
communicationLanguage,
|
|
656
|
+
installedVersionConflict,
|
|
657
|
+
},
|
|
532
658
|
};
|
|
533
659
|
|
|
534
|
-
function
|
|
535
|
-
|
|
536
|
-
return
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
660
|
+
function packLabel(id) {
|
|
661
|
+
const icon = DERIVED.packs[id]?.iconEmoji;
|
|
662
|
+
return (icon ? icon + ' ' : '') + PACKS[id].name;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Refuse any version change; `update` owns upgrades and downgrades are never silent. */
|
|
666
|
+
function installedVersionConflict(installed, executing) {
|
|
667
|
+
if (
|
|
668
|
+
!installed ||
|
|
669
|
+
installed === executing ||
|
|
670
|
+
!isExactVersion(installed) ||
|
|
671
|
+
!isExactVersion(executing)
|
|
672
|
+
)
|
|
673
|
+
return null;
|
|
674
|
+
if (semver.gt(installed, executing)) {
|
|
675
|
+
return (
|
|
676
|
+
`Downgrade refused: BMAD+ v${installed} is installed and this package is v${executing}. ` +
|
|
677
|
+
'Run a newer package (npx bmad-plus@latest install), or `bmad-plus update --latest` to check for updates.'
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
return (
|
|
681
|
+
`BMAD+ v${installed} is installed; this package is v${executing}. ` +
|
|
682
|
+
'Run `bmad-plus update` first (it backs up changed files and records a restorable receipt), ' +
|
|
683
|
+
'then re-run install to add packs or tools.'
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function parseList(value) {
|
|
688
|
+
return String(value)
|
|
689
|
+
.split(',')
|
|
690
|
+
.map((item) => item.trim())
|
|
691
|
+
.filter(Boolean);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** Explicit pack IDs; unknown IDs fail instead of being silently dropped. */
|
|
695
|
+
function parseRequestedPacks(value) {
|
|
696
|
+
if (value === undefined) return null;
|
|
697
|
+
const requested = parseList(value);
|
|
698
|
+
const available = Object.keys(PACKS).filter((id) => !PACKS[id].disabled);
|
|
699
|
+
if (requested.includes('all')) return available;
|
|
700
|
+
const unknown = requested.filter((id) => id !== 'none' && !available.includes(id));
|
|
701
|
+
if (unknown.length) {
|
|
702
|
+
throw new Error(
|
|
703
|
+
`Unknown pack ID(s): ${unknown.join(', ')}. Available: ${available.join(', ')}, all.`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
return requested.filter((id) => id !== 'none');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function parseRequestedTools(value) {
|
|
710
|
+
if (value === undefined) return null;
|
|
711
|
+
if (value === 'none' || value === 'skip') return [];
|
|
712
|
+
const available = Object.keys(IDE_CONFIGS);
|
|
713
|
+
const requested = parseList(value);
|
|
714
|
+
if (requested.includes('all')) return available;
|
|
715
|
+
const unknown = requested.filter((id) => !available.includes(id));
|
|
716
|
+
if (unknown.length) {
|
|
717
|
+
throw new Error(
|
|
718
|
+
`Unknown tool ID(s): ${unknown.join(', ')}. Available: ${available.join(', ')}, all, none.`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
return [...new Set(requested)];
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** Communication language name from --lang, then the system locale; English otherwise. */
|
|
725
|
+
function communicationLanguage(code, locale = Intl.DateTimeFormat().resolvedOptions().locale) {
|
|
726
|
+
const candidates = [code, locale, String(locale || '').split('-')[0]]
|
|
727
|
+
.filter(Boolean)
|
|
728
|
+
.map((value) => String(value).toLowerCase());
|
|
729
|
+
for (const candidate of candidates) {
|
|
730
|
+
if (Object.hasOwn(LANGUAGES, candidate)) return LANGUAGES[candidate].name;
|
|
731
|
+
}
|
|
732
|
+
return 'English';
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function readExistingConfig(projectDir) {
|
|
736
|
+
try {
|
|
737
|
+
const parsed = yaml.load(
|
|
738
|
+
fs.readFileSync(path.join(projectDir, '_bmad', 'config.yaml'), 'utf8')
|
|
739
|
+
);
|
|
740
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
741
|
+
} catch {
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const isPlainObject = (value) =>
|
|
747
|
+
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
748
|
+
|
|
749
|
+
/** Existing keys win, missing defaults are added; explicit answers replace. */
|
|
750
|
+
function mergeSettings(existing, defaults, explicit) {
|
|
751
|
+
const merged = { ...existing };
|
|
752
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
753
|
+
if (!(key in merged)) merged[key] = value;
|
|
754
|
+
else if (isPlainObject(merged[key]) && isPlainObject(value))
|
|
755
|
+
merged[key] = mergeSettings(merged[key], value, {});
|
|
756
|
+
}
|
|
757
|
+
for (const [key, value] of Object.entries(explicit)) {
|
|
758
|
+
merged[key] =
|
|
759
|
+
isPlainObject(value) && isPlainObject(merged[key])
|
|
760
|
+
? mergeSettings(merged[key], {}, value)
|
|
761
|
+
: value;
|
|
762
|
+
}
|
|
763
|
+
return merged;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** Returns null when the existing file is not a YAML mapping (left untouched). */
|
|
767
|
+
function mergeConfigYaml(existingText, generatedText, explicit = {}) {
|
|
768
|
+
let existing;
|
|
769
|
+
try {
|
|
770
|
+
existing = yaml.load(existingText);
|
|
771
|
+
} catch {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
if (!isPlainObject(existing)) return null;
|
|
775
|
+
const merged = mergeSettings(existing, yaml.load(generatedText), explicit);
|
|
776
|
+
if (isDeepStrictEqual(merged, existing)) return { changed: false, content: existingText };
|
|
777
|
+
return {
|
|
778
|
+
changed: true,
|
|
779
|
+
content:
|
|
780
|
+
'# BMAD+ Project Configuration\n# Generated by bmad-plus install; existing settings preserved\n\n' +
|
|
781
|
+
yaml.dump(merged, { lineWidth: -1, forceQuotes: true, quotingType: '"' }),
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function generateConfigYaml(
|
|
786
|
+
userName,
|
|
787
|
+
language,
|
|
788
|
+
projectDir,
|
|
789
|
+
execMode = 'manual',
|
|
790
|
+
uatEnvironment = ''
|
|
791
|
+
) {
|
|
792
|
+
if (!['manual', 'autopilot', 'hybrid'].includes(execMode))
|
|
793
|
+
throw new Error('Unknown execution mode: ' + execMode);
|
|
794
|
+
return (
|
|
795
|
+
'# BMAD+ Project Configuration\n# Generated by bmad-plus install\n\n' +
|
|
796
|
+
yaml.dump(
|
|
797
|
+
{
|
|
798
|
+
user_name: userName,
|
|
799
|
+
communication_language: language,
|
|
800
|
+
document_output_language: language,
|
|
801
|
+
output_folder: '_bmad-output',
|
|
802
|
+
project_name: path.basename(projectDir),
|
|
803
|
+
execution_mode: execMode,
|
|
804
|
+
auto_role_activation: true,
|
|
805
|
+
parallel_execution: true,
|
|
806
|
+
checkpoints: {
|
|
807
|
+
discovery: 'require_approval',
|
|
808
|
+
architecture: 'require_approval',
|
|
809
|
+
story: execMode === 'autopilot' ? 'notify_only' : 'require_approval',
|
|
810
|
+
delivery: 'require_approval',
|
|
811
|
+
},
|
|
812
|
+
// Human acceptance recipes. advisory: every delivery produces and offers its page and
|
|
813
|
+
// nothing is ever blocked. gate: the delivery waits for a passing "bmad-plus uat gate".
|
|
814
|
+
// off: no recipe, and the delivery report says so.
|
|
815
|
+
uat: {
|
|
816
|
+
mode: 'advisory',
|
|
817
|
+
granularity: 'delivery',
|
|
818
|
+
environment: { name: uatEnvironment ? 'TEST' : '', url: uatEnvironment || '' },
|
|
819
|
+
page_budget: { max_steps: 15, max_minutes: 30 },
|
|
820
|
+
dir: '_bmad-output/uat',
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
{ lineWidth: -1, forceQuotes: true, quotingType: '"' }
|
|
824
|
+
)
|
|
825
|
+
);
|
|
562
826
|
}
|