bmad-enhanced 1.1.2 → 1.3.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.
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const yaml = require('js-yaml');
6
+ const configMerger = require('./config-merger');
7
+
8
+ /**
9
+ * Validator for BMAD-Enhanced
10
+ * Verifies installation integrity post-migration
11
+ */
12
+
13
+ /**
14
+ * Validate entire installation
15
+ * @param {object} preM
16
+
17
+ igrationData - Data from before migration for comparison
18
+ * @returns {Promise<object>} Validation result
19
+ */
20
+ async function validateInstallation(preMigrationData = {}) {
21
+ const checks = [];
22
+
23
+ // 1. Config structure validation
24
+ checks.push(await validateConfigStructure());
25
+
26
+ // 2. Agent files validation
27
+ checks.push(await validateAgentFiles());
28
+
29
+ // 3. Workflow files validation
30
+ checks.push(await validateWorkflows());
31
+
32
+ // 4. Manifest consistency validation
33
+ checks.push(await validateManifest());
34
+
35
+ // 5. User data integrity validation
36
+ if (preMigrationData.userDataCount) {
37
+ checks.push(await validateUserDataIntegrity(preMigrationData.userDataCount));
38
+ }
39
+
40
+ // 6. Deprecated workflows validation (if applicable)
41
+ checks.push(await validateDeprecatedWorkflows());
42
+
43
+ const allPassed = checks.every(c => c.passed);
44
+
45
+ return {
46
+ valid: allPassed,
47
+ checks
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Validate config.yaml structure
53
+ * @returns {Promise<object>} Validation check result
54
+ */
55
+ async function validateConfigStructure() {
56
+ const check = {
57
+ name: 'Config structure',
58
+ passed: false,
59
+ error: null
60
+ };
61
+
62
+ try {
63
+ const configPath = path.join(process.cwd(), '_bmad/bme/_vortex/config.yaml');
64
+
65
+ if (!fs.existsSync(configPath)) {
66
+ check.error = 'config.yaml not found';
67
+ return check;
68
+ }
69
+
70
+ const configContent = fs.readFileSync(configPath, 'utf8');
71
+ const config = yaml.load(configContent);
72
+
73
+ // Validate using config-merger
74
+ const validation = configMerger.validateConfig(config);
75
+
76
+ if (!validation.valid) {
77
+ check.error = validation.errors.join(', ');
78
+ return check;
79
+ }
80
+
81
+ check.passed = true;
82
+ } catch (error) {
83
+ check.error = error.message;
84
+ }
85
+
86
+ return check;
87
+ }
88
+
89
+ /**
90
+ * Validate agent files exist
91
+ * @returns {Promise<object>} Validation check result
92
+ */
93
+ async function validateAgentFiles() {
94
+ const check = {
95
+ name: 'Agent files',
96
+ passed: false,
97
+ error: null
98
+ };
99
+
100
+ try {
101
+ const agentsDir = path.join(process.cwd(), '_bmad/bme/_vortex/agents');
102
+ const requiredAgents = [
103
+ 'contextualization-expert.md',
104
+ 'lean-experiments-specialist.md'
105
+ ];
106
+
107
+ if (!fs.existsSync(agentsDir)) {
108
+ check.error = 'agents/ directory not found';
109
+ return check;
110
+ }
111
+
112
+ const missingAgents = [];
113
+ for (const agent of requiredAgents) {
114
+ const agentPath = path.join(agentsDir, agent);
115
+ if (!fs.existsSync(agentPath)) {
116
+ missingAgents.push(agent);
117
+ }
118
+ }
119
+
120
+ if (missingAgents.length > 0) {
121
+ check.error = `Missing agent files: ${missingAgents.join(', ')}`;
122
+ return check;
123
+ }
124
+
125
+ check.passed = true;
126
+ } catch (error) {
127
+ check.error = error.message;
128
+ }
129
+
130
+ return check;
131
+ }
132
+
133
+ /**
134
+ * Validate workflow files exist
135
+ * @returns {Promise<object>} Validation check result
136
+ */
137
+ async function validateWorkflows() {
138
+ const check = {
139
+ name: 'Workflow files',
140
+ passed: false,
141
+ error: null
142
+ };
143
+
144
+ try {
145
+ const workflowsDir = path.join(process.cwd(), '_bmad/bme/_vortex/workflows');
146
+ const requiredWorkflows = [
147
+ 'lean-persona',
148
+ 'product-vision',
149
+ 'contextualize-scope',
150
+ 'mvp',
151
+ 'lean-experiment',
152
+ 'proof-of-concept',
153
+ 'proof-of-value'
154
+ ];
155
+
156
+ if (!fs.existsSync(workflowsDir)) {
157
+ check.error = 'workflows/ directory not found';
158
+ return check;
159
+ }
160
+
161
+ const missingWorkflows = [];
162
+ for (const workflow of requiredWorkflows) {
163
+ const workflowFile = path.join(workflowsDir, workflow, 'workflow.md');
164
+ if (!fs.existsSync(workflowFile)) {
165
+ missingWorkflows.push(workflow);
166
+ }
167
+ }
168
+
169
+ if (missingWorkflows.length > 0) {
170
+ check.error = `Missing workflows: ${missingWorkflows.join(', ')}`;
171
+ return check;
172
+ }
173
+
174
+ check.passed = true;
175
+ } catch (error) {
176
+ check.error = error.message;
177
+ }
178
+
179
+ return check;
180
+ }
181
+
182
+ /**
183
+ * Validate agent manifest consistency
184
+ * @returns {Promise<object>} Validation check result
185
+ */
186
+ async function validateManifest() {
187
+ const check = {
188
+ name: 'Agent manifest',
189
+ passed: false,
190
+ error: null
191
+ };
192
+
193
+ try {
194
+ const manifestPath = path.join(process.cwd(), '_bmad/_config/agent-manifest.csv');
195
+
196
+ if (!fs.existsSync(manifestPath)) {
197
+ // Manifest is optional, so this is not a failure
198
+ check.passed = true;
199
+ check.warning = 'agent-manifest.csv not found (optional)';
200
+ return check;
201
+ }
202
+
203
+ const manifestContent = fs.readFileSync(manifestPath, 'utf8');
204
+
205
+ // Check for BMAD-Enhanced agents
206
+ const hasEmma = manifestContent.includes('contextualization-expert');
207
+ const hasWade = manifestContent.includes('lean-experiments-specialist');
208
+
209
+ if (!hasEmma || !hasWade) {
210
+ check.error = 'Agent manifest missing BMAD-Enhanced agents';
211
+ return check;
212
+ }
213
+
214
+ check.passed = true;
215
+ } catch (error) {
216
+ check.error = error.message;
217
+ }
218
+
219
+ return check;
220
+ }
221
+
222
+ /**
223
+ * Validate user data integrity
224
+ * @param {number} expectedCount - Expected file count
225
+ * @returns {Promise<object>} Validation check result
226
+ */
227
+ async function validateUserDataIntegrity(expectedCount) {
228
+ const check = {
229
+ name: 'User data preserved',
230
+ passed: false,
231
+ error: null
232
+ };
233
+
234
+ try {
235
+ const outputDir = path.join(process.cwd(), '_bmad-output');
236
+
237
+ if (!fs.existsSync(outputDir)) {
238
+ check.error = '_bmad-output/ directory not found';
239
+ return check;
240
+ }
241
+
242
+ const currentCount = await countUserDataFiles(outputDir);
243
+
244
+ // Allow for slight variation (user guides may have been updated)
245
+ if (currentCount >= expectedCount - 2) {
246
+ check.passed = true;
247
+ check.info = `Files: ${currentCount} (expected: ${expectedCount})`;
248
+ } else {
249
+ check.error = `User data count mismatch: ${currentCount} (expected: ${expectedCount})`;
250
+ }
251
+ } catch (error) {
252
+ check.error = error.message;
253
+ }
254
+
255
+ return check;
256
+ }
257
+
258
+ /**
259
+ * Validate deprecated workflows structure
260
+ * @returns {Promise<object>} Validation check result
261
+ */
262
+ async function validateDeprecatedWorkflows() {
263
+ const check = {
264
+ name: 'Deprecated workflows',
265
+ passed: true, // Not required, so pass by default
266
+ error: null
267
+ };
268
+
269
+ try {
270
+ const deprecatedDir = path.join(process.cwd(), '_bmad/bme/_vortex/workflows/_deprecated');
271
+
272
+ if (!fs.existsSync(deprecatedDir)) {
273
+ check.info = 'No deprecated workflows (fresh installation)';
274
+ return check;
275
+ }
276
+
277
+ // If deprecated dir exists, check for expected workflows
278
+ const empathyMapDir = path.join(deprecatedDir, 'empathy-map');
279
+ const wireframeDir = path.join(deprecatedDir, 'wireframe');
280
+
281
+ if (!fs.existsSync(empathyMapDir) && !fs.existsSync(wireframeDir)) {
282
+ check.warning = '_deprecated/ directory exists but is empty';
283
+ } else {
284
+ check.info = 'Deprecated workflows preserved in _deprecated/';
285
+ }
286
+
287
+ check.passed = true;
288
+ } catch (error) {
289
+ check.error = error.message;
290
+ check.passed = false;
291
+ }
292
+
293
+ return check;
294
+ }
295
+
296
+ /**
297
+ * Count user data files in _bmad-output
298
+ * @param {string} outputDir - Output directory path
299
+ * @returns {Promise<number>} File count
300
+ */
301
+ async function countUserDataFiles(outputDir) {
302
+ let count = 0;
303
+
304
+ async function countRecursive(dir) {
305
+ const entries = await fs.readdir(dir, { withFileTypes: true });
306
+
307
+ for (const entry of entries) {
308
+ const fullPath = path.join(dir, entry.name);
309
+
310
+ // Skip .backups and .logs directories
311
+ if (entry.name === '.backups' || entry.name === '.logs') {
312
+ continue;
313
+ }
314
+
315
+ if (entry.isDirectory()) {
316
+ await countRecursive(fullPath);
317
+ } else if (entry.isFile()) {
318
+ count++;
319
+ }
320
+ }
321
+ }
322
+
323
+ await countRecursive(outputDir);
324
+ return count;
325
+ }
326
+
327
+ /**
328
+ * Run smoke tests after migration
329
+ * @returns {Promise<object>} Smoke test result
330
+ */
331
+ async function runSmokeTests() {
332
+ const tests = [];
333
+
334
+ // Test 1: Can read Emma agent file
335
+ tests.push(await testReadAgentFile('contextualization-expert.md', 'Emma'));
336
+
337
+ // Test 2: Can read Wade agent file
338
+ tests.push(await testReadAgentFile('lean-experiments-specialist.md', 'Wade'));
339
+
340
+ // Test 3: Can read a workflow template
341
+ tests.push(await testReadWorkflowTemplate('lean-persona'));
342
+
343
+ // Test 4: Config.yaml is parseable
344
+ tests.push(await testConfigParseable());
345
+
346
+ const allPassed = tests.every(t => t.passed);
347
+
348
+ return {
349
+ passed: allPassed,
350
+ tests
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Test reading an agent file
356
+ */
357
+ async function testReadAgentFile(filename, agentName) {
358
+ const test = { name: `Read ${agentName} agent file`, passed: false };
359
+
360
+ try {
361
+ const agentPath = path.join(process.cwd(), '_bmad/bme/_vortex/agents', filename);
362
+ const content = await fs.readFile(agentPath, 'utf8');
363
+
364
+ if (content.length > 0) {
365
+ test.passed = true;
366
+ }
367
+ } catch (error) {
368
+ test.error = error.message;
369
+ }
370
+
371
+ return test;
372
+ }
373
+
374
+ /**
375
+ * Test reading a workflow template
376
+ */
377
+ async function testReadWorkflowTemplate(workflowName) {
378
+ const test = { name: `Read ${workflowName} template`, passed: false };
379
+
380
+ try {
381
+ const templatePath = path.join(
382
+ process.cwd(),
383
+ '_bmad/bme/_vortex/workflows',
384
+ workflowName,
385
+ `${workflowName}.template.md`
386
+ );
387
+
388
+ const content = await fs.readFile(templatePath, 'utf8');
389
+
390
+ if (content.length > 0) {
391
+ test.passed = true;
392
+ }
393
+ } catch (error) {
394
+ test.error = error.message;
395
+ }
396
+
397
+ return test;
398
+ }
399
+
400
+ /**
401
+ * Test config.yaml parseability
402
+ */
403
+ async function testConfigParseable() {
404
+ const test = { name: 'Parse config.yaml', passed: false };
405
+
406
+ try {
407
+ const configPath = path.join(process.cwd(), '_bmad/bme/_vortex/config.yaml');
408
+ const content = await fs.readFile(configPath, 'utf8');
409
+ const config = yaml.load(content);
410
+
411
+ if (config && config.version) {
412
+ test.passed = true;
413
+ }
414
+ } catch (error) {
415
+ test.error = error.message;
416
+ }
417
+
418
+ return test;
419
+ }
420
+
421
+ module.exports = {
422
+ validateInstallation,
423
+ validateConfigStructure,
424
+ validateAgentFiles,
425
+ validateWorkflows,
426
+ validateManifest,
427
+ validateUserDataIntegrity,
428
+ validateDeprecatedWorkflows,
429
+ runSmokeTests
430
+ };
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const yaml = require('js-yaml');
6
+
7
+ /**
8
+ * Version Detector for BMAD-Enhanced
9
+ * Reliably detects current installed version and determines migration path
10
+ */
11
+
12
+ /**
13
+ * Get current installed version from config.yaml
14
+ * @returns {string|null} Version string or null if not found
15
+ */
16
+ function getCurrentVersion() {
17
+ try {
18
+ const configPath = path.join(process.cwd(), '_bmad/bme/_vortex/config.yaml');
19
+
20
+ if (!fs.existsSync(configPath)) {
21
+ return null; // No config = fresh install
22
+ }
23
+
24
+ const configContent = fs.readFileSync(configPath, 'utf8');
25
+ const config = yaml.load(configContent);
26
+
27
+ if (config && config.version) {
28
+ return config.version;
29
+ }
30
+
31
+ // Fallback: Check for deprecated folder structure to guess version
32
+ console.warn('Warning: config.yaml exists but has no version field');
33
+ return guessVersionFromFileStructure();
34
+
35
+ } catch (error) {
36
+ console.warn('Warning: Could not read config.yaml:', error.message);
37
+ return guessVersionFromFileStructure();
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Guess version from file structure (fallback method)
43
+ * @returns {string|null} Guessed version or null
44
+ */
45
+ function guessVersionFromFileStructure() {
46
+ const vortexDir = path.join(process.cwd(), '_bmad/bme/_vortex');
47
+
48
+ if (!fs.existsSync(vortexDir)) {
49
+ return null; // No installation
50
+ }
51
+
52
+ // Check for deprecated folder (exists in v1.1.0+)
53
+ const deprecatedDir = path.join(vortexDir, 'workflows/_deprecated');
54
+ if (fs.existsSync(deprecatedDir)) {
55
+ return '1.1.0'; // Has deprecated folder = v1.1.0+
56
+ }
57
+
58
+ // Check for old empathy-map workflow (v1.0.0)
59
+ const empathyMapDir = path.join(vortexDir, 'workflows/empathy-map');
60
+ if (fs.existsSync(empathyMapDir)) {
61
+ return '1.0.0'; // Has empathy-map in workflows = v1.0.0
62
+ }
63
+
64
+ // Has vortex dir but can't determine version
65
+ return '1.0.0'; // Default to oldest version
66
+ }
67
+
68
+ /**
69
+ * Get target version from package.json
70
+ * @returns {string} Target version
71
+ */
72
+ function getTargetVersion() {
73
+ try {
74
+ const packagePath = path.join(__dirname, '../../../package.json');
75
+ const packageContent = fs.readFileSync(packagePath, 'utf8');
76
+ const packageJson = JSON.parse(packageContent);
77
+
78
+ return packageJson.version;
79
+ } catch (error) {
80
+ throw new Error(`Could not read package.json: ${error.message}`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Determine migration path based on versions
86
+ * @param {string|null} currentVersion - Current installed version
87
+ * @param {string} targetVersion - Target version from package
88
+ * @returns {object} Migration path info
89
+ */
90
+ function getMigrationPath(currentVersion, targetVersion) {
91
+ // Fresh install (no current version)
92
+ if (!currentVersion) {
93
+ return {
94
+ type: 'fresh-install',
95
+ needsMigration: false,
96
+ breaking: false
97
+ };
98
+ }
99
+
100
+ // Already up to date
101
+ if (currentVersion === targetVersion) {
102
+ return {
103
+ type: 'up-to-date',
104
+ needsMigration: false,
105
+ breaking: false
106
+ };
107
+ }
108
+
109
+ // Downgrade attempt (not supported)
110
+ if (compareVersions(currentVersion, targetVersion) > 0) {
111
+ return {
112
+ type: 'downgrade',
113
+ needsMigration: false,
114
+ breaking: false,
115
+ fromVersion: currentVersion,
116
+ toVersion: targetVersion
117
+ };
118
+ }
119
+
120
+ // Upgrade needed
121
+ const breaking = isBreakingChange(currentVersion, targetVersion);
122
+
123
+ return {
124
+ type: 'upgrade-needed',
125
+ needsMigration: true,
126
+ breaking,
127
+ fromVersion: currentVersion,
128
+ toVersion: targetVersion
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Detect installation scenario
134
+ * @returns {string} Scenario: 'fresh' | 'partial' | 'complete' | 'corrupted'
135
+ */
136
+ function detectInstallationScenario() {
137
+ const vortexDir = path.join(process.cwd(), '_bmad/bme/_vortex');
138
+ const configPath = path.join(vortexDir, 'config.yaml');
139
+ const agentsDir = path.join(vortexDir, 'agents');
140
+ const workflowsDir = path.join(vortexDir, 'workflows');
141
+
142
+ // No vortex directory = fresh install
143
+ if (!fs.existsSync(vortexDir)) {
144
+ return 'fresh';
145
+ }
146
+
147
+ // No config.yaml = partial installation
148
+ if (!fs.existsSync(configPath)) {
149
+ return 'partial';
150
+ }
151
+
152
+ // Check agent files
153
+ const requiredAgents = [
154
+ 'contextualization-expert.md',
155
+ 'lean-experiments-specialist.md'
156
+ ];
157
+
158
+ if (!fs.existsSync(agentsDir)) {
159
+ return 'corrupted';
160
+ }
161
+
162
+ const missingAgents = requiredAgents.filter(agent =>
163
+ !fs.existsSync(path.join(agentsDir, agent))
164
+ );
165
+
166
+ if (missingAgents.length > 0) {
167
+ return 'corrupted';
168
+ }
169
+
170
+ // Check workflows directory
171
+ if (!fs.existsSync(workflowsDir)) {
172
+ return 'corrupted';
173
+ }
174
+
175
+ return 'complete';
176
+ }
177
+
178
+ /**
179
+ * Compare two semantic versions
180
+ * @param {string} v1 - First version
181
+ * @param {string} v2 - Second version
182
+ * @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
183
+ */
184
+ function compareVersions(v1, v2) {
185
+ const parts1 = v1.split('.').map(Number);
186
+ const parts2 = v2.split('.').map(Number);
187
+
188
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
189
+ const part1 = parts1[i] || 0;
190
+ const part2 = parts2[i] || 0;
191
+
192
+ if (part1 < part2) return -1;
193
+ if (part1 > part2) return 1;
194
+ }
195
+
196
+ return 0;
197
+ }
198
+
199
+ /**
200
+ * Determine if upgrade involves breaking changes
201
+ * @param {string} fromVersion - Current version
202
+ * @param {string} toVersion - Target version
203
+ * @returns {boolean} True if breaking changes exist
204
+ */
205
+ function isBreakingChange(fromVersion, toVersion) {
206
+ // v1.0.x → any later version = breaking (empathy-map removed)
207
+ if (fromVersion.startsWith('1.0.')) {
208
+ return true;
209
+ }
210
+
211
+ // v1.1.x → v1.2.0 = not breaking (just updates)
212
+ if (fromVersion.startsWith('1.1.') && toVersion.startsWith('1.2.')) {
213
+ return false;
214
+ }
215
+
216
+ // Major version change = breaking
217
+ const fromMajor = parseInt(fromVersion.split('.')[0]);
218
+ const toMajor = parseInt(toVersion.split('.')[0]);
219
+
220
+ if (fromMajor < toMajor) {
221
+ return true;
222
+ }
223
+
224
+ return false;
225
+ }
226
+
227
+ /**
228
+ * Get version range pattern for migrations (e.g., "1.0.x")
229
+ * @param {string} version - Full version string
230
+ * @returns {string} Version range pattern
231
+ */
232
+ function getVersionRange(version) {
233
+ const parts = version.split('.');
234
+ return `${parts[0]}.${parts[1]}.x`;
235
+ }
236
+
237
+ module.exports = {
238
+ getCurrentVersion,
239
+ getTargetVersion,
240
+ getMigrationPath,
241
+ detectInstallationScenario,
242
+ compareVersions,
243
+ isBreakingChange,
244
+ getVersionRange,
245
+ guessVersionFromFileStructure
246
+ };