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.
- package/README.md +178 -58
- package/package.json +7 -3
- package/scripts/install-all-agents.js +10 -12
- package/scripts/install-emma.js +5 -5
- package/scripts/install-wade.js +5 -5
- package/scripts/postinstall.js +100 -11
- package/scripts/update/bmad-migrate.js +116 -0
- package/scripts/update/bmad-update.js +206 -0
- package/scripts/update/bmad-version.js +94 -0
- package/scripts/update/lib/backup-manager.js +279 -0
- package/scripts/update/lib/config-merger.js +232 -0
- package/scripts/update/lib/migration-runner.js +402 -0
- package/scripts/update/lib/validator.js +430 -0
- package/scripts/update/lib/version-detector.js +246 -0
- package/scripts/update/migrations/1.0.x-to-1.3.0.js +265 -0
- package/scripts/update/migrations/1.1.x-to-1.3.0.js +166 -0
- package/scripts/update/migrations/1.2.x-to-1.3.0.js +166 -0
- package/scripts/update/migrations/registry.js +185 -0
|
@@ -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
|
+
};
|