bmad-fh 6.0.0-alpha.23.6390fcb0 → 6.0.0-alpha.23.6874ced1
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 +17 -3
- package/.husky/post-checkout +12 -0
- package/.husky/pre-commit +17 -2
- package/.husky/pre-push +10 -0
- package/README.md +13 -7
- package/docs/migration-guide.md +27 -7
- package/docs/multi-scope-guide.md +69 -33
- package/docs/plans/multi-scope-parallel-artifacts-plan.md +112 -91
- package/package.json +3 -3
- package/src/core/lib/scope/scope-context.js +13 -2
- package/src/core/lib/scope/scope-manager.js +1 -1
- package/src/core/lib/scope/scope-validator.js +6 -4
- package/test/test-cli-arguments.js +686 -0
- package/test/test-scope-cli.js +171 -2
- package/tools/bmad-npx-wrapper.js +12 -2
- package/tools/build-docs.js +1 -0
- package/tools/cli/commands/scope.js +37 -2
package/test/test-scope-cli.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
const fs = require('fs-extra');
|
|
15
15
|
const path = require('node:path');
|
|
16
16
|
const os = require('node:os');
|
|
17
|
-
const { execSync,
|
|
17
|
+
const { execSync, spawnSync } = require('node:child_process');
|
|
18
18
|
|
|
19
19
|
// ANSI color codes
|
|
20
20
|
const colors = {
|
|
@@ -133,7 +133,7 @@ function cleanupTestProject(tmpDir) {
|
|
|
133
133
|
// Get path to CLI
|
|
134
134
|
const CLI_PATH = path.join(__dirname, '..', 'tools', 'cli', 'bmad-cli.js');
|
|
135
135
|
|
|
136
|
-
// Execute CLI command and capture output
|
|
136
|
+
// Execute CLI command and capture output (string-based, for simple cases)
|
|
137
137
|
function runCli(args, cwd, options = {}) {
|
|
138
138
|
const cmd = `node "${CLI_PATH}" ${args}`;
|
|
139
139
|
try {
|
|
@@ -155,6 +155,32 @@ function runCli(args, cwd, options = {}) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Execute CLI command using spawnSync with an array of arguments.
|
|
160
|
+
* This properly preserves argument boundaries, essential for arguments with spaces.
|
|
161
|
+
*
|
|
162
|
+
* @param {string[]} args - Array of arguments (NOT a joined string)
|
|
163
|
+
* @param {string} cwd - Working directory
|
|
164
|
+
* @param {Object} options - Additional options
|
|
165
|
+
* @returns {Object} Result with success, output, stderr, exitCode
|
|
166
|
+
*/
|
|
167
|
+
function runCliArray(args, cwd, options = {}) {
|
|
168
|
+
const result = spawnSync('node', [CLI_PATH, ...args], {
|
|
169
|
+
cwd,
|
|
170
|
+
encoding: 'utf8',
|
|
171
|
+
timeout: options.timeout || 30_000,
|
|
172
|
+
env: { ...process.env, ...options.env, FORCE_COLOR: '0' },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
success: result.status === 0,
|
|
177
|
+
output: result.stdout || '',
|
|
178
|
+
stderr: result.stderr || '',
|
|
179
|
+
exitCode: result.status || 0,
|
|
180
|
+
error: result.error ? result.error.message : null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
158
184
|
// ============================================================================
|
|
159
185
|
// Help System Tests
|
|
160
186
|
// ============================================================================
|
|
@@ -1252,6 +1278,148 @@ function testIntegration() {
|
|
|
1252
1278
|
});
|
|
1253
1279
|
}
|
|
1254
1280
|
|
|
1281
|
+
// ============================================================================
|
|
1282
|
+
// Argument Handling Tests (using runCliArray for proper boundary preservation)
|
|
1283
|
+
// ============================================================================
|
|
1284
|
+
|
|
1285
|
+
function testArgumentHandling() {
|
|
1286
|
+
console.log(`\n${colors.blue}${colors.bold}Argument Handling Tests${colors.reset}`);
|
|
1287
|
+
|
|
1288
|
+
test('scope create with multi-word description (array args)', () => {
|
|
1289
|
+
const tmpDir = createTestProject();
|
|
1290
|
+
try {
|
|
1291
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
1292
|
+
const result = runCliArray(
|
|
1293
|
+
['scope', 'create', 'auth', '--name', 'Auth Service', '--description', 'Handles user authentication and sessions'],
|
|
1294
|
+
tmpDir,
|
|
1295
|
+
);
|
|
1296
|
+
assertTrue(result.success, `Create should succeed: ${result.stderr || result.error}`);
|
|
1297
|
+
|
|
1298
|
+
const infoResult = runCliArray(['scope', 'info', 'auth'], tmpDir);
|
|
1299
|
+
assertContains(infoResult.output, 'Auth Service');
|
|
1300
|
+
assertContains(infoResult.output, 'Handles user authentication and sessions');
|
|
1301
|
+
} finally {
|
|
1302
|
+
cleanupTestProject(tmpDir);
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
test('scope create with 9-word description (regression test)', () => {
|
|
1307
|
+
const tmpDir = createTestProject();
|
|
1308
|
+
try {
|
|
1309
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
1310
|
+
// This exact case caused "too many arguments" error before the fix
|
|
1311
|
+
const result = runCliArray(
|
|
1312
|
+
['scope', 'create', 'auto-queue', '--name', 'AutoQueue', '--description', 'PRD Auto queue for not inbound yet products'],
|
|
1313
|
+
tmpDir,
|
|
1314
|
+
);
|
|
1315
|
+
assertTrue(result.success, `Should not fail with "too many arguments": ${result.stderr}`);
|
|
1316
|
+
assertNotContains(result.stderr || '', 'too many arguments');
|
|
1317
|
+
|
|
1318
|
+
const infoResult = runCliArray(['scope', 'info', 'auto-queue'], tmpDir);
|
|
1319
|
+
assertContains(infoResult.output, 'PRD Auto queue for not inbound yet products');
|
|
1320
|
+
} finally {
|
|
1321
|
+
cleanupTestProject(tmpDir);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
test('all subcommands work with array args', () => {
|
|
1326
|
+
const tmpDir = createTestProject();
|
|
1327
|
+
try {
|
|
1328
|
+
// init
|
|
1329
|
+
let result = runCliArray(['scope', 'init'], tmpDir);
|
|
1330
|
+
assertTrue(result.success, 'init should work');
|
|
1331
|
+
|
|
1332
|
+
// create
|
|
1333
|
+
result = runCliArray(['scope', 'create', 'test', '--name', 'Test Scope', '--description', 'A test scope'], tmpDir);
|
|
1334
|
+
assertTrue(result.success, 'create should work');
|
|
1335
|
+
|
|
1336
|
+
// list
|
|
1337
|
+
result = runCliArray(['scope', 'list'], tmpDir);
|
|
1338
|
+
assertTrue(result.success, 'list should work');
|
|
1339
|
+
assertContains(result.output, 'test');
|
|
1340
|
+
|
|
1341
|
+
// info
|
|
1342
|
+
result = runCliArray(['scope', 'info', 'test'], tmpDir);
|
|
1343
|
+
assertTrue(result.success, 'info should work');
|
|
1344
|
+
|
|
1345
|
+
// set
|
|
1346
|
+
result = runCliArray(['scope', 'set', 'test'], tmpDir);
|
|
1347
|
+
assertTrue(result.success, 'set should work');
|
|
1348
|
+
|
|
1349
|
+
// archive
|
|
1350
|
+
result = runCliArray(['scope', 'archive', 'test'], tmpDir);
|
|
1351
|
+
assertTrue(result.success, 'archive should work');
|
|
1352
|
+
|
|
1353
|
+
// activate
|
|
1354
|
+
result = runCliArray(['scope', 'activate', 'test'], tmpDir);
|
|
1355
|
+
assertTrue(result.success, 'activate should work');
|
|
1356
|
+
|
|
1357
|
+
// sync-up
|
|
1358
|
+
result = runCliArray(['scope', 'sync-up', 'test', '--dry-run'], tmpDir);
|
|
1359
|
+
assertTrue(result.success, 'sync-up should work');
|
|
1360
|
+
|
|
1361
|
+
// sync-down
|
|
1362
|
+
result = runCliArray(['scope', 'sync-down', 'test', '--dry-run'], tmpDir);
|
|
1363
|
+
assertTrue(result.success, 'sync-down should work');
|
|
1364
|
+
|
|
1365
|
+
// unset
|
|
1366
|
+
result = runCliArray(['scope', 'unset'], tmpDir);
|
|
1367
|
+
assertTrue(result.success, 'unset should work');
|
|
1368
|
+
|
|
1369
|
+
// remove
|
|
1370
|
+
result = runCliArray(['scope', 'remove', 'test', '--force'], tmpDir);
|
|
1371
|
+
assertTrue(result.success, 'remove should work');
|
|
1372
|
+
|
|
1373
|
+
// help
|
|
1374
|
+
result = runCliArray(['scope', 'help'], tmpDir);
|
|
1375
|
+
assertTrue(result.success, 'help should work');
|
|
1376
|
+
} finally {
|
|
1377
|
+
cleanupTestProject(tmpDir);
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
test('subcommand aliases work with array args', () => {
|
|
1382
|
+
const tmpDir = createTestProject();
|
|
1383
|
+
try {
|
|
1384
|
+
runCliArray(['scope', 'init'], tmpDir);
|
|
1385
|
+
|
|
1386
|
+
// new (alias for create) - include --description to avoid interactive prompt
|
|
1387
|
+
let result = runCliArray(['scope', 'new', 'test', '--name', 'Test', '--description', ''], tmpDir);
|
|
1388
|
+
assertTrue(result.success, 'new alias should work');
|
|
1389
|
+
|
|
1390
|
+
// ls (alias for list)
|
|
1391
|
+
result = runCliArray(['scope', 'ls'], tmpDir);
|
|
1392
|
+
assertTrue(result.success, 'ls alias should work');
|
|
1393
|
+
|
|
1394
|
+
// show (alias for info)
|
|
1395
|
+
result = runCliArray(['scope', 'show', 'test'], tmpDir);
|
|
1396
|
+
assertTrue(result.success, 'show alias should work');
|
|
1397
|
+
|
|
1398
|
+
// use (alias for set)
|
|
1399
|
+
result = runCliArray(['scope', 'use', 'test'], tmpDir);
|
|
1400
|
+
assertTrue(result.success, 'use alias should work');
|
|
1401
|
+
|
|
1402
|
+
// clear (alias for unset)
|
|
1403
|
+
result = runCliArray(['scope', 'clear'], tmpDir);
|
|
1404
|
+
assertTrue(result.success, 'clear alias should work');
|
|
1405
|
+
|
|
1406
|
+
// syncup (alias for sync-up)
|
|
1407
|
+
result = runCliArray(['scope', 'syncup', 'test', '--dry-run'], tmpDir);
|
|
1408
|
+
assertTrue(result.success, 'syncup alias should work');
|
|
1409
|
+
|
|
1410
|
+
// syncdown (alias for sync-down)
|
|
1411
|
+
result = runCliArray(['scope', 'syncdown', 'test', '--dry-run'], tmpDir);
|
|
1412
|
+
assertTrue(result.success, 'syncdown alias should work');
|
|
1413
|
+
|
|
1414
|
+
// rm (alias for remove)
|
|
1415
|
+
result = runCliArray(['scope', 'rm', 'test', '--force'], tmpDir);
|
|
1416
|
+
assertTrue(result.success, 'rm alias should work');
|
|
1417
|
+
} finally {
|
|
1418
|
+
cleanupTestProject(tmpDir);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1255
1423
|
// ============================================================================
|
|
1256
1424
|
// Main Test Runner
|
|
1257
1425
|
// ============================================================================
|
|
@@ -1274,6 +1442,7 @@ function main() {
|
|
|
1274
1442
|
testSyncCommands();
|
|
1275
1443
|
testEdgeCases();
|
|
1276
1444
|
testIntegration();
|
|
1445
|
+
testArgumentHandling();
|
|
1277
1446
|
|
|
1278
1447
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
1279
1448
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* This file ensures proper execution when run via npx from GitHub or npm registry
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const {
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
9
9
|
const path = require('node:path');
|
|
10
10
|
const fs = require('node:fs');
|
|
11
11
|
|
|
@@ -25,10 +25,20 @@ if (isNpxExecution) {
|
|
|
25
25
|
|
|
26
26
|
try {
|
|
27
27
|
// Execute CLI from user's working directory (process.cwd()), not npm cache
|
|
28
|
-
|
|
28
|
+
// Use spawnSync with array args to preserve argument boundaries
|
|
29
|
+
// (args.join(' ') would break arguments containing spaces)
|
|
30
|
+
const result = spawnSync('node', [bmadCliPath, ...args], {
|
|
29
31
|
stdio: 'inherit',
|
|
30
32
|
cwd: process.cwd(), // This preserves the user's working directory
|
|
31
33
|
});
|
|
34
|
+
|
|
35
|
+
if (result.error) {
|
|
36
|
+
throw result.error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (result.status !== 0) {
|
|
40
|
+
process.exit(result.status || 1);
|
|
41
|
+
}
|
|
32
42
|
} catch (error) {
|
|
33
43
|
process.exit(error.status || 1);
|
|
34
44
|
}
|
package/tools/build-docs.js
CHANGED
|
@@ -36,6 +36,7 @@ const LLM_EXCLUDE_PATTERNS = [
|
|
|
36
36
|
'v4-to-v6-upgrade',
|
|
37
37
|
'downloads/',
|
|
38
38
|
'faq',
|
|
39
|
+
'plans/', // Internal planning docs, not user-facing
|
|
39
40
|
'reference/glossary/',
|
|
40
41
|
'explanation/game-dev/',
|
|
41
42
|
// Note: Files/dirs starting with _ (like _STYLE_GUIDE.md, _archive/) are excluded in shouldExcludeFromLlm()
|
|
@@ -232,8 +232,28 @@ async function handleCreate(projectRoot, scopeId, options) {
|
|
|
232
232
|
console.log(` ${paths.implementation}`);
|
|
233
233
|
console.log(` ${paths.tests}`);
|
|
234
234
|
console.log();
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
|
|
236
|
+
// Prompt to set as active scope (critical UX improvement)
|
|
237
|
+
const setActive = await confirm({
|
|
238
|
+
message: `Set '${scopeId}' as your active scope for this session?`,
|
|
239
|
+
initialValue: true,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!isCancel(setActive) && setActive) {
|
|
243
|
+
const scopeFilePath = path.join(projectRoot, '.bmad-scope');
|
|
244
|
+
const scopeContent = `# BMAD Active Scope Configuration
|
|
245
|
+
# This file is auto-generated. Do not edit manually.
|
|
246
|
+
# To change: npx bmad-fh scope set <scope-id>
|
|
247
|
+
|
|
248
|
+
active_scope: ${scopeId}
|
|
249
|
+
set_at: "${new Date().toISOString()}"
|
|
250
|
+
`;
|
|
251
|
+
await fs.writeFile(scopeFilePath, scopeContent, 'utf8');
|
|
252
|
+
console.log(chalk.green(`\n✓ Active scope set to '${scopeId}'`));
|
|
253
|
+
console.log(chalk.dim(' Workflows will now use this scope automatically.\n'));
|
|
254
|
+
} else {
|
|
255
|
+
console.log(chalk.dim(`\n To activate later, run: npx bmad-fh scope set ${scopeId}\n`));
|
|
256
|
+
}
|
|
237
257
|
}
|
|
238
258
|
|
|
239
259
|
/**
|
|
@@ -304,6 +324,21 @@ async function handleRemove(projectRoot, scopeId, options) {
|
|
|
304
324
|
// Remove from configuration
|
|
305
325
|
await manager.removeScope(scopeId, { force: true });
|
|
306
326
|
|
|
327
|
+
// Clean up .bmad-scope if this was the active scope
|
|
328
|
+
const scopeFilePath = path.join(projectRoot, '.bmad-scope');
|
|
329
|
+
if (await fs.pathExists(scopeFilePath)) {
|
|
330
|
+
try {
|
|
331
|
+
const content = await fs.readFile(scopeFilePath, 'utf8');
|
|
332
|
+
const match = content.match(/active_scope:\s*(\S+)/);
|
|
333
|
+
if (match && match[1] === scopeId) {
|
|
334
|
+
await fs.remove(scopeFilePath);
|
|
335
|
+
console.log(chalk.yellow(`\n Note: Cleared active scope (was set to '${scopeId}')`));
|
|
336
|
+
}
|
|
337
|
+
} catch {
|
|
338
|
+
// Ignore errors reading .bmad-scope
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
307
342
|
console.log(chalk.green(`\n✓ Scope '${scopeId}' removed successfully!`));
|
|
308
343
|
if (shouldBackup) {
|
|
309
344
|
console.log(chalk.dim(' A backup was created in _bmad-output/'));
|