forge-workflow 1.4.9 → 1.5.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/bin/forge.js CHANGED
@@ -35,13 +35,16 @@
35
35
  const fs = require('node:fs');
36
36
  const path = require('node:path');
37
37
  const readline = require('node:readline');
38
- const { execSync } = require('node:child_process');
38
+ const { execSync, execFileSync } = require('node:child_process');
39
39
 
40
40
  // Get version from package.json (single source of truth)
41
41
  const packageDir = path.dirname(__dirname);
42
42
  const packageJson = require(path.join(packageDir, 'package.json'));
43
43
  const VERSION = packageJson.version;
44
44
 
45
+ // Load PluginManager for discoverable agent architecture
46
+ const PluginManager = require('../lib/plugin-manager');
47
+
45
48
  // Get the project root
46
49
  const projectRoot = process.env.INIT_CWD || process.cwd();
47
50
  const args = process.argv.slice(2);
@@ -49,91 +52,36 @@ const args = process.argv.slice(2);
49
52
  // Detected package manager
50
53
  let PKG_MANAGER = 'npm';
51
54
 
52
- // Agent definitions
53
- const AGENTS = {
54
- claude: {
55
- name: 'Claude Code',
56
- description: "Anthropic's CLI agent",
57
- dirs: ['.claude/commands', '.claude/rules', '.claude/skills/forge-workflow', '.claude/scripts'],
58
- hasCommands: true,
59
- hasSkill: true,
60
- linkFile: 'CLAUDE.md'
61
- },
62
- cursor: {
63
- name: 'Cursor',
64
- description: 'AI-first code editor',
65
- dirs: ['.cursor/rules', '.cursor/skills/forge-workflow'],
66
- hasSkill: true,
67
- linkFile: '.cursorrules',
68
- customSetup: 'cursor'
69
- },
70
- windsurf: {
71
- name: 'Windsurf',
72
- description: "Codeium's agentic IDE",
73
- dirs: ['.windsurf/workflows', '.windsurf/rules', '.windsurf/skills/forge-workflow'],
74
- hasSkill: true,
75
- linkFile: '.windsurfrules',
76
- needsConversion: true
77
- },
78
- kilocode: {
79
- name: 'Kilo Code',
80
- description: 'VS Code extension',
81
- dirs: ['.kilocode/workflows', '.kilocode/rules', '.kilocode/skills/forge-workflow'],
82
- hasSkill: true,
83
- needsConversion: true
84
- },
85
- antigravity: {
86
- name: 'Google Antigravity',
87
- description: "Google's agent IDE",
88
- dirs: ['.agent/workflows', '.agent/rules', '.agent/skills/forge-workflow'],
89
- hasSkill: true,
90
- linkFile: 'GEMINI.md',
91
- needsConversion: true
92
- },
93
- copilot: {
94
- name: 'GitHub Copilot',
95
- description: "GitHub's AI assistant",
96
- dirs: ['.github/prompts', '.github/instructions'],
97
- linkFile: '.github/copilot-instructions.md',
98
- needsConversion: true,
99
- promptFormat: true
100
- },
101
- continue: {
102
- name: 'Continue',
103
- description: 'Open-source AI assistant',
104
- dirs: ['.continue/prompts', '.continue/skills/forge-workflow'],
105
- hasSkill: true,
106
- needsConversion: true,
107
- continueFormat: true
108
- },
109
- opencode: {
110
- name: 'OpenCode',
111
- description: 'Open-source agent',
112
- dirs: ['.opencode/commands', '.opencode/skills/forge-workflow'],
113
- hasSkill: true,
114
- copyCommands: true
115
- },
116
- cline: {
117
- name: 'Cline',
118
- description: 'VS Code agent extension',
119
- dirs: ['.cline/skills/forge-workflow'],
120
- hasSkill: true,
121
- linkFile: '.clinerules'
122
- },
123
- roo: {
124
- name: 'Roo Code',
125
- description: 'Cline fork with modes',
126
- dirs: ['.roo/commands'],
127
- linkFile: '.clinerules',
128
- needsConversion: true
129
- },
130
- aider: {
131
- name: 'Aider',
132
- description: 'Terminal-based agent',
133
- dirs: [],
134
- customSetup: 'aider'
135
- }
136
- };
55
+ /**
56
+ * Load agent definitions from plugin architecture
57
+ * Maintains backwards compatibility with original AGENTS object structure
58
+ */
59
+ function loadAgentsFromPlugins() {
60
+ const pluginManager = new PluginManager();
61
+ const agents = {};
62
+
63
+ pluginManager.getAllPlugins().forEach((plugin, id) => {
64
+ // Convert plugin structure to AGENTS structure for backwards compatibility
65
+ agents[id] = {
66
+ name: plugin.name,
67
+ description: plugin.description || '',
68
+ dirs: Object.values(plugin.directories || {}),
69
+ hasCommands: plugin.capabilities?.commands || plugin.setup?.copyCommands || false,
70
+ hasSkill: plugin.capabilities?.skills || plugin.setup?.createSkill || false,
71
+ linkFile: plugin.files?.rootConfig || '',
72
+ customSetup: plugin.setup?.customSetup || '',
73
+ needsConversion: plugin.setup?.needsConversion || false,
74
+ copyCommands: plugin.setup?.copyCommands || false,
75
+ promptFormat: plugin.setup?.promptFormat || false,
76
+ continueFormat: plugin.setup?.continueFormat || false
77
+ };
78
+ });
79
+
80
+ return agents;
81
+ }
82
+
83
+ // Agent definitions - loaded from plugin system
84
+ const AGENTS = loadAgentsFromPlugins();
137
85
 
138
86
  // SECURITY: Freeze AGENTS to prevent runtime manipulation
139
87
  Object.freeze(AGENTS);
@@ -683,7 +631,12 @@ function detectProjectStatus() {
683
631
  agentsMdSize: 0,
684
632
  claudeMdSize: 0,
685
633
  agentsMdLines: 0,
686
- claudeMdLines: 0
634
+ claudeMdLines: 0,
635
+ // Project tools status
636
+ hasBeads: isBeadsInitialized(),
637
+ hasOpenSpec: isOpenSpecInitialized(),
638
+ beadsInstallType: checkForBeads(),
639
+ openspecInstallType: checkForOpenSpec()
687
640
  };
688
641
 
689
642
  // Get file sizes and line counts for context warnings
@@ -1670,8 +1623,9 @@ function minimalInstall() {
1670
1623
  console.log('');
1671
1624
  console.log('To configure for your AI coding agents, run:');
1672
1625
  console.log('');
1673
- console.log(' npx forge setup # Interactive setup (agents + API tokens)');
1674
- console.log(' bunx forge setup # Same with bun');
1626
+ console.log(' npm install -D lefthook # Install git hooks (one-time)');
1627
+ console.log(' npx forge setup # Interactive setup (agents + API tokens)');
1628
+ console.log(' bunx forge setup # Same with bun');
1675
1629
  console.log('');
1676
1630
  console.log('Or specify agents directly:');
1677
1631
  console.log(' npx forge setup --agents claude,cursor,windsurf');
@@ -2166,9 +2120,27 @@ function displaySetupSummary(selectedAgents) {
2166
2120
  console.log('');
2167
2121
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2168
2122
  console.log('');
2169
- console.log('Optional tools:');
2170
- console.log(` ${PKG_MANAGER} install -g @beads/bd && bd init`);
2171
- console.log(` ${PKG_MANAGER} install -g @fission-ai/openspec`);
2123
+ console.log('Project Tools Status:');
2124
+ console.log('');
2125
+
2126
+ // Beads status
2127
+ if (isBeadsInitialized()) {
2128
+ console.log(' ✓ Beads initialized - Track work: bd ready');
2129
+ } else if (checkForBeads()) {
2130
+ console.log(' ! Beads available - Run: bd init');
2131
+ } else {
2132
+ console.log(` - Beads not installed - Run: ${PKG_MANAGER} install -g @beads/bd && bd init`);
2133
+ }
2134
+
2135
+ // OpenSpec status
2136
+ if (isOpenSpecInitialized()) {
2137
+ console.log(' ✓ OpenSpec initialized - Specs in openspec/');
2138
+ } else if (checkForOpenSpec()) {
2139
+ console.log(' ! OpenSpec available - Run: openspec init');
2140
+ } else {
2141
+ console.log(` - OpenSpec not installed - Run: ${PKG_MANAGER} install -g @fission-ai/openspec`);
2142
+ }
2143
+
2172
2144
  console.log('');
2173
2145
  console.log('Start with: /status');
2174
2146
  console.log('');
@@ -2265,10 +2237,15 @@ async function interactiveSetup() {
2265
2237
  setupAgentsWithProgress(selectedAgents, claudeCommands, skipFiles);
2266
2238
 
2267
2239
  // =============================================
2268
- // STEP 2: External Services Configuration
2240
+ // STEP 2: Project Tools Setup
2241
+ // =============================================
2242
+ await setupProjectTools(rl, question);
2243
+
2244
+ // =============================================
2245
+ // STEP 3: External Services Configuration
2269
2246
  // =============================================
2270
2247
  console.log('');
2271
- console.log('STEP 2: External Services (Optional)');
2248
+ console.log('STEP 3: External Services (Optional)');
2272
2249
  console.log('=====================================');
2273
2250
 
2274
2251
  await configureExternalServices(rl, question, selectedAgents, projectStatus);
@@ -2400,6 +2377,374 @@ function showHelp() {
2400
2377
  console.log('');
2401
2378
  }
2402
2379
 
2380
+ // Install git hooks via lefthook
2381
+ // SECURITY: Uses execSync with HARDCODED strings only (no user input)
2382
+ function installGitHooks() {
2383
+ console.log('Installing git hooks (TDD enforcement)...');
2384
+
2385
+ // Check if lefthook.yml exists (it should, as it's in the package)
2386
+ const lefthookConfig = path.join(packageDir, 'lefthook.yml');
2387
+ const targetHooks = path.join(projectRoot, '.forge/hooks');
2388
+
2389
+ try {
2390
+ // Copy lefthook.yml to project root
2391
+ const lefthookTarget = path.join(projectRoot, 'lefthook.yml');
2392
+ if (!fs.existsSync(lefthookTarget)) {
2393
+ if (copyFile(lefthookConfig, 'lefthook.yml')) {
2394
+ console.log(' ✓ Created lefthook.yml');
2395
+ }
2396
+ }
2397
+
2398
+ // Copy check-tdd.js hook script
2399
+ const hookSource = path.join(packageDir, '.forge/hooks/check-tdd.js');
2400
+ if (fs.existsSync(hookSource)) {
2401
+ // Ensure .forge/hooks directory exists
2402
+ if (!fs.existsSync(targetHooks)) {
2403
+ fs.mkdirSync(targetHooks, { recursive: true });
2404
+ }
2405
+
2406
+ const hookTarget = path.join(targetHooks, 'check-tdd.js');
2407
+ if (copyFile(hookSource, hookTarget)) {
2408
+ console.log(' ✓ Created .forge/hooks/check-tdd.js');
2409
+
2410
+ // Make hook executable (Unix systems)
2411
+ try {
2412
+ fs.chmodSync(hookTarget, 0o755);
2413
+ } catch (err) {
2414
+ // Windows doesn't need chmod
2415
+ }
2416
+ }
2417
+ }
2418
+
2419
+ // Try to install lefthook hooks
2420
+ // SECURITY: Using execFileSync with hardcoded commands (no user input)
2421
+ try {
2422
+ // Try npx first (local install), fallback to global
2423
+ try {
2424
+ execFileSync('npx', ['lefthook', 'install'], { stdio: 'inherit', cwd: projectRoot });
2425
+ console.log(' ✓ Lefthook hooks installed (local)');
2426
+ } catch (npxErr) {
2427
+ // Fallback to global lefthook
2428
+ execFileSync('lefthook', ['version'], { stdio: 'ignore' });
2429
+ execFileSync('lefthook', ['install'], { stdio: 'inherit', cwd: projectRoot });
2430
+ console.log(' ✓ Lefthook hooks installed (global)');
2431
+ }
2432
+ } catch (err) {
2433
+ console.log(' ℹ Lefthook not found. Install it:');
2434
+ console.log(' npm install -D lefthook (recommended)');
2435
+ console.log(' OR: npm install -g lefthook (global)');
2436
+ console.log(' Then run: npx lefthook install');
2437
+ }
2438
+
2439
+ console.log('');
2440
+
2441
+ } catch (error) {
2442
+ console.log(' ⚠ Failed to install hooks:', error.message);
2443
+ console.log(' You can install manually later with: lefthook install');
2444
+ console.log('');
2445
+ }
2446
+ }
2447
+
2448
+ // Check if lefthook is already installed in project
2449
+ function checkForLefthook() {
2450
+ const pkgPath = path.join(projectRoot, 'package.json');
2451
+ if (!fs.existsSync(pkgPath)) return false;
2452
+
2453
+ try {
2454
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
2455
+ return !!(pkg.devDependencies?.lefthook || pkg.dependencies?.lefthook);
2456
+ } catch (err) {
2457
+ return false;
2458
+ }
2459
+ }
2460
+
2461
+ // Check if Beads is installed (global, local, or bunx-capable)
2462
+ function checkForBeads() {
2463
+ // Try global install first
2464
+ try {
2465
+ execFileSync('bd', ['version'], { stdio: 'ignore' });
2466
+ return 'global';
2467
+ } catch (err) {
2468
+ // Not global
2469
+ }
2470
+
2471
+ // Check if bunx can run it
2472
+ try {
2473
+ execFileSync('bunx', ['@beads/bd', 'version'], { stdio: 'ignore' });
2474
+ return 'bunx';
2475
+ } catch (err) {
2476
+ // Not bunx-capable
2477
+ }
2478
+
2479
+ // Check local project installation
2480
+ const pkgPath = path.join(projectRoot, 'package.json');
2481
+ if (!fs.existsSync(pkgPath)) return false;
2482
+
2483
+ try {
2484
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
2485
+ return !!(pkg.devDependencies?.['@beads/bd'] || pkg.dependencies?.['@beads/bd']) ? 'local' : false;
2486
+ } catch (err) {
2487
+ return false;
2488
+ }
2489
+ }
2490
+
2491
+ // Check if OpenSpec is installed
2492
+ function checkForOpenSpec() {
2493
+ // Try global install first
2494
+ try {
2495
+ execFileSync('openspec', ['version'], { stdio: 'ignore' });
2496
+ return 'global';
2497
+ } catch (err) {
2498
+ // Not global
2499
+ }
2500
+
2501
+ // Check if bunx can run it
2502
+ try {
2503
+ execFileSync('bunx', ['@fission-ai/openspec', 'version'], { stdio: 'ignore' });
2504
+ return 'bunx';
2505
+ } catch (err) {
2506
+ // Not bunx-capable
2507
+ }
2508
+
2509
+ // Check local project installation
2510
+ const pkgPath = path.join(projectRoot, 'package.json');
2511
+ if (!fs.existsSync(pkgPath)) return false;
2512
+
2513
+ try {
2514
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
2515
+ return !!(pkg.devDependencies?.['@fission-ai/openspec'] || pkg.dependencies?.['@fission-ai/openspec']) ? 'local' : false;
2516
+ } catch (err) {
2517
+ return false;
2518
+ }
2519
+ }
2520
+
2521
+ // Check if Beads is initialized in project
2522
+ function isBeadsInitialized() {
2523
+ return fs.existsSync(path.join(projectRoot, '.beads'));
2524
+ }
2525
+
2526
+ // Check if OpenSpec is initialized in project
2527
+ function isOpenSpecInitialized() {
2528
+ return fs.existsSync(path.join(projectRoot, 'openspec'));
2529
+ }
2530
+
2531
+ // Initialize Beads in the project
2532
+ function initializeBeads(installType) {
2533
+ console.log('Initializing Beads in project...');
2534
+
2535
+ try {
2536
+ // SECURITY: execFileSync with hardcoded commands
2537
+ if (installType === 'global') {
2538
+ execFileSync('bd', ['init'], { stdio: 'inherit', cwd: projectRoot });
2539
+ } else if (installType === 'bunx') {
2540
+ execFileSync('bunx', ['@beads/bd', 'init'], { stdio: 'inherit', cwd: projectRoot });
2541
+ } else if (installType === 'local') {
2542
+ execFileSync('npx', ['bd', 'init'], { stdio: 'inherit', cwd: projectRoot });
2543
+ }
2544
+ console.log(' ✓ Beads initialized');
2545
+ return true;
2546
+ } catch (err) {
2547
+ console.log(' ⚠ Failed to initialize Beads:', err.message);
2548
+ console.log(' Run manually: bd init');
2549
+ return false;
2550
+ }
2551
+ }
2552
+
2553
+ // Initialize OpenSpec in the project
2554
+ function initializeOpenSpec(installType) {
2555
+ console.log('Initializing OpenSpec in project...');
2556
+
2557
+ try {
2558
+ // SECURITY: execFileSync with hardcoded commands
2559
+ if (installType === 'global') {
2560
+ execFileSync('openspec', ['init'], { stdio: 'inherit', cwd: projectRoot });
2561
+ } else if (installType === 'bunx') {
2562
+ execFileSync('bunx', ['@fission-ai/openspec', 'init'], { stdio: 'inherit', cwd: projectRoot });
2563
+ } else if (installType === 'local') {
2564
+ execFileSync('npx', ['openspec', 'init'], { stdio: 'inherit', cwd: projectRoot });
2565
+ }
2566
+ console.log(' ✓ OpenSpec initialized');
2567
+ return true;
2568
+ } catch (err) {
2569
+ console.log(' ⚠ Failed to initialize OpenSpec:', err.message);
2570
+ console.log(' Run manually: openspec init');
2571
+ return false;
2572
+ }
2573
+ }
2574
+
2575
+ // Interactive setup for Beads and OpenSpec
2576
+ async function setupProjectTools(rl, question) {
2577
+ console.log('');
2578
+ console.log('═══════════════════════════════════════════════════════════');
2579
+ console.log(' STEP 2: Project Tools (Recommended)');
2580
+ console.log('═══════════════════════════════════════════════════════════');
2581
+ console.log('');
2582
+ console.log('Forge recommends two tools for enhanced workflows:');
2583
+ console.log('');
2584
+ console.log('• Beads - Git-backed issue tracking');
2585
+ console.log(' Persists tasks across sessions, tracks dependencies.');
2586
+ console.log(' Command: bd ready, bd create, bd close');
2587
+ console.log('');
2588
+ console.log('• OpenSpec - Spec-driven development');
2589
+ console.log(' Structured specifications for complex features.');
2590
+ console.log(' Command: openspec init, openspec status');
2591
+ console.log('');
2592
+
2593
+ // ========================================
2594
+ // BEADS SETUP
2595
+ // ========================================
2596
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2597
+ console.log('Beads Setup (Recommended)');
2598
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2599
+ console.log('');
2600
+
2601
+ const beadsInitialized = isBeadsInitialized();
2602
+ const beadsStatus = checkForBeads();
2603
+
2604
+ if (beadsInitialized) {
2605
+ console.log('✓ Beads is already initialized in this project');
2606
+ console.log('');
2607
+ } else if (beadsStatus) {
2608
+ // Already installed, just need to initialize
2609
+ console.log(`ℹ Beads is installed (${beadsStatus}), but not initialized`);
2610
+ const initBeads = await question('Initialize Beads in this project? (y/n): ');
2611
+
2612
+ if (initBeads.toLowerCase() === 'y') {
2613
+ initializeBeads(beadsStatus);
2614
+ } else {
2615
+ console.log('Skipped Beads initialization. Run manually: bd init');
2616
+ }
2617
+ console.log('');
2618
+ } else {
2619
+ // Not installed
2620
+ console.log('ℹ Beads is not installed');
2621
+ const installBeads = await question('Install Beads? (y/n): ');
2622
+
2623
+ if (installBeads.toLowerCase() === 'y') {
2624
+ console.log('');
2625
+ console.log('Choose installation method:');
2626
+ console.log(' 1. Global (recommended) - Available system-wide');
2627
+ console.log(' 2. Local - Project-specific devDependency');
2628
+ console.log(' 3. Bunx - Use via bunx (requires bun)');
2629
+ console.log('');
2630
+ const method = await question('Choose method (1-3): ');
2631
+
2632
+ console.log('');
2633
+ try {
2634
+ // SECURITY: execFileSync with hardcoded commands
2635
+ if (method === '1') {
2636
+ console.log('Installing Beads globally...');
2637
+ const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
2638
+ execFileSync(pkgManager, ['install', '-g', '@beads/bd'], { stdio: 'inherit' });
2639
+ console.log(' ✓ Beads installed globally');
2640
+ initializeBeads('global');
2641
+ } else if (method === '2') {
2642
+ console.log('Installing Beads locally...');
2643
+ const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
2644
+ execFileSync(pkgManager, ['install', '-D', '@beads/bd'], { stdio: 'inherit', cwd: projectRoot });
2645
+ console.log(' ✓ Beads installed locally');
2646
+ initializeBeads('local');
2647
+ } else if (method === '3') {
2648
+ console.log('Testing bunx capability...');
2649
+ try {
2650
+ execFileSync('bunx', ['@beads/bd', 'version'], { stdio: 'ignore' });
2651
+ console.log(' ✓ Bunx is available');
2652
+ initializeBeads('bunx');
2653
+ } catch (err) {
2654
+ console.log(' ⚠ Bunx not available. Install bun first: npm install -g bun');
2655
+ }
2656
+ } else {
2657
+ console.log('Invalid choice. Skipping Beads installation.');
2658
+ }
2659
+ } catch (err) {
2660
+ console.log(' ⚠ Failed to install Beads:', err.message);
2661
+ console.log(' Run manually: npm install -g @beads/bd && bd init');
2662
+ }
2663
+ console.log('');
2664
+ } else {
2665
+ console.log('Skipped Beads installation');
2666
+ console.log('');
2667
+ }
2668
+ }
2669
+
2670
+ // ========================================
2671
+ // OPENSPEC SETUP
2672
+ // ========================================
2673
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2674
+ console.log('OpenSpec Setup (Optional)');
2675
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2676
+ console.log('');
2677
+
2678
+ const openspecInitialized = isOpenSpecInitialized();
2679
+ const openspecStatus = checkForOpenSpec();
2680
+
2681
+ if (openspecInitialized) {
2682
+ console.log('✓ OpenSpec is already initialized in this project');
2683
+ console.log('');
2684
+ } else if (openspecStatus) {
2685
+ // Already installed, just need to initialize
2686
+ console.log(`ℹ OpenSpec is installed (${openspecStatus}), but not initialized`);
2687
+ const initOpenSpec = await question('Initialize OpenSpec in this project? (y/n): ');
2688
+
2689
+ if (initOpenSpec.toLowerCase() === 'y') {
2690
+ initializeOpenSpec(openspecStatus);
2691
+ } else {
2692
+ console.log('Skipped OpenSpec initialization. Run manually: openspec init');
2693
+ }
2694
+ console.log('');
2695
+ } else {
2696
+ // Not installed
2697
+ console.log('ℹ OpenSpec is not installed');
2698
+ const installOpenSpec = await question('Install OpenSpec? (y/n): ');
2699
+
2700
+ if (installOpenSpec.toLowerCase() === 'y') {
2701
+ console.log('');
2702
+ console.log('Choose installation method:');
2703
+ console.log(' 1. Global (recommended) - Available system-wide');
2704
+ console.log(' 2. Local - Project-specific devDependency');
2705
+ console.log(' 3. Bunx - Use via bunx (requires bun)');
2706
+ console.log('');
2707
+ const method = await question('Choose method (1-3): ');
2708
+
2709
+ console.log('');
2710
+ try {
2711
+ // SECURITY: execFileSync with hardcoded commands
2712
+ if (method === '1') {
2713
+ console.log('Installing OpenSpec globally...');
2714
+ const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
2715
+ execFileSync(pkgManager, ['install', '-g', '@fission-ai/openspec'], { stdio: 'inherit' });
2716
+ console.log(' ✓ OpenSpec installed globally');
2717
+ initializeOpenSpec('global');
2718
+ } else if (method === '2') {
2719
+ console.log('Installing OpenSpec locally...');
2720
+ const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
2721
+ execFileSync(pkgManager, ['install', '-D', '@fission-ai/openspec'], { stdio: 'inherit', cwd: projectRoot });
2722
+ console.log(' ✓ OpenSpec installed locally');
2723
+ initializeOpenSpec('local');
2724
+ } else if (method === '3') {
2725
+ console.log('Testing bunx capability...');
2726
+ try {
2727
+ execFileSync('bunx', ['@fission-ai/openspec', 'version'], { stdio: 'ignore' });
2728
+ console.log(' ✓ Bunx is available');
2729
+ initializeOpenSpec('bunx');
2730
+ } catch (err) {
2731
+ console.log(' ⚠ Bunx not available. Install bun first: npm install -g bun');
2732
+ }
2733
+ } else {
2734
+ console.log('Invalid choice. Skipping OpenSpec installation.');
2735
+ }
2736
+ } catch (err) {
2737
+ console.log(' ⚠ Failed to install OpenSpec:', err.message);
2738
+ console.log(' Run manually: npm install -g @fission-ai/openspec && openspec init');
2739
+ }
2740
+ console.log('');
2741
+ } else {
2742
+ console.log('Skipped OpenSpec installation');
2743
+ console.log('');
2744
+ }
2745
+ }
2746
+ }
2747
+
2403
2748
  // Quick setup with defaults
2404
2749
  async function quickSetup(selectedAgents, skipExternal) {
2405
2750
  showBanner('Quick Setup');
@@ -2422,6 +2767,55 @@ async function quickSetup(selectedAgents, skipExternal) {
2422
2767
  setupCoreDocs();
2423
2768
  console.log('');
2424
2769
 
2770
+ // Check if lefthook is installed, auto-install if not
2771
+ const hasLefthook = checkForLefthook();
2772
+ if (!hasLefthook) {
2773
+ console.log('📦 Installing lefthook for git hooks...');
2774
+ try {
2775
+ // SECURITY: execFileSync with hardcoded command
2776
+ execFileSync('npm', ['install', '-D', 'lefthook'], { stdio: 'inherit', cwd: projectRoot });
2777
+ console.log(' ✓ Lefthook installed');
2778
+ } catch (err) {
2779
+ console.log(' ⚠ Could not install lefthook automatically');
2780
+ console.log(' Run manually: npm install -D lefthook');
2781
+ }
2782
+ console.log('');
2783
+ }
2784
+
2785
+ // Auto-setup Beads in quick mode (non-interactive)
2786
+ const beadsStatus = checkForBeads();
2787
+ const beadsInitialized = isBeadsInitialized();
2788
+
2789
+ if (!beadsInitialized && beadsStatus) {
2790
+ console.log('📦 Initializing Beads...');
2791
+ initializeBeads(beadsStatus);
2792
+ console.log('');
2793
+ } else if (!beadsInitialized && !beadsStatus) {
2794
+ console.log('📦 Installing Beads globally...');
2795
+ try {
2796
+ // SECURITY: execFileSync with hardcoded command
2797
+ const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
2798
+ execFileSync(pkgManager, ['install', '-g', '@beads/bd'], { stdio: 'inherit' });
2799
+ console.log(' ✓ Beads installed globally');
2800
+ initializeBeads('global');
2801
+ } catch (err) {
2802
+ console.log(' ⚠ Could not install Beads automatically');
2803
+ console.log(' Run manually: npm install -g @beads/bd && bd init');
2804
+ }
2805
+ console.log('');
2806
+ }
2807
+
2808
+ // OpenSpec: skip in quick mode (optional tool)
2809
+ // Only initialize if already installed
2810
+ const openspecStatus = checkForOpenSpec();
2811
+ const openspecInitialized = isOpenSpecInitialized();
2812
+
2813
+ if (openspecStatus && !openspecInitialized) {
2814
+ console.log('📦 Initializing OpenSpec...');
2815
+ initializeOpenSpec(openspecStatus);
2816
+ console.log('');
2817
+ }
2818
+
2425
2819
  // Load Claude commands if needed
2426
2820
  let claudeCommands = {};
2427
2821
  if (selectedAgents.includes('claude')) {
@@ -2457,6 +2851,10 @@ async function quickSetup(selectedAgents, skipExternal) {
2457
2851
  console.log(` * ${agent.name}`);
2458
2852
  });
2459
2853
 
2854
+ // Install git hooks for TDD enforcement
2855
+ console.log('');
2856
+ installGitHooks();
2857
+
2460
2858
  // Configure external services with defaults (unless skipped)
2461
2859
  if (skipExternal) {
2462
2860
  console.log('');
@@ -2783,9 +3181,27 @@ async function interactiveSetupWithFlags(flags) {
2783
3181
  console.log('');
2784
3182
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2785
3183
  console.log('');
2786
- console.log('Optional tools:');
2787
- console.log(` ${PKG_MANAGER} install -g @beads/bd && bd init`);
2788
- console.log(` ${PKG_MANAGER} install -g @fission-ai/openspec`);
3184
+ console.log('Project Tools Status:');
3185
+ console.log('');
3186
+
3187
+ // Beads status
3188
+ if (isBeadsInitialized()) {
3189
+ console.log(' ✓ Beads initialized - Track work: bd ready');
3190
+ } else if (checkForBeads()) {
3191
+ console.log(' ! Beads available - Run: bd init');
3192
+ } else {
3193
+ console.log(` - Beads not installed - Run: ${PKG_MANAGER} install -g @beads/bd && bd init`);
3194
+ }
3195
+
3196
+ // OpenSpec status
3197
+ if (isOpenSpecInitialized()) {
3198
+ console.log(' ✓ OpenSpec initialized - Specs in openspec/');
3199
+ } else if (checkForOpenSpec()) {
3200
+ console.log(' ! OpenSpec available - Run: openspec init');
3201
+ } else {
3202
+ console.log(` - OpenSpec not installed - Run: ${PKG_MANAGER} install -g @fission-ai/openspec`);
3203
+ }
3204
+
2789
3205
  console.log('');
2790
3206
  console.log('Start with: /status');
2791
3207
  console.log('');
@@ -2878,6 +3294,10 @@ async function handleSetupCommand(selectedAgents, flags) {
2878
3294
  console.log('');
2879
3295
  console.log('Agent configuration complete!');
2880
3296
 
3297
+ // Install git hooks for TDD enforcement
3298
+ console.log('');
3299
+ installGitHooks();
3300
+
2881
3301
  // External services (unless skipped)
2882
3302
  await handleExternalServices(flags.skipExternal, selectedAgents);
2883
3303