bmad-fh 6.0.0-alpha.23 → 6.0.0-alpha.23.3b00cb36
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/.github/workflows/publish.yaml +68 -0
- package/.husky/pre-commit +17 -2
- package/.husky/pre-push +10 -0
- package/README.md +117 -14
- package/eslint.config.mjs +2 -2
- package/package.json +1 -2
- package/src/bmm/module.yaml +2 -2
- package/src/core/lib/scope/artifact-resolver.js +26 -26
- package/src/core/lib/scope/event-logger.js +34 -45
- package/src/core/lib/scope/index.js +3 -3
- package/src/core/lib/scope/scope-context.js +22 -28
- package/src/core/lib/scope/scope-initializer.js +29 -31
- package/src/core/lib/scope/scope-manager.js +57 -24
- package/src/core/lib/scope/scope-migrator.js +44 -52
- package/src/core/lib/scope/scope-sync.js +42 -48
- package/src/core/lib/scope/scope-validator.js +16 -21
- package/src/core/lib/scope/state-lock.js +37 -43
- package/src/core/module.yaml +2 -2
- package/test/test-scope-cli.js +1306 -0
- package/test/test-scope-e2e.js +682 -92
- package/test/test-scope-system.js +973 -169
- package/tools/cli/bmad-cli.js +5 -0
- package/tools/cli/commands/scope.js +1250 -115
- package/tools/cli/installers/lib/modules/manager.js +6 -2
- package/tools/cli/scripts/migrate-workflows.js +43 -51
- package/.github/workflows/publish-multi-artifact.yaml +0 -50
|
@@ -2,16 +2,17 @@ const path = require('node:path');
|
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
3
|
const yaml = require('yaml');
|
|
4
4
|
const { ScopeValidator } = require('./scope-validator');
|
|
5
|
+
const { ScopeInitializer } = require('./scope-initializer');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Manages scope lifecycle and CRUD operations
|
|
8
9
|
* Handles scope configuration in scopes.yaml file
|
|
9
|
-
*
|
|
10
|
+
*
|
|
10
11
|
* @class ScopeManager
|
|
11
12
|
* @requires fs-extra
|
|
12
13
|
* @requires yaml
|
|
13
14
|
* @requires ScopeValidator
|
|
14
|
-
*
|
|
15
|
+
*
|
|
15
16
|
* @example
|
|
16
17
|
* const manager = new ScopeManager({ projectRoot: '/path/to/project' });
|
|
17
18
|
* await manager.initialize();
|
|
@@ -23,8 +24,9 @@ class ScopeManager {
|
|
|
23
24
|
this.bmadPath = options.bmadPath || path.join(this.projectRoot, '_bmad');
|
|
24
25
|
this.configPath = options.configPath || path.join(this.bmadPath, '_config');
|
|
25
26
|
this.scopesFilePath = options.scopesFilePath || path.join(this.configPath, 'scopes.yaml');
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
this.validator = new ScopeValidator();
|
|
29
|
+
this.initializer = new ScopeInitializer({ projectRoot: this.projectRoot });
|
|
28
30
|
this._config = null; // Cached configuration
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -38,6 +40,7 @@ class ScopeManager {
|
|
|
38
40
|
this.configPath = path.join(this.bmadPath, '_config');
|
|
39
41
|
this.scopesFilePath = path.join(this.configPath, 'scopes.yaml');
|
|
40
42
|
this._config = null; // Clear cache
|
|
43
|
+
this.initializer.setProjectRoot(projectRoot);
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/**
|
|
@@ -52,13 +55,16 @@ class ScopeManager {
|
|
|
52
55
|
|
|
53
56
|
// Check if scopes.yaml exists
|
|
54
57
|
const exists = await fs.pathExists(this.scopesFilePath);
|
|
55
|
-
|
|
58
|
+
|
|
56
59
|
if (!exists) {
|
|
57
60
|
// Create default configuration
|
|
58
61
|
const defaultConfig = this.validator.createDefaultConfig();
|
|
59
62
|
await this.saveConfig(defaultConfig);
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
// Initialize scope system directories (_shared, _events)
|
|
66
|
+
await this.initializer.initializeScopeSystem();
|
|
67
|
+
|
|
62
68
|
// Load and validate configuration
|
|
63
69
|
const config = await this.loadConfig();
|
|
64
70
|
return config !== null;
|
|
@@ -122,7 +128,7 @@ class ScopeManager {
|
|
|
122
128
|
// Write to file
|
|
123
129
|
const yamlContent = yaml.stringify(config, {
|
|
124
130
|
indent: 2,
|
|
125
|
-
lineWidth: 100
|
|
131
|
+
lineWidth: 100,
|
|
126
132
|
});
|
|
127
133
|
await fs.writeFile(this.scopesFilePath, yamlContent, 'utf8');
|
|
128
134
|
|
|
@@ -146,7 +152,7 @@ class ScopeManager {
|
|
|
146
152
|
|
|
147
153
|
// Apply filters
|
|
148
154
|
if (filters.status) {
|
|
149
|
-
scopes = scopes.filter(scope => scope.status === filters.status);
|
|
155
|
+
scopes = scopes.filter((scope) => scope.status === filters.status);
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
// Sort by created date (newest first)
|
|
@@ -223,8 +229,8 @@ class ScopeManager {
|
|
|
223
229
|
created: new Date().toISOString(),
|
|
224
230
|
_meta: {
|
|
225
231
|
last_activity: new Date().toISOString(),
|
|
226
|
-
artifact_count: 0
|
|
227
|
-
}
|
|
232
|
+
artifact_count: 0,
|
|
233
|
+
},
|
|
228
234
|
};
|
|
229
235
|
|
|
230
236
|
// Validate scope with existing scopes
|
|
@@ -239,6 +245,9 @@ class ScopeManager {
|
|
|
239
245
|
// Save configuration
|
|
240
246
|
await this.saveConfig(config);
|
|
241
247
|
|
|
248
|
+
// Create scope directory structure
|
|
249
|
+
await this.initializer.initializeScope(scopeId, options);
|
|
250
|
+
|
|
242
251
|
return scope;
|
|
243
252
|
} catch (error) {
|
|
244
253
|
throw new Error(`Failed to create scope '${scopeId}': ${error.message}`);
|
|
@@ -272,8 +281,8 @@ class ScopeManager {
|
|
|
272
281
|
_meta: {
|
|
273
282
|
...currentScope._meta,
|
|
274
283
|
...updates._meta,
|
|
275
|
-
last_activity: new Date().toISOString()
|
|
276
|
-
}
|
|
284
|
+
last_activity: new Date().toISOString(),
|
|
285
|
+
},
|
|
277
286
|
};
|
|
278
287
|
|
|
279
288
|
// Validate updated scope
|
|
@@ -311,10 +320,10 @@ class ScopeManager {
|
|
|
311
320
|
}
|
|
312
321
|
|
|
313
322
|
// Check if other scopes depend on this one
|
|
314
|
-
const dependentScopes = this.
|
|
323
|
+
const dependentScopes = this.findDependentScopesSync(scopeId, config.scopes);
|
|
315
324
|
if (dependentScopes.length > 0 && !options.force) {
|
|
316
325
|
throw new Error(
|
|
317
|
-
`Cannot remove scope '${scopeId}'. The following scopes depend on it: ${dependentScopes.join(', ')}. Use force option to remove anyway
|
|
326
|
+
`Cannot remove scope '${scopeId}'. The following scopes depend on it: ${dependentScopes.join(', ')}. Use force option to remove anyway.`,
|
|
318
327
|
);
|
|
319
328
|
}
|
|
320
329
|
|
|
@@ -325,7 +334,7 @@ class ScopeManager {
|
|
|
325
334
|
if (options.force && dependentScopes.length > 0) {
|
|
326
335
|
for (const depScopeId of dependentScopes) {
|
|
327
336
|
const depScope = config.scopes[depScopeId];
|
|
328
|
-
depScope.dependencies = depScope.dependencies.filter(dep => dep !== scopeId);
|
|
337
|
+
depScope.dependencies = depScope.dependencies.filter((dep) => dep !== scopeId);
|
|
329
338
|
}
|
|
330
339
|
}
|
|
331
340
|
|
|
@@ -347,7 +356,7 @@ class ScopeManager {
|
|
|
347
356
|
try {
|
|
348
357
|
const config = await this.loadConfig();
|
|
349
358
|
const scope = config.scopes[scopeId];
|
|
350
|
-
|
|
359
|
+
|
|
351
360
|
if (!scope) {
|
|
352
361
|
throw new Error(`Scope '${scopeId}' does not exist`);
|
|
353
362
|
}
|
|
@@ -360,7 +369,7 @@ class ScopeManager {
|
|
|
360
369
|
planning: path.join(scopePath, 'planning-artifacts'),
|
|
361
370
|
implementation: path.join(scopePath, 'implementation-artifacts'),
|
|
362
371
|
tests: path.join(scopePath, 'tests'),
|
|
363
|
-
meta: path.join(scopePath, '.scope-meta.yaml')
|
|
372
|
+
meta: path.join(scopePath, '.scope-meta.yaml'),
|
|
364
373
|
};
|
|
365
374
|
} catch (error) {
|
|
366
375
|
throw new Error(`Failed to get scope paths for '${scopeId}': ${error.message}`);
|
|
@@ -388,7 +397,7 @@ class ScopeManager {
|
|
|
388
397
|
try {
|
|
389
398
|
const config = await this.loadConfig();
|
|
390
399
|
const scope = config.scopes[scopeId];
|
|
391
|
-
|
|
400
|
+
|
|
392
401
|
if (!scope) {
|
|
393
402
|
throw new Error(`Scope '${scopeId}' does not exist`);
|
|
394
403
|
}
|
|
@@ -396,7 +405,7 @@ class ScopeManager {
|
|
|
396
405
|
const tree = {
|
|
397
406
|
scope: scopeId,
|
|
398
407
|
dependencies: [],
|
|
399
|
-
dependents: this.
|
|
408
|
+
dependents: this.findDependentScopesSync(scopeId, config.scopes),
|
|
400
409
|
};
|
|
401
410
|
|
|
402
411
|
// Build dependency tree recursively
|
|
@@ -407,7 +416,7 @@ class ScopeManager {
|
|
|
407
416
|
tree.dependencies.push({
|
|
408
417
|
scope: depId,
|
|
409
418
|
name: depScope.name,
|
|
410
|
-
status: depScope.status
|
|
419
|
+
status: depScope.status,
|
|
411
420
|
});
|
|
412
421
|
}
|
|
413
422
|
}
|
|
@@ -422,12 +431,36 @@ class ScopeManager {
|
|
|
422
431
|
/**
|
|
423
432
|
* Find scopes that depend on a given scope
|
|
424
433
|
* @param {string} scopeId - The scope ID
|
|
425
|
-
* @param {object} allScopes - All scopes object
|
|
434
|
+
* @param {object} allScopes - All scopes object (optional, will load if not provided)
|
|
435
|
+
* @returns {Promise<string[]>|string[]} Array of dependent scope IDs
|
|
436
|
+
*/
|
|
437
|
+
async findDependentScopes(scopeId, allScopes = null) {
|
|
438
|
+
// If allScopes not provided, load from config
|
|
439
|
+
if (!allScopes) {
|
|
440
|
+
const config = await this.loadConfig();
|
|
441
|
+
allScopes = config.scopes || {};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const dependents = [];
|
|
445
|
+
|
|
446
|
+
for (const [sid, scope] of Object.entries(allScopes)) {
|
|
447
|
+
if (scope.dependencies && scope.dependencies.includes(scopeId)) {
|
|
448
|
+
dependents.push(sid);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return dependents;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Find scopes that depend on a given scope (synchronous version)
|
|
457
|
+
* @param {string} scopeId - The scope ID
|
|
458
|
+
* @param {object} allScopes - All scopes object (required)
|
|
426
459
|
* @returns {string[]} Array of dependent scope IDs
|
|
427
460
|
*/
|
|
428
|
-
|
|
461
|
+
findDependentScopesSync(scopeId, allScopes) {
|
|
429
462
|
const dependents = [];
|
|
430
|
-
|
|
463
|
+
|
|
431
464
|
for (const [sid, scope] of Object.entries(allScopes)) {
|
|
432
465
|
if (scope.dependencies && scope.dependencies.includes(scopeId)) {
|
|
433
466
|
dependents.push(sid);
|
|
@@ -462,7 +495,7 @@ class ScopeManager {
|
|
|
462
495
|
*/
|
|
463
496
|
async touchScope(scopeId) {
|
|
464
497
|
return this.updateScope(scopeId, {
|
|
465
|
-
_meta: { last_activity: new Date().toISOString() }
|
|
498
|
+
_meta: { last_activity: new Date().toISOString() },
|
|
466
499
|
});
|
|
467
500
|
}
|
|
468
501
|
|
|
@@ -480,7 +513,7 @@ class ScopeManager {
|
|
|
480
513
|
|
|
481
514
|
const currentCount = scope._meta?.artifact_count || 0;
|
|
482
515
|
return this.updateScope(scopeId, {
|
|
483
|
-
_meta: { artifact_count: currentCount + increment }
|
|
516
|
+
_meta: { artifact_count: currentCount + increment },
|
|
484
517
|
});
|
|
485
518
|
}
|
|
486
519
|
|
|
@@ -502,7 +535,7 @@ class ScopeManager {
|
|
|
502
535
|
const config = await this.loadConfig();
|
|
503
536
|
config.settings = {
|
|
504
537
|
...config.settings,
|
|
505
|
-
...settings
|
|
538
|
+
...settings,
|
|
506
539
|
};
|
|
507
540
|
await this.saveConfig(config);
|
|
508
541
|
return config.settings;
|
|
@@ -5,11 +5,11 @@ const yaml = require('yaml');
|
|
|
5
5
|
/**
|
|
6
6
|
* Migrates existing artifacts to scoped structure
|
|
7
7
|
* Handles migration of legacy non-scoped installations
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* @class ScopeMigrator
|
|
10
10
|
* @requires fs-extra
|
|
11
11
|
* @requires yaml
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* @example
|
|
14
14
|
* const migrator = new ScopeMigrator({ projectRoot: '/path/to/project' });
|
|
15
15
|
* await migrator.migrate();
|
|
@@ -41,18 +41,14 @@ class ScopeMigrator {
|
|
|
41
41
|
async needsMigration() {
|
|
42
42
|
try {
|
|
43
43
|
// Check if output directory exists
|
|
44
|
-
if (!await fs.pathExists(this.outputPath)) {
|
|
44
|
+
if (!(await fs.pathExists(this.outputPath))) {
|
|
45
45
|
return false;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Check for legacy structure indicators
|
|
49
|
-
const hasLegacyPlanning = await fs.pathExists(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const hasLegacyImplementation = await fs.pathExists(
|
|
53
|
-
path.join(this.outputPath, 'implementation-artifacts')
|
|
54
|
-
);
|
|
55
|
-
|
|
49
|
+
const hasLegacyPlanning = await fs.pathExists(path.join(this.outputPath, 'planning-artifacts'));
|
|
50
|
+
const hasLegacyImplementation = await fs.pathExists(path.join(this.outputPath, 'implementation-artifacts'));
|
|
51
|
+
|
|
56
52
|
// Check if already migrated (scopes.yaml exists and has scopes)
|
|
57
53
|
const scopesYamlPath = path.join(this.bmadPath, '_config', 'scopes.yaml');
|
|
58
54
|
if (await fs.pathExists(scopesYamlPath)) {
|
|
@@ -80,23 +76,19 @@ class ScopeMigrator {
|
|
|
80
76
|
directories: [],
|
|
81
77
|
files: [],
|
|
82
78
|
totalSize: 0,
|
|
83
|
-
suggestedScope: this.defaultScopeId
|
|
79
|
+
suggestedScope: this.defaultScopeId,
|
|
84
80
|
};
|
|
85
81
|
|
|
86
82
|
try {
|
|
87
83
|
// Check for legacy directories
|
|
88
|
-
const legacyDirs = [
|
|
89
|
-
'planning-artifacts',
|
|
90
|
-
'implementation-artifacts',
|
|
91
|
-
'tests'
|
|
92
|
-
];
|
|
84
|
+
const legacyDirs = ['planning-artifacts', 'implementation-artifacts', 'tests'];
|
|
93
85
|
|
|
94
86
|
for (const dir of legacyDirs) {
|
|
95
87
|
const dirPath = path.join(this.outputPath, dir);
|
|
96
88
|
if (await fs.pathExists(dirPath)) {
|
|
97
89
|
analysis.hasLegacyArtifacts = true;
|
|
98
90
|
analysis.directories.push(dir);
|
|
99
|
-
|
|
91
|
+
|
|
100
92
|
// Count files and size
|
|
101
93
|
const stats = await this.getDirStats(dirPath);
|
|
102
94
|
analysis.files.push(...stats.files);
|
|
@@ -115,7 +107,6 @@ class ScopeMigrator {
|
|
|
115
107
|
analysis.totalSize += stat.size;
|
|
116
108
|
}
|
|
117
109
|
}
|
|
118
|
-
|
|
119
110
|
} catch (error) {
|
|
120
111
|
throw new Error(`Failed to analyze existing artifacts: ${error.message}`);
|
|
121
112
|
}
|
|
@@ -130,16 +121,16 @@ class ScopeMigrator {
|
|
|
130
121
|
*/
|
|
131
122
|
async getDirStats(dirPath) {
|
|
132
123
|
const stats = { files: [], size: 0 };
|
|
133
|
-
|
|
124
|
+
|
|
134
125
|
try {
|
|
135
126
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
136
|
-
|
|
127
|
+
|
|
137
128
|
for (const entry of entries) {
|
|
138
129
|
const fullPath = path.join(dirPath, entry.name);
|
|
139
|
-
|
|
130
|
+
|
|
140
131
|
if (entry.isDirectory()) {
|
|
141
132
|
const subStats = await this.getDirStats(fullPath);
|
|
142
|
-
stats.files.push(...subStats.files.map(f => path.join(entry.name, f)));
|
|
133
|
+
stats.files.push(...subStats.files.map((f) => path.join(entry.name, f)));
|
|
143
134
|
stats.size += subStats.size;
|
|
144
135
|
} else {
|
|
145
136
|
stats.files.push(entry.name);
|
|
@@ -150,7 +141,7 @@ class ScopeMigrator {
|
|
|
150
141
|
} catch {
|
|
151
142
|
// Ignore permission errors
|
|
152
143
|
}
|
|
153
|
-
|
|
144
|
+
|
|
154
145
|
return stats;
|
|
155
146
|
}
|
|
156
147
|
|
|
@@ -161,10 +152,10 @@ class ScopeMigrator {
|
|
|
161
152
|
async createBackup() {
|
|
162
153
|
const backupName = `_backup_migration_${Date.now()}`;
|
|
163
154
|
const backupPath = path.join(this.outputPath, backupName);
|
|
164
|
-
|
|
155
|
+
|
|
165
156
|
try {
|
|
166
157
|
await fs.ensureDir(backupPath);
|
|
167
|
-
|
|
158
|
+
|
|
168
159
|
// Copy legacy directories
|
|
169
160
|
const legacyDirs = ['planning-artifacts', 'implementation-artifacts', 'tests'];
|
|
170
161
|
for (const dir of legacyDirs) {
|
|
@@ -173,7 +164,7 @@ class ScopeMigrator {
|
|
|
173
164
|
await fs.copy(sourcePath, path.join(backupPath, dir));
|
|
174
165
|
}
|
|
175
166
|
}
|
|
176
|
-
|
|
167
|
+
|
|
177
168
|
// Copy root-level files
|
|
178
169
|
const rootFiles = ['project-context.md', 'sprint-status.yaml', 'bmm-workflow-status.yaml'];
|
|
179
170
|
for (const file of rootFiles) {
|
|
@@ -182,7 +173,7 @@ class ScopeMigrator {
|
|
|
182
173
|
await fs.copy(sourcePath, path.join(backupPath, file));
|
|
183
174
|
}
|
|
184
175
|
}
|
|
185
|
-
|
|
176
|
+
|
|
186
177
|
return backupPath;
|
|
187
178
|
} catch (error) {
|
|
188
179
|
throw new Error(`Failed to create backup: ${error.message}`);
|
|
@@ -197,13 +188,13 @@ class ScopeMigrator {
|
|
|
197
188
|
async migrate(options = {}) {
|
|
198
189
|
const scopeId = options.scopeId || this.defaultScopeId;
|
|
199
190
|
const createBackup = options.backup !== false;
|
|
200
|
-
|
|
191
|
+
|
|
201
192
|
const result = {
|
|
202
193
|
success: false,
|
|
203
194
|
scopeId,
|
|
204
195
|
backupPath: null,
|
|
205
196
|
migratedFiles: [],
|
|
206
|
-
errors: []
|
|
197
|
+
errors: [],
|
|
207
198
|
};
|
|
208
199
|
|
|
209
200
|
try {
|
|
@@ -225,7 +216,7 @@ class ScopeMigrator {
|
|
|
225
216
|
const scopeDirs = {
|
|
226
217
|
planning: path.join(scopePath, 'planning-artifacts'),
|
|
227
218
|
implementation: path.join(scopePath, 'implementation-artifacts'),
|
|
228
|
-
tests: path.join(scopePath, 'tests')
|
|
219
|
+
tests: path.join(scopePath, 'tests'),
|
|
229
220
|
};
|
|
230
221
|
|
|
231
222
|
for (const dir of Object.values(scopeDirs)) {
|
|
@@ -236,7 +227,7 @@ class ScopeMigrator {
|
|
|
236
227
|
const migrations = [
|
|
237
228
|
{ from: 'planning-artifacts', to: scopeDirs.planning },
|
|
238
229
|
{ from: 'implementation-artifacts', to: scopeDirs.implementation },
|
|
239
|
-
{ from: 'tests', to: scopeDirs.tests }
|
|
230
|
+
{ from: 'tests', to: scopeDirs.tests },
|
|
240
231
|
];
|
|
241
232
|
|
|
242
233
|
for (const migration of migrations) {
|
|
@@ -247,17 +238,17 @@ class ScopeMigrator {
|
|
|
247
238
|
for (const entry of entries) {
|
|
248
239
|
const sourceFile = path.join(sourcePath, entry.name);
|
|
249
240
|
const targetFile = path.join(migration.to, entry.name);
|
|
250
|
-
|
|
241
|
+
|
|
251
242
|
// Skip if target already exists
|
|
252
243
|
if (await fs.pathExists(targetFile)) {
|
|
253
244
|
result.errors.push(`Skipped ${entry.name}: already exists in target`);
|
|
254
245
|
continue;
|
|
255
246
|
}
|
|
256
|
-
|
|
247
|
+
|
|
257
248
|
await fs.copy(sourceFile, targetFile);
|
|
258
249
|
result.migratedFiles.push(path.join(migration.from, entry.name));
|
|
259
250
|
}
|
|
260
|
-
|
|
251
|
+
|
|
261
252
|
// Remove original directory
|
|
262
253
|
await fs.remove(sourcePath);
|
|
263
254
|
}
|
|
@@ -267,7 +258,7 @@ class ScopeMigrator {
|
|
|
267
258
|
const rootFileMigrations = [
|
|
268
259
|
{ from: 'project-context.md', to: path.join(scopePath, 'project-context.md') },
|
|
269
260
|
{ from: 'sprint-status.yaml', to: path.join(scopeDirs.implementation, 'sprint-status.yaml') },
|
|
270
|
-
{ from: 'bmm-workflow-status.yaml', to: path.join(scopeDirs.planning, 'bmm-workflow-status.yaml') }
|
|
261
|
+
{ from: 'bmm-workflow-status.yaml', to: path.join(scopeDirs.planning, 'bmm-workflow-status.yaml') },
|
|
271
262
|
];
|
|
272
263
|
|
|
273
264
|
for (const migration of rootFileMigrations) {
|
|
@@ -290,20 +281,19 @@ class ScopeMigrator {
|
|
|
290
281
|
migrated: true,
|
|
291
282
|
migrated_at: new Date().toISOString(),
|
|
292
283
|
original_backup: result.backupPath,
|
|
293
|
-
version: 1
|
|
284
|
+
version: 1,
|
|
294
285
|
};
|
|
295
286
|
await fs.writeFile(metaPath, yaml.stringify(metadata), 'utf8');
|
|
296
287
|
|
|
297
288
|
// Create scope README
|
|
298
289
|
const readmePath = path.join(scopePath, 'README.md');
|
|
299
|
-
if (!await fs.pathExists(readmePath)) {
|
|
290
|
+
if (!(await fs.pathExists(readmePath))) {
|
|
300
291
|
const readme = this.generateMigrationReadme(scopeId, result.migratedFiles.length);
|
|
301
292
|
await fs.writeFile(readmePath, readme, 'utf8');
|
|
302
293
|
}
|
|
303
294
|
|
|
304
295
|
result.success = true;
|
|
305
296
|
result.message = `Migrated ${result.migratedFiles.length} items to scope '${scopeId}'`;
|
|
306
|
-
|
|
307
297
|
} catch (error) {
|
|
308
298
|
result.success = false;
|
|
309
299
|
result.errors.push(error.message);
|
|
@@ -359,22 +349,22 @@ bmad scope info ${scopeId}
|
|
|
359
349
|
*/
|
|
360
350
|
async rollback(backupPath) {
|
|
361
351
|
try {
|
|
362
|
-
if (!await fs.pathExists(backupPath)) {
|
|
352
|
+
if (!(await fs.pathExists(backupPath))) {
|
|
363
353
|
throw new Error(`Backup not found at: ${backupPath}`);
|
|
364
354
|
}
|
|
365
355
|
|
|
366
356
|
// Restore backed up directories
|
|
367
357
|
const entries = await fs.readdir(backupPath, { withFileTypes: true });
|
|
368
|
-
|
|
358
|
+
|
|
369
359
|
for (const entry of entries) {
|
|
370
360
|
const sourcePath = path.join(backupPath, entry.name);
|
|
371
361
|
const targetPath = path.join(this.outputPath, entry.name);
|
|
372
|
-
|
|
362
|
+
|
|
373
363
|
// Remove current version if exists
|
|
374
364
|
if (await fs.pathExists(targetPath)) {
|
|
375
365
|
await fs.remove(targetPath);
|
|
376
366
|
}
|
|
377
|
-
|
|
367
|
+
|
|
378
368
|
// Restore from backup
|
|
379
369
|
await fs.copy(sourcePath, targetPath);
|
|
380
370
|
}
|
|
@@ -395,34 +385,36 @@ bmad scope info ${scopeId}
|
|
|
395
385
|
*/
|
|
396
386
|
async updateReferences(scopeId) {
|
|
397
387
|
const result = { updated: [], errors: [] };
|
|
398
|
-
|
|
388
|
+
|
|
399
389
|
const scopePath = path.join(this.outputPath, scopeId);
|
|
400
|
-
|
|
390
|
+
|
|
401
391
|
// Files that might contain path references
|
|
402
392
|
const filesToUpdate = [
|
|
403
393
|
path.join(scopePath, 'implementation-artifacts', 'sprint-status.yaml'),
|
|
404
|
-
path.join(scopePath, 'planning-artifacts', 'bmm-workflow-status.yaml')
|
|
394
|
+
path.join(scopePath, 'planning-artifacts', 'bmm-workflow-status.yaml'),
|
|
405
395
|
];
|
|
406
396
|
|
|
407
397
|
for (const filePath of filesToUpdate) {
|
|
408
398
|
if (await fs.pathExists(filePath)) {
|
|
409
399
|
try {
|
|
410
400
|
let content = await fs.readFile(filePath, 'utf8');
|
|
411
|
-
|
|
401
|
+
|
|
412
402
|
// Update common path patterns
|
|
413
403
|
const patterns = [
|
|
414
404
|
{ from: /planning-artifacts\//g, to: `${scopeId}/planning-artifacts/` },
|
|
415
405
|
{ from: /implementation-artifacts\//g, to: `${scopeId}/implementation-artifacts/` },
|
|
416
|
-
{ from: /tests\//g, to: `${scopeId}/tests/` }
|
|
406
|
+
{ from: /tests\//g, to: `${scopeId}/tests/` },
|
|
417
407
|
];
|
|
418
408
|
|
|
419
409
|
let modified = false;
|
|
420
410
|
for (const pattern of patterns) {
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
411
|
+
if (
|
|
412
|
+
pattern.from.test(content) && // Only update if not already scoped
|
|
413
|
+
!content.includes(`${scopeId}/`)
|
|
414
|
+
) {
|
|
415
|
+
content = content.replace(pattern.from, pattern.to);
|
|
416
|
+
modified = true;
|
|
417
|
+
}
|
|
426
418
|
}
|
|
427
419
|
|
|
428
420
|
if (modified) {
|