bmad-fh 6.0.0-alpha.23 → 6.0.0-alpha.23.02a963fa
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-multi-artifact.yaml +6 -2
- package/eslint.config.mjs +2 -2
- package/package.json +2 -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 +21 -21
- 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-e2e.js +65 -76
- package/test/test-scope-system.js +66 -72
- package/tools/cli/commands/scope.js +73 -73
- package/tools/cli/scripts/migrate-workflows.js +43 -51
|
@@ -5,11 +5,11 @@ const yaml = require('yaml');
|
|
|
5
5
|
/**
|
|
6
6
|
* Initializes directory structure for scopes
|
|
7
7
|
* Creates scope directories, shared layer, and event system
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* @class ScopeInitializer
|
|
10
10
|
* @requires fs-extra
|
|
11
11
|
* @requires yaml
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* @example
|
|
14
14
|
* const initializer = new ScopeInitializer({ projectRoot: '/path/to/project' });
|
|
15
15
|
* await initializer.initializeScope('auth');
|
|
@@ -70,14 +70,14 @@ class ScopeInitializer {
|
|
|
70
70
|
|
|
71
71
|
// Create README in shared directory
|
|
72
72
|
const sharedReadmePath = path.join(this.sharedPath, 'README.md');
|
|
73
|
-
if (!await fs.pathExists(sharedReadmePath)) {
|
|
73
|
+
if (!(await fs.pathExists(sharedReadmePath))) {
|
|
74
74
|
const readmeContent = this.generateSharedReadme();
|
|
75
75
|
await fs.writeFile(sharedReadmePath, readmeContent, 'utf8');
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// Create global project-context.md template
|
|
79
79
|
const contextPath = path.join(this.sharedPath, 'project-context.md');
|
|
80
|
-
if (!await fs.pathExists(contextPath)) {
|
|
80
|
+
if (!(await fs.pathExists(contextPath))) {
|
|
81
81
|
const contextContent = this.generateGlobalContextTemplate();
|
|
82
82
|
await fs.writeFile(contextPath, contextContent, 'utf8');
|
|
83
83
|
}
|
|
@@ -100,20 +100,20 @@ class ScopeInitializer {
|
|
|
100
100
|
|
|
101
101
|
// Create event-log.yaml
|
|
102
102
|
const eventLogPath = path.join(this.eventsPath, 'event-log.yaml');
|
|
103
|
-
if (!await fs.pathExists(eventLogPath)) {
|
|
103
|
+
if (!(await fs.pathExists(eventLogPath))) {
|
|
104
104
|
const eventLog = {
|
|
105
105
|
version: 1,
|
|
106
|
-
events: []
|
|
106
|
+
events: [],
|
|
107
107
|
};
|
|
108
108
|
await fs.writeFile(eventLogPath, yaml.stringify(eventLog), 'utf8');
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Create subscriptions.yaml
|
|
112
112
|
const subscriptionsPath = path.join(this.eventsPath, 'subscriptions.yaml');
|
|
113
|
-
if (!await fs.pathExists(subscriptionsPath)) {
|
|
113
|
+
if (!(await fs.pathExists(subscriptionsPath))) {
|
|
114
114
|
const subscriptions = {
|
|
115
115
|
version: 1,
|
|
116
|
-
subscriptions: {}
|
|
116
|
+
subscriptions: {},
|
|
117
117
|
};
|
|
118
118
|
await fs.writeFile(subscriptionsPath, yaml.stringify(subscriptions), 'utf8');
|
|
119
119
|
}
|
|
@@ -135,10 +135,8 @@ class ScopeInitializer {
|
|
|
135
135
|
const scopePath = path.join(this.outputPath, scopeId);
|
|
136
136
|
|
|
137
137
|
// Check if scope directory already exists
|
|
138
|
-
if (await fs.pathExists(scopePath)) {
|
|
139
|
-
|
|
140
|
-
throw new Error(`Scope directory '${scopeId}' already exists. Use force option to recreate.`);
|
|
141
|
-
}
|
|
138
|
+
if ((await fs.pathExists(scopePath)) && !options.force) {
|
|
139
|
+
throw new Error(`Scope directory '${scopeId}' already exists. Use force option to recreate.`);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
// Create scope directory structure
|
|
@@ -147,7 +145,7 @@ class ScopeInitializer {
|
|
|
147
145
|
planning: path.join(scopePath, 'planning-artifacts'),
|
|
148
146
|
implementation: path.join(scopePath, 'implementation-artifacts'),
|
|
149
147
|
tests: path.join(scopePath, 'tests'),
|
|
150
|
-
meta: path.join(scopePath, '.scope-meta.yaml')
|
|
148
|
+
meta: path.join(scopePath, '.scope-meta.yaml'),
|
|
151
149
|
};
|
|
152
150
|
|
|
153
151
|
// Create directories
|
|
@@ -163,14 +161,14 @@ class ScopeInitializer {
|
|
|
163
161
|
structure: {
|
|
164
162
|
planning_artifacts: 'planning-artifacts/',
|
|
165
163
|
implementation_artifacts: 'implementation-artifacts/',
|
|
166
|
-
tests: 'tests/'
|
|
167
|
-
}
|
|
164
|
+
tests: 'tests/',
|
|
165
|
+
},
|
|
168
166
|
};
|
|
169
167
|
await fs.writeFile(paths.meta, yaml.stringify(metadata), 'utf8');
|
|
170
168
|
|
|
171
169
|
// Create README in scope directory
|
|
172
170
|
const readmePath = path.join(scopePath, 'README.md');
|
|
173
|
-
if (!await fs.pathExists(readmePath)) {
|
|
171
|
+
if (!(await fs.pathExists(readmePath))) {
|
|
174
172
|
const readmeContent = this.generateScopeReadme(scopeId, options);
|
|
175
173
|
await fs.writeFile(readmePath, readmeContent, 'utf8');
|
|
176
174
|
}
|
|
@@ -199,7 +197,7 @@ class ScopeInitializer {
|
|
|
199
197
|
const scopePath = path.join(this.outputPath, scopeId);
|
|
200
198
|
|
|
201
199
|
// Check if scope exists
|
|
202
|
-
if (!await fs.pathExists(scopePath)) {
|
|
200
|
+
if (!(await fs.pathExists(scopePath))) {
|
|
203
201
|
throw new Error(`Scope directory '${scopeId}' does not exist`);
|
|
204
202
|
}
|
|
205
203
|
|
|
@@ -251,7 +249,7 @@ class ScopeInitializer {
|
|
|
251
249
|
implementation: path.join(scopePath, 'implementation-artifacts'),
|
|
252
250
|
tests: path.join(scopePath, 'tests'),
|
|
253
251
|
meta: path.join(scopePath, '.scope-meta.yaml'),
|
|
254
|
-
context: path.join(scopePath, 'project-context.md')
|
|
252
|
+
context: path.join(scopePath, 'project-context.md'),
|
|
255
253
|
};
|
|
256
254
|
}
|
|
257
255
|
|
|
@@ -281,9 +279,9 @@ The shared layer enables:
|
|
|
281
279
|
|
|
282
280
|
## Usage
|
|
283
281
|
|
|
284
|
-
1. **Reading**: All scopes can read from
|
|
285
|
-
2. **Writing**: Use
|
|
286
|
-
3. **Syncing**: Use
|
|
282
|
+
1. **Reading**: All scopes can read from \`_shared/\`
|
|
283
|
+
2. **Writing**: Use \`bmad scope sync-up <scope>\` to promote artifacts
|
|
284
|
+
3. **Syncing**: Use \`bmad scope sync-down <scope>\` to pull updates
|
|
287
285
|
|
|
288
286
|
## Best Practices
|
|
289
287
|
|
|
@@ -368,16 +366,14 @@ ${description}
|
|
|
368
366
|
|
|
369
367
|
## Scope Information
|
|
370
368
|
|
|
371
|
-
- **ID:**
|
|
369
|
+
- **ID:** ${scopeId}
|
|
372
370
|
- **Name:** ${scopeName}
|
|
373
371
|
- **Status:** ${options.status || 'active'}
|
|
374
372
|
- **Created:** ${new Date().toISOString().split('T')[0]}
|
|
375
373
|
|
|
376
374
|
## Dependencies
|
|
377
375
|
|
|
378
|
-
${options.dependencies && options.dependencies.length > 0
|
|
379
|
-
? options.dependencies.map(dep => `- \\`${dep}\\``).join('\n')
|
|
380
|
-
: 'No dependencies'}
|
|
376
|
+
${options.dependencies && options.dependencies.length > 0 ? options.dependencies.map((dep) => `- ${dep}`).join('\n') : 'No dependencies'}
|
|
381
377
|
|
|
382
378
|
## Usage
|
|
383
379
|
|
|
@@ -403,8 +399,8 @@ bmad scope sync-down ${scopeId}
|
|
|
403
399
|
|
|
404
400
|
## Related Documentation
|
|
405
401
|
|
|
406
|
-
- Global context:
|
|
407
|
-
- Contracts:
|
|
402
|
+
- Global context: ../_shared/project-context.md
|
|
403
|
+
- Contracts: ../_shared/contracts/
|
|
408
404
|
`;
|
|
409
405
|
}
|
|
410
406
|
|
|
@@ -419,7 +415,7 @@ bmad scope sync-down ${scopeId}
|
|
|
419
415
|
|
|
420
416
|
return `# Scope Context: ${scopeName}
|
|
421
417
|
|
|
422
|
-
> This context extends the global project context in
|
|
418
|
+
> This context extends the global project context in ../_shared/project-context.md
|
|
423
419
|
|
|
424
420
|
## Scope Purpose
|
|
425
421
|
|
|
@@ -436,9 +432,11 @@ bmad scope sync-down ${scopeId}
|
|
|
436
432
|
## Integration Points
|
|
437
433
|
|
|
438
434
|
### Dependencies
|
|
439
|
-
${
|
|
440
|
-
|
|
441
|
-
|
|
435
|
+
${
|
|
436
|
+
options.dependencies && options.dependencies.length > 0
|
|
437
|
+
? options.dependencies.map((dep) => `- **${dep}**: [Describe dependency relationship]`).join('\n')
|
|
438
|
+
: 'No dependencies'
|
|
439
|
+
}
|
|
442
440
|
|
|
443
441
|
### Provides
|
|
444
442
|
[What this scope provides to other scopes]
|
|
@@ -6,12 +6,12 @@ const { ScopeValidator } = require('./scope-validator');
|
|
|
6
6
|
/**
|
|
7
7
|
* Manages scope lifecycle and CRUD operations
|
|
8
8
|
* Handles scope configuration in scopes.yaml file
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
10
|
* @class ScopeManager
|
|
11
11
|
* @requires fs-extra
|
|
12
12
|
* @requires yaml
|
|
13
13
|
* @requires ScopeValidator
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
15
|
* @example
|
|
16
16
|
* const manager = new ScopeManager({ projectRoot: '/path/to/project' });
|
|
17
17
|
* await manager.initialize();
|
|
@@ -23,7 +23,7 @@ class ScopeManager {
|
|
|
23
23
|
this.bmadPath = options.bmadPath || path.join(this.projectRoot, '_bmad');
|
|
24
24
|
this.configPath = options.configPath || path.join(this.bmadPath, '_config');
|
|
25
25
|
this.scopesFilePath = options.scopesFilePath || path.join(this.configPath, 'scopes.yaml');
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
this.validator = new ScopeValidator();
|
|
28
28
|
this._config = null; // Cached configuration
|
|
29
29
|
}
|
|
@@ -52,7 +52,7 @@ class ScopeManager {
|
|
|
52
52
|
|
|
53
53
|
// Check if scopes.yaml exists
|
|
54
54
|
const exists = await fs.pathExists(this.scopesFilePath);
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
if (!exists) {
|
|
57
57
|
// Create default configuration
|
|
58
58
|
const defaultConfig = this.validator.createDefaultConfig();
|
|
@@ -122,7 +122,7 @@ class ScopeManager {
|
|
|
122
122
|
// Write to file
|
|
123
123
|
const yamlContent = yaml.stringify(config, {
|
|
124
124
|
indent: 2,
|
|
125
|
-
lineWidth: 100
|
|
125
|
+
lineWidth: 100,
|
|
126
126
|
});
|
|
127
127
|
await fs.writeFile(this.scopesFilePath, yamlContent, 'utf8');
|
|
128
128
|
|
|
@@ -146,7 +146,7 @@ class ScopeManager {
|
|
|
146
146
|
|
|
147
147
|
// Apply filters
|
|
148
148
|
if (filters.status) {
|
|
149
|
-
scopes = scopes.filter(scope => scope.status === filters.status);
|
|
149
|
+
scopes = scopes.filter((scope) => scope.status === filters.status);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
// Sort by created date (newest first)
|
|
@@ -223,8 +223,8 @@ class ScopeManager {
|
|
|
223
223
|
created: new Date().toISOString(),
|
|
224
224
|
_meta: {
|
|
225
225
|
last_activity: new Date().toISOString(),
|
|
226
|
-
artifact_count: 0
|
|
227
|
-
}
|
|
226
|
+
artifact_count: 0,
|
|
227
|
+
},
|
|
228
228
|
};
|
|
229
229
|
|
|
230
230
|
// Validate scope with existing scopes
|
|
@@ -272,8 +272,8 @@ class ScopeManager {
|
|
|
272
272
|
_meta: {
|
|
273
273
|
...currentScope._meta,
|
|
274
274
|
...updates._meta,
|
|
275
|
-
last_activity: new Date().toISOString()
|
|
276
|
-
}
|
|
275
|
+
last_activity: new Date().toISOString(),
|
|
276
|
+
},
|
|
277
277
|
};
|
|
278
278
|
|
|
279
279
|
// Validate updated scope
|
|
@@ -314,7 +314,7 @@ class ScopeManager {
|
|
|
314
314
|
const dependentScopes = this.findDependentScopes(scopeId, config.scopes);
|
|
315
315
|
if (dependentScopes.length > 0 && !options.force) {
|
|
316
316
|
throw new Error(
|
|
317
|
-
`Cannot remove scope '${scopeId}'. The following scopes depend on it: ${dependentScopes.join(', ')}. Use force option to remove anyway
|
|
317
|
+
`Cannot remove scope '${scopeId}'. The following scopes depend on it: ${dependentScopes.join(', ')}. Use force option to remove anyway.`,
|
|
318
318
|
);
|
|
319
319
|
}
|
|
320
320
|
|
|
@@ -325,7 +325,7 @@ class ScopeManager {
|
|
|
325
325
|
if (options.force && dependentScopes.length > 0) {
|
|
326
326
|
for (const depScopeId of dependentScopes) {
|
|
327
327
|
const depScope = config.scopes[depScopeId];
|
|
328
|
-
depScope.dependencies = depScope.dependencies.filter(dep => dep !== scopeId);
|
|
328
|
+
depScope.dependencies = depScope.dependencies.filter((dep) => dep !== scopeId);
|
|
329
329
|
}
|
|
330
330
|
}
|
|
331
331
|
|
|
@@ -347,7 +347,7 @@ class ScopeManager {
|
|
|
347
347
|
try {
|
|
348
348
|
const config = await this.loadConfig();
|
|
349
349
|
const scope = config.scopes[scopeId];
|
|
350
|
-
|
|
350
|
+
|
|
351
351
|
if (!scope) {
|
|
352
352
|
throw new Error(`Scope '${scopeId}' does not exist`);
|
|
353
353
|
}
|
|
@@ -360,7 +360,7 @@ class ScopeManager {
|
|
|
360
360
|
planning: path.join(scopePath, 'planning-artifacts'),
|
|
361
361
|
implementation: path.join(scopePath, 'implementation-artifacts'),
|
|
362
362
|
tests: path.join(scopePath, 'tests'),
|
|
363
|
-
meta: path.join(scopePath, '.scope-meta.yaml')
|
|
363
|
+
meta: path.join(scopePath, '.scope-meta.yaml'),
|
|
364
364
|
};
|
|
365
365
|
} catch (error) {
|
|
366
366
|
throw new Error(`Failed to get scope paths for '${scopeId}': ${error.message}`);
|
|
@@ -388,7 +388,7 @@ class ScopeManager {
|
|
|
388
388
|
try {
|
|
389
389
|
const config = await this.loadConfig();
|
|
390
390
|
const scope = config.scopes[scopeId];
|
|
391
|
-
|
|
391
|
+
|
|
392
392
|
if (!scope) {
|
|
393
393
|
throw new Error(`Scope '${scopeId}' does not exist`);
|
|
394
394
|
}
|
|
@@ -396,7 +396,7 @@ class ScopeManager {
|
|
|
396
396
|
const tree = {
|
|
397
397
|
scope: scopeId,
|
|
398
398
|
dependencies: [],
|
|
399
|
-
dependents: this.findDependentScopes(scopeId, config.scopes)
|
|
399
|
+
dependents: this.findDependentScopes(scopeId, config.scopes),
|
|
400
400
|
};
|
|
401
401
|
|
|
402
402
|
// Build dependency tree recursively
|
|
@@ -407,7 +407,7 @@ class ScopeManager {
|
|
|
407
407
|
tree.dependencies.push({
|
|
408
408
|
scope: depId,
|
|
409
409
|
name: depScope.name,
|
|
410
|
-
status: depScope.status
|
|
410
|
+
status: depScope.status,
|
|
411
411
|
});
|
|
412
412
|
}
|
|
413
413
|
}
|
|
@@ -427,7 +427,7 @@ class ScopeManager {
|
|
|
427
427
|
*/
|
|
428
428
|
findDependentScopes(scopeId, allScopes) {
|
|
429
429
|
const dependents = [];
|
|
430
|
-
|
|
430
|
+
|
|
431
431
|
for (const [sid, scope] of Object.entries(allScopes)) {
|
|
432
432
|
if (scope.dependencies && scope.dependencies.includes(scopeId)) {
|
|
433
433
|
dependents.push(sid);
|
|
@@ -462,7 +462,7 @@ class ScopeManager {
|
|
|
462
462
|
*/
|
|
463
463
|
async touchScope(scopeId) {
|
|
464
464
|
return this.updateScope(scopeId, {
|
|
465
|
-
_meta: { last_activity: new Date().toISOString() }
|
|
465
|
+
_meta: { last_activity: new Date().toISOString() },
|
|
466
466
|
});
|
|
467
467
|
}
|
|
468
468
|
|
|
@@ -480,7 +480,7 @@ class ScopeManager {
|
|
|
480
480
|
|
|
481
481
|
const currentCount = scope._meta?.artifact_count || 0;
|
|
482
482
|
return this.updateScope(scopeId, {
|
|
483
|
-
_meta: { artifact_count: currentCount + increment }
|
|
483
|
+
_meta: { artifact_count: currentCount + increment },
|
|
484
484
|
});
|
|
485
485
|
}
|
|
486
486
|
|
|
@@ -502,7 +502,7 @@ class ScopeManager {
|
|
|
502
502
|
const config = await this.loadConfig();
|
|
503
503
|
config.settings = {
|
|
504
504
|
...config.settings,
|
|
505
|
-
...settings
|
|
505
|
+
...settings,
|
|
506
506
|
};
|
|
507
507
|
await this.saveConfig(config);
|
|
508
508
|
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) {
|