bmad-fh 6.0.0-alpha.23.66f19588 → 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.
@@ -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, spawn } = require('node:child_process');
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 { execSync } = require('node:child_process');
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
- execSync(`node "${bmadCliPath}" ${args.join(' ')}`, {
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
  }
@@ -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
- console.log(chalk.cyan(` Use with workflows by setting .bmad-scope or using BMAD_SCOPE=${scopeId}`));
236
- console.log();
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/'));