forge-workflow 1.4.7 → 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
@@ -32,16 +32,19 @@
32
32
  * bunx forge setup --quick
33
33
  */
34
34
 
35
- const fs = require('fs');
36
- const path = require('path');
37
- const readline = require('readline');
38
- const { execSync } = require('child_process');
35
+ const fs = require('node:fs');
36
+ const path = require('node:path');
37
+ const readline = require('node:readline');
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);
@@ -188,6 +136,8 @@ function safeExec(cmd) {
188
136
  try {
189
137
  return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
190
138
  } catch (e) {
139
+ // Command execution failure is expected when tool is not installed or fails
140
+ // Returning null allows caller to handle missing tools gracefully
191
141
  return null;
192
142
  }
193
143
  }
@@ -212,7 +162,7 @@ function checkPrerequisites() {
212
162
  // Check GitHub CLI
213
163
  const ghVersion = safeExec('gh --version');
214
164
  if (ghVersion) {
215
- console.log(` ✓ ${ghVersion.split('\\n')[0]}`);
165
+ console.log(` ✓ ${ghVersion.split(String.raw`\n`)[0]}`);
216
166
  // Check if authenticated
217
167
  const authStatus = safeExec('gh auth status');
218
168
  if (!authStatus) {
@@ -223,7 +173,7 @@ function checkPrerequisites() {
223
173
  }
224
174
 
225
175
  // Check Node.js version
226
- const nodeVersion = parseInt(process.version.slice(1).split('.')[0]);
176
+ const nodeVersion = Number.parseInt(process.version.slice(1).split('.')[0]);
227
177
  if (nodeVersion >= 20) {
228
178
  console.log(` ✓ node ${process.version}`);
229
179
  } else {
@@ -434,10 +384,8 @@ function copyFile(src, dest) {
434
384
  }
435
385
  fs.copyFileSync(src, destPath);
436
386
  return true;
437
- } else {
438
- if (process.env.DEBUG) {
439
- console.warn(` ⚠ Source file not found: ${src}`);
440
- }
387
+ } else if (process.env.DEBUG) {
388
+ console.warn(` ⚠ Source file not found: ${src}`);
441
389
  }
442
390
  } catch (err) {
443
391
  console.error(` ✗ Failed to copy ${src} -> ${dest}: ${err.message}`);
@@ -472,7 +420,9 @@ function createSymlinkOrCopy(source, target) {
472
420
  const relPath = path.relative(targetDir, fullSource);
473
421
  fs.symlinkSync(relPath, fullTarget);
474
422
  return 'linked';
475
- } catch (symlinkErr) {
423
+ } catch (error_) {
424
+ // Symlink creation may fail due to permissions or OS limitations (e.g., Windows without admin)
425
+ // Fall back to copying the file instead to ensure operation succeeds
476
426
  fs.copyFileSync(fullSource, fullTarget);
477
427
  return 'copied';
478
428
  }
@@ -494,7 +444,10 @@ function readEnvFile() {
494
444
  if (fs.existsSync(envPath)) {
495
445
  return fs.readFileSync(envPath, 'utf8');
496
446
  }
497
- } catch (err) {}
447
+ } catch (err) {
448
+ // File read failure is acceptable - file may not exist or have permission issues
449
+ // Return empty string to allow caller to proceed with defaults
450
+ }
498
451
  return '';
499
452
  }
500
453
 
@@ -535,7 +488,7 @@ function writeEnvTokens(tokens, preserveExisting = true) {
535
488
 
536
489
  // Add/update tokens - PRESERVE existing values if preserveExisting is true
537
490
  Object.entries(tokens).forEach(([key, value]) => {
538
- if (value && value.trim()) {
491
+ if (value?.trim()) {
539
492
  if (preserveExisting && existingKeys.has(key)) {
540
493
  // Keep existing value, don't overwrite
541
494
  preserved.push(key);
@@ -552,12 +505,14 @@ function writeEnvTokens(tokens, preserveExisting = true) {
552
505
 
553
506
  // Add header if new file
554
507
  if (!content.includes('# External Service API Keys')) {
555
- outputLines.push('# External Service API Keys for Forge Workflow');
556
- outputLines.push('# Get your keys from:');
557
- outputLines.push('# Parallel AI: https://platform.parallel.ai');
558
- outputLines.push('# Greptile: https://app.greptile.com/api');
559
- outputLines.push('# SonarCloud: https://sonarcloud.io/account/security');
560
- outputLines.push('');
508
+ outputLines.push(
509
+ '# External Service API Keys for Forge Workflow',
510
+ '# Get your keys from:',
511
+ '# Parallel AI: https://platform.parallel.ai',
512
+ '# Greptile: https://app.greptile.com/api',
513
+ '# SonarCloud: https://sonarcloud.io/account/security',
514
+ ''
515
+ );
561
516
  }
562
517
 
563
518
  // Add existing content (preserve order and comments)
@@ -591,7 +546,10 @@ function writeEnvTokens(tokens, preserveExisting = true) {
591
546
  if (!gitignore.includes('.env.local')) {
592
547
  fs.appendFileSync(gitignorePath, '\n# Local environment variables\n.env.local\n');
593
548
  }
594
- } catch (err) {}
549
+ } catch (err) {
550
+ // Gitignore update is optional - failure doesn't prevent .env.local creation
551
+ // User can manually add .env.local to .gitignore if needed
552
+ }
595
553
 
596
554
  return { added, preserved };
597
555
  }
@@ -648,7 +606,7 @@ async function askYesNo(question, prompt, defaultNo = true) {
648
606
  const normalized = answer.trim().toLowerCase();
649
607
 
650
608
  // Handle empty input (use default)
651
- if (normalized === '') return defaultNo ? false : true;
609
+ if (normalized === '') return !defaultNo;
652
610
 
653
611
  // Accept yes variations
654
612
  if (normalized === 'y' || normalized === 'yes') return true;
@@ -673,7 +631,12 @@ function detectProjectStatus() {
673
631
  agentsMdSize: 0,
674
632
  claudeMdSize: 0,
675
633
  agentsMdLines: 0,
676
- claudeMdLines: 0
634
+ claudeMdLines: 0,
635
+ // Project tools status
636
+ hasBeads: isBeadsInitialized(),
637
+ hasOpenSpec: isOpenSpecInitialized(),
638
+ beadsInstallType: checkForBeads(),
639
+ openspecInstallType: checkForOpenSpec()
677
640
  };
678
641
 
679
642
  // Get file sizes and line counts for context warnings
@@ -709,6 +672,238 @@ function detectProjectStatus() {
709
672
  return status;
710
673
  }
711
674
 
675
+ // Helper: Detect test framework from dependencies
676
+ function detectTestFramework(deps) {
677
+ if (deps.jest) return 'jest';
678
+ if (deps.vitest) return 'vitest';
679
+ if (deps.mocha) return 'mocha';
680
+ if (deps['@playwright/test']) return 'playwright';
681
+ if (deps.cypress) return 'cypress';
682
+ if (deps.karma) return 'karma';
683
+ return null;
684
+ }
685
+
686
+ // Helper: Detect language features (TypeScript, monorepo, Docker, CI/CD)
687
+ function detectLanguageFeatures(pkg) {
688
+ const features = {
689
+ typescript: false,
690
+ monorepo: false,
691
+ docker: false,
692
+ cicd: false
693
+ };
694
+
695
+ // Detect TypeScript
696
+ if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) {
697
+ features.typescript = true;
698
+ }
699
+
700
+ // Detect monorepo
701
+ if (pkg.workspaces ||
702
+ fs.existsSync(path.join(projectRoot, 'pnpm-workspace.yaml')) ||
703
+ fs.existsSync(path.join(projectRoot, 'lerna.json'))) {
704
+ features.monorepo = true;
705
+ }
706
+
707
+ // Detect Docker
708
+ if (fs.existsSync(path.join(projectRoot, 'Dockerfile')) ||
709
+ fs.existsSync(path.join(projectRoot, 'docker-compose.yml'))) {
710
+ features.docker = true;
711
+ }
712
+
713
+ // Detect CI/CD
714
+ if (fs.existsSync(path.join(projectRoot, '.github/workflows')) ||
715
+ fs.existsSync(path.join(projectRoot, '.gitlab-ci.yml')) ||
716
+ fs.existsSync(path.join(projectRoot, 'azure-pipelines.yml')) ||
717
+ fs.existsSync(path.join(projectRoot, '.circleci/config.yml'))) {
718
+ features.cicd = true;
719
+ }
720
+
721
+ return features;
722
+ }
723
+
724
+ // Helper: Detect Next.js framework
725
+ function detectNextJs(deps) {
726
+ if (!deps.next) return null;
727
+
728
+ return {
729
+ framework: 'Next.js',
730
+ frameworkConfidence: 100,
731
+ projectType: 'fullstack',
732
+ buildTool: 'next',
733
+ testFramework: detectTestFramework(deps)
734
+ };
735
+ }
736
+
737
+ // Helper: Detect NestJS framework
738
+ function detectNestJs(deps) {
739
+ if (!deps['@nestjs/core'] && !deps['@nestjs/common']) return null;
740
+
741
+ return {
742
+ framework: 'NestJS',
743
+ frameworkConfidence: 100,
744
+ projectType: 'backend',
745
+ buildTool: 'nest',
746
+ testFramework: 'jest'
747
+ };
748
+ }
749
+
750
+ // Helper: Detect Angular framework
751
+ function detectAngular(deps) {
752
+ if (!deps['@angular/core'] && !deps['@angular/cli']) return null;
753
+
754
+ return {
755
+ framework: 'Angular',
756
+ frameworkConfidence: 100,
757
+ projectType: 'frontend',
758
+ buildTool: 'ng',
759
+ testFramework: 'karma'
760
+ };
761
+ }
762
+
763
+ // Helper: Detect Vue.js framework
764
+ function detectVue(deps) {
765
+ if (!deps.vue) return null;
766
+
767
+ if (deps.nuxt) {
768
+ return {
769
+ framework: 'Nuxt',
770
+ frameworkConfidence: 100,
771
+ projectType: 'fullstack',
772
+ buildTool: 'nuxt',
773
+ testFramework: detectTestFramework(deps)
774
+ };
775
+ }
776
+
777
+ const hasVite = deps.vite;
778
+ const hasWebpack = deps.webpack;
779
+
780
+ return {
781
+ framework: 'Vue.js',
782
+ frameworkConfidence: deps['@vue/cli'] ? 100 : 90,
783
+ projectType: 'frontend',
784
+ buildTool: hasVite ? 'vite' : (hasWebpack ? 'webpack' : 'vue-cli'),
785
+ testFramework: detectTestFramework(deps)
786
+ };
787
+ }
788
+
789
+ // Helper: Detect React framework
790
+ function detectReact(deps) {
791
+ if (!deps.react) return null;
792
+
793
+ const hasVite = deps.vite;
794
+ const hasReactScripts = deps['react-scripts'];
795
+
796
+ return {
797
+ framework: 'React',
798
+ frameworkConfidence: 95,
799
+ projectType: 'frontend',
800
+ buildTool: hasVite ? 'vite' : (hasReactScripts ? 'create-react-app' : 'webpack'),
801
+ testFramework: detectTestFramework(deps)
802
+ };
803
+ }
804
+
805
+ // Helper: Detect Express framework
806
+ function detectExpress(deps, features) {
807
+ if (!deps.express) return null;
808
+
809
+ return {
810
+ framework: 'Express',
811
+ frameworkConfidence: 90,
812
+ projectType: 'backend',
813
+ buildTool: features.typescript ? 'tsc' : 'node',
814
+ testFramework: detectTestFramework(deps)
815
+ };
816
+ }
817
+
818
+ // Helper: Detect Fastify framework
819
+ function detectFastify(deps, features) {
820
+ if (!deps.fastify) return null;
821
+
822
+ return {
823
+ framework: 'Fastify',
824
+ frameworkConfidence: 95,
825
+ projectType: 'backend',
826
+ buildTool: features.typescript ? 'tsc' : 'node',
827
+ testFramework: detectTestFramework(deps)
828
+ };
829
+ }
830
+
831
+ // Helper: Detect Svelte framework
832
+ function detectSvelte(deps) {
833
+ if (!deps.svelte) return null;
834
+
835
+ if (deps['@sveltejs/kit']) {
836
+ return {
837
+ framework: 'SvelteKit',
838
+ frameworkConfidence: 100,
839
+ projectType: 'fullstack',
840
+ buildTool: 'vite',
841
+ testFramework: detectTestFramework(deps)
842
+ };
843
+ }
844
+
845
+ return {
846
+ framework: 'Svelte',
847
+ frameworkConfidence: 95,
848
+ projectType: 'frontend',
849
+ buildTool: 'vite',
850
+ testFramework: detectTestFramework(deps)
851
+ };
852
+ }
853
+
854
+ // Helper: Detect Remix framework
855
+ function detectRemix(deps) {
856
+ if (!deps['@remix-run/react']) return null;
857
+
858
+ return {
859
+ framework: 'Remix',
860
+ frameworkConfidence: 100,
861
+ projectType: 'fullstack',
862
+ buildTool: 'remix',
863
+ testFramework: detectTestFramework(deps)
864
+ };
865
+ }
866
+
867
+ // Helper: Detect Astro framework
868
+ function detectAstro(deps) {
869
+ if (!deps.astro) return null;
870
+
871
+ return {
872
+ framework: 'Astro',
873
+ frameworkConfidence: 100,
874
+ projectType: 'frontend',
875
+ buildTool: 'astro',
876
+ testFramework: detectTestFramework(deps)
877
+ };
878
+ }
879
+
880
+ // Helper: Detect generic Node.js project
881
+ function detectGenericNodeJs(pkg, deps, features) {
882
+ if (!pkg.main && !pkg.scripts?.start) return null;
883
+
884
+ return {
885
+ framework: 'Node.js',
886
+ frameworkConfidence: 70,
887
+ projectType: 'backend',
888
+ buildTool: features.typescript ? 'tsc' : 'node',
889
+ testFramework: detectTestFramework(deps)
890
+ };
891
+ }
892
+
893
+ // Helper: Detect generic JavaScript/TypeScript project (fallback)
894
+ function detectGenericProject(deps, features) {
895
+ const hasVite = deps.vite;
896
+ const hasWebpack = deps.webpack;
897
+
898
+ return {
899
+ framework: features.typescript ? 'TypeScript' : 'JavaScript',
900
+ frameworkConfidence: 60,
901
+ projectType: 'library',
902
+ buildTool: hasVite ? 'vite' : (hasWebpack ? 'webpack' : 'npm'),
903
+ testFramework: detectTestFramework(deps)
904
+ };
905
+ }
906
+
712
907
  // Detect project type from package.json
713
908
  function detectProjectType() {
714
909
  const detection = {
@@ -733,174 +928,34 @@ function detectProjectType() {
733
928
 
734
929
  detection.hasPackageJson = true;
735
930
 
736
- // Detect TypeScript
737
- if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) {
738
- detection.features.typescript = true;
931
+ // Detect language features
932
+ detection.features = detectLanguageFeatures(pkg);
933
+ if (detection.features.typescript) {
739
934
  detection.language = 'typescript';
740
935
  }
741
936
 
742
- // Detect monorepo
743
- if (pkg.workspaces || fs.existsSync(path.join(projectRoot, 'pnpm-workspace.yaml')) || fs.existsSync(path.join(projectRoot, 'lerna.json'))) {
744
- detection.features.monorepo = true;
745
- }
746
-
747
- // Detect Docker
748
- if (fs.existsSync(path.join(projectRoot, 'Dockerfile')) || fs.existsSync(path.join(projectRoot, 'docker-compose.yml'))) {
749
- detection.features.docker = true;
750
- }
751
-
752
- // Detect CI/CD
753
- if (fs.existsSync(path.join(projectRoot, '.github/workflows')) ||
754
- fs.existsSync(path.join(projectRoot, '.gitlab-ci.yml')) ||
755
- fs.existsSync(path.join(projectRoot, 'azure-pipelines.yml')) ||
756
- fs.existsSync(path.join(projectRoot, '.circleci/config.yml'))) {
757
- detection.features.cicd = true;
758
- }
759
-
760
937
  // Framework detection with confidence scoring
761
938
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
762
939
 
763
- // Helper function for test framework detection
764
- const detectTestFramework = (deps) => {
765
- if (deps.jest) return 'jest';
766
- if (deps.vitest) return 'vitest';
767
- if (deps.mocha) return 'mocha';
768
- if (deps['@playwright/test']) return 'playwright';
769
- if (deps.cypress) return 'cypress';
770
- if (deps.karma) return 'karma';
771
- return null;
772
- };
773
-
774
- // Next.js (highest priority for React projects)
775
- if (deps.next) {
776
- detection.framework = 'Next.js';
777
- detection.frameworkConfidence = 100;
778
- detection.projectType = 'fullstack';
779
- detection.buildTool = 'next';
780
- detection.testFramework = detectTestFramework(deps);
781
- return detection;
782
- }
783
-
784
- // NestJS (backend framework)
785
- if (deps['@nestjs/core'] || deps['@nestjs/common']) {
786
- detection.framework = 'NestJS';
787
- detection.frameworkConfidence = 100;
788
- detection.projectType = 'backend';
789
- detection.buildTool = 'nest';
790
- detection.testFramework = 'jest';
791
- return detection;
792
- }
793
-
794
- // Angular
795
- if (deps['@angular/core'] || deps['@angular/cli']) {
796
- detection.framework = 'Angular';
797
- detection.frameworkConfidence = 100;
798
- detection.projectType = 'frontend';
799
- detection.buildTool = 'ng';
800
- detection.testFramework = 'karma';
801
- return detection;
802
- }
803
-
804
- // Vue.js
805
- if (deps.vue) {
806
- if (deps.nuxt) {
807
- detection.framework = 'Nuxt';
808
- detection.frameworkConfidence = 100;
809
- detection.projectType = 'fullstack';
810
- detection.buildTool = 'nuxt';
811
- } else {
812
- detection.framework = 'Vue.js';
813
- detection.frameworkConfidence = deps['@vue/cli'] ? 100 : 90;
814
- detection.projectType = 'frontend';
815
- detection.buildTool = deps.vite ? 'vite' : deps.webpack ? 'webpack' : 'vue-cli';
816
- }
817
- detection.testFramework = detectTestFramework(deps);
818
- return detection;
819
- }
820
-
821
- // React (without Next.js)
822
- if (deps.react) {
823
- detection.framework = 'React';
824
- detection.frameworkConfidence = 95;
825
- detection.projectType = 'frontend';
826
- detection.buildTool = deps.vite ? 'vite' : deps['react-scripts'] ? 'create-react-app' : 'webpack';
827
- detection.testFramework = detectTestFramework(deps);
828
- return detection;
829
- }
830
-
831
- // Express (backend)
832
- if (deps.express) {
833
- detection.framework = 'Express';
834
- detection.frameworkConfidence = 90;
835
- detection.projectType = 'backend';
836
- detection.buildTool = detection.features.typescript ? 'tsc' : 'node';
837
- detection.testFramework = detectTestFramework(deps);
838
- return detection;
839
- }
840
-
841
- // Fastify (backend)
842
- if (deps.fastify) {
843
- detection.framework = 'Fastify';
844
- detection.frameworkConfidence = 95;
845
- detection.projectType = 'backend';
846
- detection.buildTool = detection.features.typescript ? 'tsc' : 'node';
847
- detection.testFramework = detectTestFramework(deps);
848
- return detection;
849
- }
850
-
851
- // Svelte
852
- if (deps.svelte) {
853
- if (deps['@sveltejs/kit']) {
854
- detection.framework = 'SvelteKit';
855
- detection.frameworkConfidence = 100;
856
- detection.projectType = 'fullstack';
857
- detection.buildTool = 'vite';
858
- } else {
859
- detection.framework = 'Svelte';
860
- detection.frameworkConfidence = 95;
861
- detection.projectType = 'frontend';
862
- detection.buildTool = 'vite';
863
- }
864
- detection.testFramework = detectTestFramework(deps);
865
- return detection;
866
- }
867
-
868
- // Remix
869
- if (deps['@remix-run/react']) {
870
- detection.framework = 'Remix';
871
- detection.frameworkConfidence = 100;
872
- detection.projectType = 'fullstack';
873
- detection.buildTool = 'remix';
874
- detection.testFramework = detectTestFramework(deps);
875
- return detection;
876
- }
877
-
878
- // Astro
879
- if (deps.astro) {
880
- detection.framework = 'Astro';
881
- detection.frameworkConfidence = 100;
882
- detection.projectType = 'frontend';
883
- detection.buildTool = 'astro';
884
- detection.testFramework = detectTestFramework(deps);
885
- return detection;
886
- }
887
-
888
- // Generic Node.js project
889
- if (pkg.main || pkg.scripts?.start) {
890
- detection.framework = 'Node.js';
891
- detection.frameworkConfidence = 70;
892
- detection.projectType = 'backend';
893
- detection.buildTool = detection.features.typescript ? 'tsc' : 'node';
894
- detection.testFramework = detectTestFramework(deps);
895
- return detection;
896
- }
897
-
898
- // Fallback: generic JavaScript/TypeScript project
899
- detection.framework = detection.features.typescript ? 'TypeScript' : 'JavaScript';
900
- detection.frameworkConfidence = 60;
901
- detection.projectType = 'library';
902
- detection.buildTool = deps.vite ? 'vite' : deps.webpack ? 'webpack' : 'npm';
903
- detection.testFramework = detectTestFramework(deps);
940
+ // Try framework detectors in priority order
941
+ const frameworkResult =
942
+ detectNextJs(deps) ||
943
+ detectNestJs(deps) ||
944
+ detectAngular(deps) ||
945
+ detectVue(deps) ||
946
+ detectReact(deps) ||
947
+ detectExpress(deps, detection.features) ||
948
+ detectFastify(deps, detection.features) ||
949
+ detectSvelte(deps) ||
950
+ detectRemix(deps) ||
951
+ detectAstro(deps) ||
952
+ detectGenericNodeJs(pkg, deps, detection.features) ||
953
+ detectGenericProject(deps, detection.features);
954
+
955
+ // Merge framework detection results
956
+ if (frameworkResult) {
957
+ Object.assign(detection, frameworkResult);
958
+ }
904
959
 
905
960
  return detection;
906
961
  }
@@ -1045,9 +1100,7 @@ function updateAgentsMdWithProjectType(detection) {
1045
1100
  // Add framework-specific tips
1046
1101
  const tips = generateFrameworkTips(detection);
1047
1102
  if (tips.length > 0) {
1048
- metadata.push('');
1049
- metadata.push('**Framework conventions**:');
1050
- metadata.push(...tips);
1103
+ metadata.push('', '**Framework conventions**:', ...tips);
1051
1104
  }
1052
1105
 
1053
1106
  // Insert metadata
@@ -1056,142 +1109,149 @@ function updateAgentsMdWithProjectType(detection) {
1056
1109
  fs.writeFileSync(agentsPath, lines.join('\n'), 'utf-8');
1057
1110
  }
1058
1111
 
1059
- // Smart file selection with context warnings
1060
- async function handleInstructionFiles(rl, question, selectedAgents, projectStatus) {
1061
- const hasClaude = selectedAgents.some(a => a.key === 'claude');
1062
- const hasOtherAgents = selectedAgents.some(a => a.key !== 'claude');
1063
-
1064
- // Calculate estimated tokens (rough: ~4 chars per token)
1065
- const estimateTokens = (bytes) => Math.ceil(bytes / 4);
1112
+ // Helper: Calculate estimated tokens (rough: ~4 chars per token)
1113
+ function estimateTokens(bytes) {
1114
+ return Math.ceil(bytes / 4);
1115
+ }
1066
1116
 
1067
- const result = {
1068
- createAgentsMd: false,
1069
- createClaudeMd: false,
1070
- skipAgentsMd: false,
1071
- skipClaudeMd: false
1117
+ // Helper: Create instruction files result object
1118
+ function createInstructionFilesResult(createAgentsMd = false, createClaudeMd = false, skipAgentsMd = false, skipClaudeMd = false) {
1119
+ return {
1120
+ createAgentsMd,
1121
+ createClaudeMd,
1122
+ skipAgentsMd,
1123
+ skipClaudeMd
1072
1124
  };
1125
+ }
1073
1126
 
1074
- // Scenario 1: Both files exist (potential context bloat)
1075
- if (projectStatus.hasAgentsMd && projectStatus.hasClaudeMd) {
1076
- const totalLines = projectStatus.agentsMdLines + projectStatus.claudeMdLines;
1077
- const totalTokens = estimateTokens(projectStatus.agentsMdSize + projectStatus.claudeMdSize);
1127
+ // Helper: Handle scenario where both AGENTS.md and CLAUDE.md exist
1128
+ async function handleBothFilesExist(question, projectStatus) {
1129
+ const totalLines = projectStatus.agentsMdLines + projectStatus.claudeMdLines;
1130
+ const totalTokens = estimateTokens(projectStatus.agentsMdSize + projectStatus.claudeMdSize);
1078
1131
 
1079
- console.log('');
1080
- console.log('⚠️ WARNING: Multiple Instruction Files Detected');
1081
- console.log('='.repeat(60));
1082
- console.log(` AGENTS.md: ${projectStatus.agentsMdLines} lines (~${estimateTokens(projectStatus.agentsMdSize)} tokens)`);
1083
- console.log(` CLAUDE.md: ${projectStatus.claudeMdLines} lines (~${estimateTokens(projectStatus.claudeMdSize)} tokens)`);
1084
- console.log(` Total: ${totalLines} lines (~${totalTokens} tokens)`);
1085
- console.log('');
1086
- console.log(' ⚠️ Claude Code reads BOTH files on every request');
1087
- console.log(' ⚠️ This increases context usage and costs');
1088
- console.log('');
1089
- console.log(' Options:');
1090
- console.log(' 1) Keep CLAUDE.md only (recommended for Claude Code only)');
1091
- console.log(' 2) Keep AGENTS.md only (recommended for multi-agent users)');
1092
- console.log(' 3) Keep both (higher context usage)');
1093
- console.log('');
1132
+ console.log('');
1133
+ console.log('⚠️ WARNING: Multiple Instruction Files Detected');
1134
+ console.log('='.repeat(60));
1135
+ console.log(` AGENTS.md: ${projectStatus.agentsMdLines} lines (~${estimateTokens(projectStatus.agentsMdSize)} tokens)`);
1136
+ console.log(` CLAUDE.md: ${projectStatus.claudeMdLines} lines (~${estimateTokens(projectStatus.claudeMdSize)} tokens)`);
1137
+ console.log(` Total: ${totalLines} lines (~${totalTokens} tokens)`);
1138
+ console.log('');
1139
+ console.log(' ⚠️ Claude Code reads BOTH files on every request');
1140
+ console.log(' ⚠️ This increases context usage and costs');
1141
+ console.log('');
1142
+ console.log(' Options:');
1143
+ console.log(' 1) Keep CLAUDE.md only (recommended for Claude Code only)');
1144
+ console.log(' 2) Keep AGENTS.md only (recommended for multi-agent users)');
1145
+ console.log(' 3) Keep both (higher context usage)');
1146
+ console.log('');
1094
1147
 
1095
- while (true) {
1096
- const choice = await question('Your choice (1/2/3) [2]: ');
1097
- const normalized = choice.trim() || '2';
1098
-
1099
- if (normalized === '1') {
1100
- result.skipAgentsMd = true;
1101
- result.createClaudeMd = false; // Keep existing
1102
- console.log(' ✓ Will keep CLAUDE.md, remove AGENTS.md');
1103
- break;
1104
- } else if (normalized === '2') {
1105
- result.skipClaudeMd = true;
1106
- result.createAgentsMd = false; // Keep existing
1107
- console.log(' ✓ Will keep AGENTS.md, remove CLAUDE.md');
1108
- break;
1109
- } else if (normalized === '3') {
1110
- result.createAgentsMd = false; // Keep existing
1111
- result.createClaudeMd = false; // Keep existing
1112
- console.log(' ✓ Will keep both files (context: ~' + totalTokens + ' tokens)');
1113
- break;
1114
- } else {
1115
- console.log(' Please enter 1, 2, or 3');
1116
- }
1148
+ while (true) {
1149
+ const choice = await question('Your choice (1/2/3) [2]: ');
1150
+ const normalized = choice.trim() || '2';
1151
+
1152
+ if (normalized === '1') {
1153
+ console.log(' ✓ Will keep CLAUDE.md, remove AGENTS.md');
1154
+ return createInstructionFilesResult(false, false, true, false);
1155
+ } else if (normalized === '2') {
1156
+ console.log(' ✓ Will keep AGENTS.md, remove CLAUDE.md');
1157
+ return createInstructionFilesResult(false, false, false, true);
1158
+ } else if (normalized === '3') {
1159
+ console.log(' ✓ Will keep both files (context: ~' + totalTokens + ' tokens)');
1160
+ return createInstructionFilesResult(false, false, false, false);
1161
+ } else {
1162
+ console.log(' Please enter 1, 2, or 3');
1117
1163
  }
1118
-
1119
- return result;
1120
1164
  }
1165
+ }
1121
1166
 
1122
- // Scenario 2: Only CLAUDE.md exists
1123
- if (projectStatus.hasClaudeMd && !projectStatus.hasAgentsMd) {
1124
- if (hasOtherAgents) {
1125
- console.log('');
1126
- console.log('📋 Found existing CLAUDE.md (' + projectStatus.claudeMdLines + ' lines)');
1127
- console.log(' You selected multiple agents. Recommendation:');
1128
- console.log(' → Migrate to AGENTS.md (works with all agents)');
1129
- console.log('');
1167
+ // Helper: Handle scenario where only CLAUDE.md exists
1168
+ async function handleOnlyClaudeMdExists(question, projectStatus, hasOtherAgents) {
1169
+ if (hasOtherAgents) {
1170
+ console.log('');
1171
+ console.log('📋 Found existing CLAUDE.md (' + projectStatus.claudeMdLines + ' lines)');
1172
+ console.log(' You selected multiple agents. Recommendation:');
1173
+ console.log(' → Migrate to AGENTS.md (works with all agents)');
1174
+ console.log('');
1130
1175
 
1131
- const migrate = await askYesNo(question, 'Migrate CLAUDE.md to AGENTS.md?', false);
1132
- if (migrate) {
1133
- result.createAgentsMd = true;
1134
- result.skipClaudeMd = true;
1135
- console.log(' ✓ Will migrate content to AGENTS.md');
1136
- } else {
1137
- result.createAgentsMd = true;
1138
- result.createClaudeMd = false; // Keep existing
1139
- console.log(' ✓ Will keep CLAUDE.md and create AGENTS.md');
1140
- }
1176
+ const migrate = await askYesNo(question, 'Migrate CLAUDE.md to AGENTS.md?', false);
1177
+ if (migrate) {
1178
+ console.log(' ✓ Will migrate content to AGENTS.md');
1179
+ return createInstructionFilesResult(true, false, false, true);
1141
1180
  } else {
1142
- // Claude Code only - keep CLAUDE.md
1143
- result.createClaudeMd = false; // Keep existing
1144
- console.log(' ✓ Keeping existing CLAUDE.md');
1181
+ console.log(' ✓ Will keep CLAUDE.md and create AGENTS.md');
1182
+ return createInstructionFilesResult(true, false, false, false);
1145
1183
  }
1146
-
1147
- return result;
1184
+ } else {
1185
+ // Claude Code only - keep CLAUDE.md
1186
+ console.log(' ✓ Keeping existing CLAUDE.md');
1187
+ return createInstructionFilesResult(false, false, false, false);
1148
1188
  }
1189
+ }
1149
1190
 
1150
- // Scenario 3: Only AGENTS.md exists
1151
- if (projectStatus.hasAgentsMd && !projectStatus.hasClaudeMd) {
1152
- if (hasClaude && !hasOtherAgents) {
1153
- console.log('');
1154
- console.log('📋 Found existing AGENTS.md (' + projectStatus.agentsMdLines + ' lines)');
1155
- console.log(' You selected Claude Code only. Options:');
1156
- console.log(' 1) Keep AGENTS.md (works fine)');
1157
- console.log(' 2) Rename to CLAUDE.md (Claude-specific naming)');
1158
- console.log('');
1191
+ // Helper: Handle scenario where only AGENTS.md exists
1192
+ async function handleOnlyAgentsMdExists(question, projectStatus, hasClaude, hasOtherAgents) {
1193
+ if (hasClaude && !hasOtherAgents) {
1194
+ console.log('');
1195
+ console.log('📋 Found existing AGENTS.md (' + projectStatus.agentsMdLines + ' lines)');
1196
+ console.log(' You selected Claude Code only. Options:');
1197
+ console.log(' 1) Keep AGENTS.md (works fine)');
1198
+ console.log(' 2) Rename to CLAUDE.md (Claude-specific naming)');
1199
+ console.log('');
1159
1200
 
1160
- const rename = await askYesNo(question, 'Rename to CLAUDE.md?', true);
1161
- if (rename) {
1162
- result.createClaudeMd = true;
1163
- result.skipAgentsMd = true;
1164
- console.log(' ✓ Will rename to CLAUDE.md');
1165
- } else {
1166
- result.createAgentsMd = false; // Keep existing
1167
- console.log(' ✓ Keeping AGENTS.md');
1168
- }
1201
+ const rename = await askYesNo(question, 'Rename to CLAUDE.md?', true);
1202
+ if (rename) {
1203
+ console.log(' ✓ Will rename to CLAUDE.md');
1204
+ return createInstructionFilesResult(false, true, true, false);
1169
1205
  } else {
1170
- // Multi-agent or other agents - keep AGENTS.md
1171
- result.createAgentsMd = false; // Keep existing
1172
- console.log(' ✓ Keeping existing AGENTS.md');
1206
+ console.log(' ✓ Keeping AGENTS.md');
1207
+ return createInstructionFilesResult(false, false, false, false);
1173
1208
  }
1174
-
1175
- return result;
1209
+ } else {
1210
+ // Multi-agent or other agents - keep AGENTS.md
1211
+ console.log(' ✓ Keeping existing AGENTS.md');
1212
+ return createInstructionFilesResult(false, false, false, false);
1176
1213
  }
1214
+ }
1177
1215
 
1178
- // Scenario 4: Neither file exists (fresh install)
1216
+ // Helper: Handle scenario where no instruction files exist (fresh install)
1217
+ function handleNoFilesExist(hasClaude, hasOtherAgents) {
1179
1218
  if (hasClaude && !hasOtherAgents) {
1180
1219
  // Claude Code only → create CLAUDE.md
1181
- result.createClaudeMd = true;
1182
1220
  console.log(' ✓ Will create CLAUDE.md (Claude Code specific)');
1221
+ return createInstructionFilesResult(false, true, false, false);
1183
1222
  } else if (!hasClaude && hasOtherAgents) {
1184
1223
  // Other agents only → create AGENTS.md
1185
- result.createAgentsMd = true;
1186
1224
  console.log(' ✓ Will create AGENTS.md (universal)');
1225
+ return createInstructionFilesResult(true, false, false, false);
1187
1226
  } else {
1188
1227
  // Multiple agents including Claude → create AGENTS.md + reference CLAUDE.md
1189
- result.createAgentsMd = true;
1190
- result.createClaudeMd = true; // Will be minimal reference
1191
1228
  console.log(' ✓ Will create AGENTS.md (main) + CLAUDE.md (reference)');
1229
+ return createInstructionFilesResult(true, true, false, false);
1230
+ }
1231
+ }
1232
+
1233
+ // Smart file selection with context warnings
1234
+ async function handleInstructionFiles(rl, question, selectedAgents, projectStatus) {
1235
+ const hasClaude = selectedAgents.some(a => a.key === 'claude');
1236
+ const hasOtherAgents = selectedAgents.some(a => a.key !== 'claude');
1237
+
1238
+ // Scenario 1: Both files exist (potential context bloat)
1239
+ if (projectStatus.hasAgentsMd && projectStatus.hasClaudeMd) {
1240
+ return await handleBothFilesExist(question, projectStatus);
1241
+ }
1242
+
1243
+ // Scenario 2: Only CLAUDE.md exists
1244
+ if (projectStatus.hasClaudeMd && !projectStatus.hasAgentsMd) {
1245
+ return await handleOnlyClaudeMdExists(question, projectStatus, hasOtherAgents);
1192
1246
  }
1193
1247
 
1194
- return result;
1248
+ // Scenario 3: Only AGENTS.md exists
1249
+ if (projectStatus.hasAgentsMd && !projectStatus.hasClaudeMd) {
1250
+ return await handleOnlyAgentsMdExists(question, projectStatus, hasClaude, hasOtherAgents);
1251
+ }
1252
+
1253
+ // Scenario 4: Neither file exists (fresh install)
1254
+ return handleNoFilesExist(hasClaude, hasOtherAgents);
1195
1255
  }
1196
1256
 
1197
1257
  // Create minimal CLAUDE.md that references AGENTS.md
@@ -1284,18 +1344,20 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
1284
1344
  const codeReviewChoice = await question('Select [1]: ') || '1';
1285
1345
 
1286
1346
  switch (codeReviewChoice) {
1287
- case '1':
1347
+ case '1': {
1288
1348
  tokens['CODE_REVIEW_TOOL'] = 'github-code-quality';
1289
1349
  console.log(' ✓ Using GitHub Code Quality (FREE)');
1290
1350
  break;
1291
- case '2':
1351
+ }
1352
+ case '2': {
1292
1353
  tokens['CODE_REVIEW_TOOL'] = 'coderabbit';
1293
1354
  console.log(' ✓ Using CodeRabbit - Install the GitHub App to activate');
1294
1355
  console.log(' https://coderabbit.ai');
1295
1356
  break;
1296
- case '3':
1357
+ }
1358
+ case '3': {
1297
1359
  const greptileKey = await question(' Enter Greptile API key: ');
1298
- if (greptileKey && greptileKey.trim()) {
1360
+ if (greptileKey?.trim()) {
1299
1361
  tokens['CODE_REVIEW_TOOL'] = 'greptile';
1300
1362
  tokens['GREPTILE_API_KEY'] = greptileKey.trim();
1301
1363
  console.log(' ✓ Greptile configured');
@@ -1304,9 +1366,11 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
1304
1366
  console.log(' Skipped - No API key provided');
1305
1367
  }
1306
1368
  break;
1307
- default:
1369
+ }
1370
+ default: {
1308
1371
  tokens['CODE_REVIEW_TOOL'] = 'none';
1309
1372
  console.log(' Skipped code review integration');
1373
+ }
1310
1374
  }
1311
1375
 
1312
1376
  // ============================================
@@ -1332,15 +1396,16 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
1332
1396
  const codeQualityChoice = await question('Select [1]: ') || '1';
1333
1397
 
1334
1398
  switch (codeQualityChoice) {
1335
- case '1':
1399
+ case '1': {
1336
1400
  tokens['CODE_QUALITY_TOOL'] = 'eslint';
1337
1401
  console.log(' ✓ Using ESLint (built-in)');
1338
1402
  break;
1339
- case '2':
1403
+ }
1404
+ case '2': {
1340
1405
  const sonarToken = await question(' Enter SonarCloud token: ');
1341
1406
  const sonarOrg = await question(' Enter SonarCloud organization: ');
1342
1407
  const sonarProject = await question(' Enter SonarCloud project key: ');
1343
- if (sonarToken && sonarToken.trim()) {
1408
+ if (sonarToken?.trim()) {
1344
1409
  tokens['CODE_QUALITY_TOOL'] = 'sonarcloud';
1345
1410
  tokens['SONAR_TOKEN'] = sonarToken.trim();
1346
1411
  if (sonarOrg) tokens['SONAR_ORGANIZATION'] = sonarOrg.trim();
@@ -1351,7 +1416,8 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
1351
1416
  console.log(' Falling back to ESLint');
1352
1417
  }
1353
1418
  break;
1354
- case '3':
1419
+ }
1420
+ case '3': {
1355
1421
  console.log('');
1356
1422
  console.log(' SonarQube Self-Hosted Setup:');
1357
1423
  console.log(' docker run -d --name sonarqube -p 9000:9000 sonarqube:community');
@@ -1361,14 +1427,16 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
1361
1427
  const sqToken = await question(' Enter SonarQube token (optional): ');
1362
1428
  tokens['CODE_QUALITY_TOOL'] = 'sonarqube';
1363
1429
  tokens['SONARQUBE_URL'] = sqUrl;
1364
- if (sqToken && sqToken.trim()) {
1430
+ if (sqToken?.trim()) {
1365
1431
  tokens['SONARQUBE_TOKEN'] = sqToken.trim();
1366
1432
  }
1367
1433
  console.log(' ✓ SonarQube self-hosted configured');
1368
1434
  break;
1369
- default:
1435
+ }
1436
+ default: {
1370
1437
  tokens['CODE_QUALITY_TOOL'] = 'none';
1371
1438
  console.log(' Skipped code quality integration');
1439
+ }
1372
1440
  }
1373
1441
 
1374
1442
  // ============================================
@@ -1390,7 +1458,7 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
1390
1458
 
1391
1459
  if (researchChoice === '2') {
1392
1460
  const parallelKey = await question(' Enter Parallel AI API key: ');
1393
- if (parallelKey && parallelKey.trim()) {
1461
+ if (parallelKey?.trim()) {
1394
1462
  tokens['PARALLEL_API_KEY'] = parallelKey.trim();
1395
1463
  console.log(' ✓ Parallel AI configured');
1396
1464
  } else {
@@ -1555,8 +1623,9 @@ function minimalInstall() {
1555
1623
  console.log('');
1556
1624
  console.log('To configure for your AI coding agents, run:');
1557
1625
  console.log('');
1558
- console.log(' npx forge setup # Interactive setup (agents + API tokens)');
1559
- 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');
1560
1629
  console.log('');
1561
1630
  console.log('Or specify agents directly:');
1562
1631
  console.log(' npx forge setup --agents claude,cursor,windsurf');
@@ -1564,137 +1633,150 @@ function minimalInstall() {
1564
1633
  console.log('');
1565
1634
  }
1566
1635
 
1567
- // Setup specific agent
1568
- function setupAgent(agentKey, claudeCommands, skipFiles = {}) {
1569
- const agent = AGENTS[agentKey];
1570
- if (!agent) return;
1571
-
1572
- console.log(`\nSetting up ${agent.name}...`);
1573
-
1574
- // Create directories
1575
- agent.dirs.forEach(dir => ensureDir(dir));
1636
+ // Helper: Setup Claude agent
1637
+ function setupClaudeAgent(skipFiles = {}) {
1638
+ // Copy commands from package (unless skipped)
1639
+ if (skipFiles.claudeCommands) {
1640
+ console.log(' Skipped: .claude/commands/ (keeping existing)');
1641
+ } else {
1642
+ COMMANDS.forEach(cmd => {
1643
+ const src = path.join(packageDir, `.claude/commands/${cmd}.md`);
1644
+ copyFile(src, `.claude/commands/${cmd}.md`);
1645
+ });
1646
+ console.log(' Copied: 9 workflow commands');
1647
+ }
1576
1648
 
1577
- // Handle Claude Code specifically (downloads commands)
1578
- if (agentKey === 'claude') {
1579
- // Copy commands from package (unless skipped)
1580
- if (skipFiles.claudeCommands) {
1581
- console.log(' Skipped: .claude/commands/ (keeping existing)');
1582
- } else {
1583
- COMMANDS.forEach(cmd => {
1584
- const src = path.join(packageDir, `.claude/commands/${cmd}.md`);
1585
- copyFile(src, `.claude/commands/${cmd}.md`);
1586
- });
1587
- console.log(' Copied: 9 workflow commands');
1588
- }
1649
+ // Copy rules
1650
+ const rulesSrc = path.join(packageDir, '.claude/rules/workflow.md');
1651
+ copyFile(rulesSrc, '.claude/rules/workflow.md');
1589
1652
 
1590
- // Copy rules
1591
- const rulesSrc = path.join(packageDir, '.claude/rules/workflow.md');
1592
- copyFile(rulesSrc, '.claude/rules/workflow.md');
1653
+ // Copy scripts
1654
+ const scriptSrc = path.join(packageDir, '.claude/scripts/load-env.sh');
1655
+ copyFile(scriptSrc, '.claude/scripts/load-env.sh');
1656
+ }
1593
1657
 
1594
- // Copy scripts
1595
- const scriptSrc = path.join(packageDir, '.claude/scripts/load-env.sh');
1596
- copyFile(scriptSrc, '.claude/scripts/load-env.sh');
1597
- }
1658
+ // Helper: Setup Cursor agent
1659
+ function setupCursorAgent() {
1660
+ writeFile('.cursor/rules/forge-workflow.mdc', CURSOR_RULE);
1661
+ console.log(' Created: .cursor/rules/forge-workflow.mdc');
1662
+ }
1598
1663
 
1599
- // Custom setups
1600
- if (agent.customSetup === 'cursor') {
1601
- writeFile('.cursor/rules/forge-workflow.mdc', CURSOR_RULE);
1602
- console.log(' Created: .cursor/rules/forge-workflow.mdc');
1664
+ // Helper: Setup Aider agent
1665
+ function setupAiderAgent() {
1666
+ const aiderPath = path.join(projectRoot, '.aider.conf.yml');
1667
+ if (fs.existsSync(aiderPath)) {
1668
+ console.log(' Skipped: .aider.conf.yml already exists');
1669
+ return true; // Signal early return
1603
1670
  }
1604
1671
 
1605
- if (agent.customSetup === 'aider') {
1606
- const aiderPath = path.join(projectRoot, '.aider.conf.yml');
1607
- if (!fs.existsSync(aiderPath)) {
1608
- writeFile('.aider.conf.yml', `# Aider configuration
1672
+ writeFile('.aider.conf.yml', `# Aider configuration
1609
1673
  # Read AGENTS.md for workflow instructions
1610
1674
  read:
1611
1675
  - AGENTS.md
1612
1676
  - docs/WORKFLOW.md
1613
1677
  `);
1614
- console.log(' Created: .aider.conf.yml');
1615
- } else {
1616
- console.log(' Skipped: .aider.conf.yml already exists');
1617
- }
1618
- return;
1619
- }
1678
+ console.log(' Created: .aider.conf.yml');
1679
+ return true; // Signal early return
1680
+ }
1620
1681
 
1621
- // Convert/copy commands
1622
- if (claudeCommands && (agent.needsConversion || agent.copyCommands || agent.promptFormat || agent.continueFormat)) {
1623
- Object.entries(claudeCommands).forEach(([cmd, content]) => {
1624
- let targetContent = content;
1625
- let targetFile = cmd;
1682
+ // Helper: Convert command to agent-specific format
1683
+ function convertCommandToAgentFormat(cmd, content, agent) {
1684
+ let targetContent = content;
1685
+ let targetFile = cmd;
1626
1686
 
1627
- if (agent.needsConversion) {
1628
- targetContent = stripFrontmatter(content);
1629
- }
1687
+ if (agent.needsConversion) {
1688
+ targetContent = stripFrontmatter(content);
1689
+ }
1630
1690
 
1631
- if (agent.promptFormat) {
1632
- targetFile = cmd.replace('.md', '.prompt.md');
1633
- targetContent = stripFrontmatter(content);
1634
- }
1691
+ if (agent.promptFormat) {
1692
+ targetFile = cmd.replace('.md', '.prompt.md');
1693
+ targetContent = stripFrontmatter(content);
1694
+ }
1635
1695
 
1636
- if (agent.continueFormat) {
1637
- const baseName = cmd.replace('.md', '');
1638
- targetFile = `${baseName}.prompt`;
1639
- targetContent = `---
1696
+ if (agent.continueFormat) {
1697
+ const baseName = cmd.replace('.md', '');
1698
+ targetFile = `${baseName}.prompt`;
1699
+ targetContent = `---
1640
1700
  name: ${baseName}
1641
1701
  description: Forge workflow command - ${baseName}
1642
1702
  invokable: true
1643
1703
  ---
1644
1704
 
1645
1705
  ${stripFrontmatter(content)}`;
1646
- }
1647
-
1648
- const targetDir = agent.dirs[0]; // First dir is commands/workflows
1649
- writeFile(`${targetDir}/${targetFile}`, targetContent);
1650
- });
1651
- console.log(' Converted: 9 workflow commands');
1652
1706
  }
1653
1707
 
1654
- // Copy rules if needed
1655
- if (agent.needsConversion && fs.existsSync(path.join(projectRoot, '.claude/rules/workflow.md'))) {
1656
- const rulesDir = agent.dirs.find(d => d.includes('/rules'));
1657
- if (rulesDir) {
1658
- const ruleContent = readFile(path.join(projectRoot, '.claude/rules/workflow.md'));
1659
- if (ruleContent) {
1660
- writeFile(`${rulesDir}/workflow.md`, ruleContent);
1661
- }
1662
- }
1708
+ return { targetFile, targetContent };
1709
+ }
1710
+
1711
+ // Helper: Copy commands for agent
1712
+ function copyAgentCommands(agent, claudeCommands) {
1713
+ if (!claudeCommands) return;
1714
+ if (!agent.needsConversion && !agent.copyCommands && !agent.promptFormat && !agent.continueFormat) return;
1715
+
1716
+ Object.entries(claudeCommands).forEach(([cmd, content]) => {
1717
+ const { targetFile, targetContent } = convertCommandToAgentFormat(cmd, content, agent);
1718
+ const targetDir = agent.dirs[0]; // First dir is commands/workflows
1719
+ writeFile(`${targetDir}/${targetFile}`, targetContent);
1720
+ });
1721
+ console.log(' Converted: 9 workflow commands');
1722
+ }
1723
+
1724
+ // Helper: Copy rules for agent
1725
+ function copyAgentRules(agent) {
1726
+ if (!agent.needsConversion) return;
1727
+
1728
+ const workflowMdPath = path.join(projectRoot, '.claude/rules/workflow.md');
1729
+ if (!fs.existsSync(workflowMdPath)) return;
1730
+
1731
+ const rulesDir = agent.dirs.find(d => d.includes('/rules'));
1732
+ if (!rulesDir) return;
1733
+
1734
+ const ruleContent = readFile(workflowMdPath);
1735
+ if (ruleContent) {
1736
+ writeFile(`${rulesDir}/workflow.md`, ruleContent);
1663
1737
  }
1738
+ }
1664
1739
 
1665
- // Create SKILL.md
1666
- if (agent.hasSkill) {
1667
- const skillDir = agent.dirs.find(d => d.includes('/skills/'));
1668
- if (skillDir) {
1669
- writeFile(`${skillDir}/SKILL.md`, SKILL_CONTENT);
1670
- console.log(' Created: forge-workflow skill');
1671
- }
1740
+ // Helper: Create skill file for agent
1741
+ function createAgentSkill(agent) {
1742
+ if (!agent.hasSkill) return;
1743
+
1744
+ const skillDir = agent.dirs.find(d => d.includes('/skills/'));
1745
+ if (skillDir) {
1746
+ writeFile(`${skillDir}/SKILL.md`, SKILL_CONTENT);
1747
+ console.log(' Created: forge-workflow skill');
1672
1748
  }
1749
+ }
1673
1750
 
1674
- // Create .mcp.json with Context7 MCP (Claude Code only)
1675
- if (agentKey === 'claude') {
1676
- const mcpPath = path.join(projectRoot, '.mcp.json');
1677
- if (!fs.existsSync(mcpPath)) {
1678
- const mcpConfig = {
1679
- mcpServers: {
1680
- context7: {
1681
- command: 'npx',
1682
- args: ['-y', '@upstash/context7-mcp@latest']
1683
- }
1684
- }
1685
- };
1686
- writeFile('.mcp.json', JSON.stringify(mcpConfig, null, 2));
1687
- console.log(' Created: .mcp.json with Context7 MCP');
1688
- } else {
1689
- console.log(' Skipped: .mcp.json already exists');
1690
- }
1751
+ // Helper: Setup MCP config for Claude
1752
+ function setupClaudeMcpConfig() {
1753
+ const mcpPath = path.join(projectRoot, '.mcp.json');
1754
+ if (fs.existsSync(mcpPath)) {
1755
+ console.log(' Skipped: .mcp.json already exists');
1756
+ return;
1691
1757
  }
1692
1758
 
1693
- // Create config.yaml with Context7 MCP (Continue only)
1694
- if (agentKey === 'continue') {
1695
- const configPath = path.join(projectRoot, '.continue/config.yaml');
1696
- if (!fs.existsSync(configPath)) {
1697
- const continueConfig = `# Continue Configuration
1759
+ const mcpConfig = {
1760
+ mcpServers: {
1761
+ context7: {
1762
+ command: 'npx',
1763
+ args: ['-y', '@upstash/context7-mcp@latest']
1764
+ }
1765
+ }
1766
+ };
1767
+ writeFile('.mcp.json', JSON.stringify(mcpConfig, null, 2));
1768
+ console.log(' Created: .mcp.json with Context7 MCP');
1769
+ }
1770
+
1771
+ // Helper: Setup MCP config for Continue
1772
+ function setupContinueMcpConfig() {
1773
+ const configPath = path.join(projectRoot, '.continue/config.yaml');
1774
+ if (fs.existsSync(configPath)) {
1775
+ console.log(' Skipped: config.yaml already exists');
1776
+ return;
1777
+ }
1778
+
1779
+ const continueConfig = `# Continue Configuration
1698
1780
  # https://docs.continue.dev/customize/deep-dives/configuration
1699
1781
 
1700
1782
  name: Forge Workflow
@@ -1710,22 +1792,363 @@ mcpServers:
1710
1792
 
1711
1793
  # Rules loaded from .continuerules
1712
1794
  `;
1713
- writeFile('.continue/config.yaml', continueConfig);
1714
- console.log(' Created: config.yaml with Context7 MCP');
1715
- } else {
1716
- console.log(' Skipped: config.yaml already exists');
1717
- }
1795
+ writeFile('.continue/config.yaml', continueConfig);
1796
+ console.log(' Created: config.yaml with Context7 MCP');
1797
+ }
1798
+
1799
+ // Helper: Create agent link file
1800
+ function createAgentLinkFile(agent) {
1801
+ if (!agent.linkFile) return;
1802
+
1803
+ const result = createSymlinkOrCopy('AGENTS.md', agent.linkFile);
1804
+ if (result) {
1805
+ console.log(` ${result === 'linked' ? 'Linked' : 'Copied'}: ${agent.linkFile}`);
1806
+ }
1807
+ }
1808
+
1809
+ // Setup specific agent
1810
+ function setupAgent(agentKey, claudeCommands, skipFiles = {}) {
1811
+ const agent = AGENTS[agentKey];
1812
+ if (!agent) return;
1813
+
1814
+ console.log(`\nSetting up ${agent.name}...`);
1815
+
1816
+ // Create directories
1817
+ agent.dirs.forEach(dir => ensureDir(dir));
1818
+
1819
+ // Handle agent-specific setup
1820
+ if (agentKey === 'claude') {
1821
+ setupClaudeAgent(skipFiles);
1822
+ }
1823
+
1824
+ if (agent.customSetup === 'cursor') {
1825
+ setupCursorAgent();
1826
+ }
1827
+
1828
+ if (agent.customSetup === 'aider') {
1829
+ const shouldReturn = setupAiderAgent();
1830
+ if (shouldReturn) return;
1831
+ }
1832
+
1833
+ // Convert/copy commands
1834
+ copyAgentCommands(agent, claudeCommands);
1835
+
1836
+ // Copy rules if needed
1837
+ copyAgentRules(agent);
1838
+
1839
+ // Create SKILL.md
1840
+ createAgentSkill(agent);
1841
+
1842
+ // Setup MCP configs
1843
+ if (agentKey === 'claude') {
1844
+ setupClaudeMcpConfig();
1845
+ }
1846
+
1847
+ if (agentKey === 'continue') {
1848
+ setupContinueMcpConfig();
1718
1849
  }
1719
1850
 
1720
1851
  // Create link file
1721
- if (agent.linkFile) {
1722
- const result = createSymlinkOrCopy('AGENTS.md', agent.linkFile);
1723
- if (result) {
1724
- console.log(` ${result === 'linked' ? 'Linked' : 'Copied'}: ${agent.linkFile}`);
1852
+ createAgentLinkFile(agent);
1853
+ }
1854
+
1855
+
1856
+ // =============================================
1857
+ // Helper Functions for Interactive Setup
1858
+ // =============================================
1859
+
1860
+ /**
1861
+ * Display existing installation status
1862
+ */
1863
+ function displayInstallationStatus(projectStatus) {
1864
+ if (projectStatus.type === 'fresh') return;
1865
+
1866
+ console.log('==============================================');
1867
+ console.log(' Existing Installation Detected');
1868
+ console.log('==============================================');
1869
+ console.log('');
1870
+
1871
+ if (projectStatus.type === 'upgrade') {
1872
+ console.log('Found existing Forge installation:');
1873
+ } else {
1874
+ console.log('Found partial installation:');
1875
+ }
1876
+
1877
+ if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
1878
+ if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
1879
+ if (projectStatus.hasEnvLocal) console.log(' - .env.local');
1880
+ if (projectStatus.hasDocsWorkflow) console.log(' - docs/WORKFLOW.md');
1881
+ console.log('');
1882
+ }
1883
+
1884
+ /**
1885
+ * Prompt for file overwrite and update skipFiles
1886
+ */
1887
+ async function promptForFileOverwrite(question, fileType, exists, skipFiles) {
1888
+ if (!exists) return;
1889
+
1890
+ const fileLabels = {
1891
+ agentsMd: { prompt: 'Found existing AGENTS.md. Overwrite?', message: 'AGENTS.md', key: 'agentsMd' },
1892
+ claudeCommands: { prompt: 'Found existing .claude/commands/. Overwrite?', message: '.claude/commands/', key: 'claudeCommands' }
1893
+ };
1894
+
1895
+ const config = fileLabels[fileType];
1896
+ if (!config) return;
1897
+
1898
+ const overwrite = await askYesNo(question, config.prompt, true);
1899
+ if (overwrite) {
1900
+ console.log(` Will overwrite ${config.message}`);
1901
+ } else {
1902
+ skipFiles[config.key] = true;
1903
+ console.log(` Keeping existing ${config.message}`);
1904
+ }
1905
+ }
1906
+
1907
+ /**
1908
+ * Display agent selection options
1909
+ */
1910
+ function displayAgentOptions(agentKeys) {
1911
+ console.log('STEP 1: Select AI Coding Agents');
1912
+ console.log('================================');
1913
+ console.log('');
1914
+ console.log('Which AI coding agents do you use?');
1915
+ console.log('(Enter numbers separated by spaces, or "all")');
1916
+ console.log('');
1917
+
1918
+ agentKeys.forEach((key, index) => {
1919
+ const agent = AGENTS[key];
1920
+ console.log(` ${(index + 1).toString().padStart(2)}) ${agent.name.padEnd(20)} - ${agent.description}`);
1921
+ });
1922
+ console.log('');
1923
+ console.log(' all) Install for all agents');
1924
+ console.log('');
1925
+ }
1926
+
1927
+ /**
1928
+ * Validate and parse agent selection input
1929
+ */
1930
+ function validateAgentSelection(input, agentKeys) {
1931
+ // Handle empty input
1932
+ if (!input || !input.trim()) {
1933
+ return { valid: false, agents: [], message: 'Please enter at least one agent number or "all".' };
1934
+ }
1935
+
1936
+ // Handle "all" selection
1937
+ if (input.toLowerCase() === 'all') {
1938
+ return { valid: true, agents: agentKeys, message: null };
1939
+ }
1940
+
1941
+ // Parse numbers
1942
+ const nums = input.split(/[\s,]+/).map(n => Number.parseInt(n.trim())).filter(n => !Number.isNaN(n));
1943
+
1944
+ // Validate numbers are in range
1945
+ const validNums = nums.filter(n => n >= 1 && n <= agentKeys.length);
1946
+ const invalidNums = nums.filter(n => n < 1 || n > agentKeys.length);
1947
+
1948
+ if (invalidNums.length > 0) {
1949
+ console.log(` ⚠ Invalid numbers ignored: ${invalidNums.join(', ')} (valid: 1-${agentKeys.length})`);
1950
+ }
1951
+
1952
+ // Deduplicate selected agents using Set
1953
+ const selectedAgents = [...new Set(validNums.map(n => agentKeys[n - 1]))].filter(Boolean);
1954
+
1955
+ if (selectedAgents.length === 0) {
1956
+ return { valid: false, agents: [], message: 'No valid agents selected. Please try again.' };
1957
+ }
1958
+
1959
+ return { valid: true, agents: selectedAgents, message: null };
1960
+ }
1961
+
1962
+ /**
1963
+ * Prompt for agent selection with validation loop
1964
+ */
1965
+ async function promptForAgentSelection(question, agentKeys) {
1966
+ displayAgentOptions(agentKeys);
1967
+
1968
+ let selectedAgents = [];
1969
+
1970
+ // Loop until valid input is provided
1971
+ while (selectedAgents.length === 0) {
1972
+ const answer = await question('Your selection: ');
1973
+ const result = validateAgentSelection(answer, agentKeys);
1974
+
1975
+ if (result.valid) {
1976
+ selectedAgents = result.agents;
1977
+ } else if (result.message) {
1978
+ console.log(` ${result.message}`);
1979
+ }
1980
+ }
1981
+
1982
+ return selectedAgents;
1983
+ }
1984
+
1985
+ /**
1986
+ * Handle AGENTS.md installation
1987
+ */
1988
+ async function installAgentsMd(skipFiles) {
1989
+ if (skipFiles.agentsMd) {
1990
+ console.log(' Skipped: AGENTS.md (keeping existing)');
1991
+ return;
1992
+ }
1993
+
1994
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
1995
+ const agentsDest = path.join(projectRoot, 'AGENTS.md');
1996
+
1997
+ // Try smart merge if file exists
1998
+ if (fs.existsSync(agentsDest)) {
1999
+ const existingContent = fs.readFileSync(agentsDest, 'utf8');
2000
+ const newContent = fs.readFileSync(agentsSrc, 'utf8');
2001
+ const merged = smartMergeAgentsMd(existingContent, newContent);
2002
+
2003
+ if (merged) {
2004
+ fs.writeFileSync(agentsDest, merged, 'utf8');
2005
+ console.log(' Updated: AGENTS.md (preserved USER sections)');
2006
+ } else if (copyFile(agentsSrc, 'AGENTS.md')) {
2007
+ // No markers, do normal copy (user already approved overwrite)
2008
+ console.log(' Updated: AGENTS.md (universal standard)');
2009
+ }
2010
+ } else if (copyFile(agentsSrc, 'AGENTS.md')) {
2011
+ // New file
2012
+ console.log(' Created: AGENTS.md (universal standard)');
2013
+
2014
+ // Detect project type and update AGENTS.md
2015
+ const detection = detectProjectType();
2016
+ if (detection.hasPackageJson) {
2017
+ updateAgentsMdWithProjectType(detection);
2018
+ displayProjectType(detection);
2019
+ }
2020
+ }
2021
+ }
2022
+
2023
+ /**
2024
+ * Load Claude commands for conversion
2025
+ */
2026
+ function loadClaudeCommands(selectedAgents) {
2027
+ const claudeCommands = {};
2028
+ const needsClaudeCommands = selectedAgents.includes('claude') ||
2029
+ selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands);
2030
+
2031
+ if (!needsClaudeCommands) {
2032
+ return claudeCommands;
2033
+ }
2034
+
2035
+ COMMANDS.forEach(cmd => {
2036
+ const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
2037
+ const content = readFile(cmdPath);
2038
+ if (content) {
2039
+ claudeCommands[`${cmd}.md`] = content;
2040
+ }
2041
+ });
2042
+
2043
+ return claudeCommands;
2044
+ }
2045
+
2046
+ /**
2047
+ * Setup agents with progress indication
2048
+ */
2049
+ function setupAgentsWithProgress(selectedAgents, claudeCommands, skipFiles) {
2050
+ const totalAgents = selectedAgents.length;
2051
+
2052
+ selectedAgents.forEach((agentKey, index) => {
2053
+ const agent = AGENTS[agentKey];
2054
+ console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
2055
+ if (agentKey !== 'claude') { // Claude already done above
2056
+ setupAgent(agentKey, claudeCommands, skipFiles);
2057
+ }
2058
+ });
2059
+
2060
+ // Agent installation success
2061
+ console.log('');
2062
+ console.log('Agent configuration complete!');
2063
+ console.log('');
2064
+ console.log('Installed for:');
2065
+ selectedAgents.forEach(key => {
2066
+ const agent = AGENTS[key];
2067
+ console.log(` * ${agent.name}`);
2068
+ });
2069
+ }
2070
+
2071
+ /**
2072
+ * Display final setup summary
2073
+ */
2074
+ function displaySetupSummary(selectedAgents) {
2075
+ console.log('');
2076
+ console.log('==============================================');
2077
+ console.log(` Forge v${VERSION} Setup Complete!`);
2078
+ console.log('==============================================');
2079
+ console.log('');
2080
+ console.log('What\'s installed:');
2081
+ console.log(' - AGENTS.md (universal instructions)');
2082
+ console.log(' - docs/WORKFLOW.md (full workflow guide)');
2083
+ console.log(' - docs/research/TEMPLATE.md (research template)');
2084
+ console.log(' - docs/planning/PROGRESS.md (progress tracking)');
2085
+
2086
+ selectedAgents.forEach(key => {
2087
+ const agent = AGENTS[key];
2088
+ if (agent.linkFile) {
2089
+ console.log(` - ${agent.linkFile} (${agent.name})`);
2090
+ }
2091
+ if (agent.hasCommands) {
2092
+ console.log(` - .claude/commands/ (9 workflow commands)`);
2093
+ }
2094
+ if (agent.hasSkill) {
2095
+ const skillDir = agent.dirs.find(d => d.includes('/skills/'));
2096
+ if (skillDir) {
2097
+ console.log(` - ${skillDir}/SKILL.md`);
2098
+ }
1725
2099
  }
2100
+ });
2101
+
2102
+ console.log('');
2103
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2104
+ console.log('📋 NEXT STEP - Complete AGENTS.md');
2105
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2106
+ console.log('');
2107
+ console.log('Ask your AI agent:');
2108
+ console.log(' "Fill in the project description in AGENTS.md"');
2109
+ console.log('');
2110
+ console.log('The agent will:');
2111
+ console.log(' ✓ Add one-sentence project description');
2112
+ console.log(' ✓ Confirm package manager');
2113
+ console.log(' ✓ Verify build commands');
2114
+ console.log('');
2115
+ console.log('Takes ~30 seconds. Done!');
2116
+ console.log('');
2117
+ console.log('💡 As you work: Add project patterns to AGENTS.md');
2118
+ console.log(' USER:START section. Keep it minimal - budget is');
2119
+ console.log(' ~150-200 instructions max.');
2120
+ console.log('');
2121
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2122
+ console.log('');
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`);
1726
2142
  }
2143
+
2144
+ console.log('');
2145
+ console.log('Start with: /status');
2146
+ console.log('');
2147
+ console.log(`Package manager: ${PKG_MANAGER}`);
2148
+ console.log('');
1727
2149
  }
1728
2150
 
2151
+
1729
2152
  // Interactive setup
1730
2153
  async function interactiveSetup() {
1731
2154
  const rl = readline.createInterface({
@@ -1766,25 +2189,7 @@ async function interactiveSetup() {
1766
2189
  // PROJECT DETECTION
1767
2190
  // =============================================
1768
2191
  const projectStatus = detectProjectStatus();
1769
-
1770
- if (projectStatus.type !== 'fresh') {
1771
- console.log('==============================================');
1772
- console.log(' Existing Installation Detected');
1773
- console.log('==============================================');
1774
- console.log('');
1775
-
1776
- if (projectStatus.type === 'upgrade') {
1777
- console.log('Found existing Forge installation:');
1778
- } else {
1779
- console.log('Found partial installation:');
1780
- }
1781
-
1782
- if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
1783
- if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
1784
- if (projectStatus.hasEnvLocal) console.log(' - .env.local');
1785
- if (projectStatus.hasDocsWorkflow) console.log(' - docs/WORKFLOW.md');
1786
- console.log('');
1787
- }
2192
+ displayInstallationStatus(projectStatus);
1788
2193
 
1789
2194
  // Track which files to skip based on user choices
1790
2195
  const skipFiles = {
@@ -1792,27 +2197,9 @@ async function interactiveSetup() {
1792
2197
  claudeCommands: false
1793
2198
  };
1794
2199
 
1795
- // Ask about overwriting AGENTS.md if it exists
1796
- if (projectStatus.hasAgentsMd) {
1797
- const overwriteAgents = await askYesNo(question, 'Found existing AGENTS.md. Overwrite?', true);
1798
- if (!overwriteAgents) {
1799
- skipFiles.agentsMd = true;
1800
- console.log(' Keeping existing AGENTS.md');
1801
- } else {
1802
- console.log(' Will overwrite AGENTS.md');
1803
- }
1804
- }
1805
-
1806
- // Ask about overwriting .claude/commands/ if it exists
1807
- if (projectStatus.hasClaudeCommands) {
1808
- const overwriteCommands = await askYesNo(question, 'Found existing .claude/commands/. Overwrite?', true);
1809
- if (!overwriteCommands) {
1810
- skipFiles.claudeCommands = true;
1811
- console.log(' Keeping existing .claude/commands/');
1812
- } else {
1813
- console.log(' Will overwrite .claude/commands/');
1814
- }
1815
- }
2200
+ // Ask about overwriting existing files
2201
+ await promptForFileOverwrite(question, 'agentsMd', projectStatus.hasAgentsMd, skipFiles);
2202
+ await promptForFileOverwrite(question, 'claudeCommands', projectStatus.hasClaudeCommands, skipFiles);
1816
2203
 
1817
2204
  if (projectStatus.type !== 'fresh') {
1818
2205
  console.log('');
@@ -1821,95 +2208,14 @@ async function interactiveSetup() {
1821
2208
  // =============================================
1822
2209
  // STEP 1: Agent Selection
1823
2210
  // =============================================
1824
- console.log('STEP 1: Select AI Coding Agents');
1825
- console.log('================================');
1826
- console.log('');
1827
- console.log('Which AI coding agents do you use?');
1828
- console.log('(Enter numbers separated by spaces, or "all")');
1829
- console.log('');
1830
-
1831
2211
  const agentKeys = Object.keys(AGENTS);
1832
- agentKeys.forEach((key, index) => {
1833
- const agent = AGENTS[key];
1834
- console.log(` ${(index + 1).toString().padStart(2)}) ${agent.name.padEnd(20)} - ${agent.description}`);
1835
- });
1836
- console.log('');
1837
- console.log(' all) Install for all agents');
1838
- console.log('');
1839
-
1840
- let selectedAgents = [];
1841
-
1842
- // Loop until valid input is provided
1843
- while (selectedAgents.length === 0) {
1844
- const answer = await question('Your selection: ');
1845
-
1846
- // Handle empty input - reprompt
1847
- if (!answer || !answer.trim()) {
1848
- console.log(' Please enter at least one agent number or "all".');
1849
- continue;
1850
- }
1851
-
1852
- if (answer.toLowerCase() === 'all') {
1853
- selectedAgents = agentKeys;
1854
- } else {
1855
- const nums = answer.split(/[\s,]+/).map(n => parseInt(n.trim())).filter(n => !isNaN(n));
1856
-
1857
- // Validate numbers are in range
1858
- const validNums = nums.filter(n => n >= 1 && n <= agentKeys.length);
1859
- const invalidNums = nums.filter(n => n < 1 || n > agentKeys.length);
1860
-
1861
- if (invalidNums.length > 0) {
1862
- console.log(` ⚠ Invalid numbers ignored: ${invalidNums.join(', ')} (valid: 1-${agentKeys.length})`);
1863
- }
1864
-
1865
- // Deduplicate selected agents using Set
1866
- selectedAgents = [...new Set(validNums.map(n => agentKeys[n - 1]))].filter(Boolean);
1867
- }
1868
-
1869
- if (selectedAgents.length === 0) {
1870
- console.log(' No valid agents selected. Please try again.');
1871
- }
1872
- }
2212
+ const selectedAgents = await promptForAgentSelection(question, agentKeys);
1873
2213
 
1874
2214
  console.log('');
1875
2215
  console.log('Installing Forge workflow...');
1876
2216
 
1877
- // Copy AGENTS.md unless skipped
1878
- if (skipFiles.agentsMd) {
1879
- console.log(' Skipped: AGENTS.md (keeping existing)');
1880
- } else {
1881
- const agentsSrc = path.join(packageDir, 'AGENTS.md');
1882
- const agentsDest = path.join(projectRoot, 'AGENTS.md');
1883
-
1884
- // Try smart merge if file exists
1885
- if (fs.existsSync(agentsDest)) {
1886
- const existingContent = fs.readFileSync(agentsDest, 'utf8');
1887
- const newContent = fs.readFileSync(agentsSrc, 'utf8');
1888
- const merged = smartMergeAgentsMd(existingContent, newContent);
1889
-
1890
- if (merged) {
1891
- fs.writeFileSync(agentsDest, merged, 'utf8');
1892
- console.log(' Updated: AGENTS.md (preserved USER sections)');
1893
- } else {
1894
- // No markers, do normal copy (user already approved overwrite)
1895
- if (copyFile(agentsSrc, 'AGENTS.md')) {
1896
- console.log(' Updated: AGENTS.md (universal standard)');
1897
- }
1898
- }
1899
- } else {
1900
- // New file
1901
- if (copyFile(agentsSrc, 'AGENTS.md')) {
1902
- console.log(' Created: AGENTS.md (universal standard)');
1903
-
1904
- // Detect project type and update AGENTS.md
1905
- const detection = detectProjectType();
1906
- if (detection.hasPackageJson) {
1907
- updateAgentsMdWithProjectType(detection);
1908
- displayProjectType(detection);
1909
- }
1910
- }
1911
- }
1912
- }
2217
+ // Install AGENTS.md
2218
+ await installAgentsMd(skipFiles);
1913
2219
  console.log('');
1914
2220
 
1915
2221
  // Setup core documentation
@@ -1917,47 +2223,29 @@ async function interactiveSetup() {
1917
2223
  console.log('');
1918
2224
 
1919
2225
  // Load Claude commands if needed
1920
- let claudeCommands = {};
1921
- if (selectedAgents.includes('claude') || selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands)) {
1922
- // First ensure Claude is set up
1923
- if (selectedAgents.includes('claude')) {
1924
- setupAgent('claude', null, skipFiles);
1925
- }
1926
- // Then load the commands (from existing or newly created)
1927
- COMMANDS.forEach(cmd => {
1928
- const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
1929
- const content = readFile(cmdPath);
1930
- if (content) {
1931
- claudeCommands[`${cmd}.md`] = content;
1932
- }
1933
- });
2226
+ let claudeCommands = {};
2227
+ if (selectedAgents.includes('claude') || selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands)) {
2228
+ // First ensure Claude is set up
2229
+ if (selectedAgents.includes('claude')) {
2230
+ setupAgent('claude', null, skipFiles);
2231
+ }
2232
+ // Then load the commands
2233
+ claudeCommands = loadClaudeCommands(selectedAgents);
1934
2234
  }
1935
2235
 
1936
2236
  // Setup each selected agent with progress indication
1937
- const totalAgents = selectedAgents.length;
1938
- selectedAgents.forEach((agentKey, index) => {
1939
- const agent = AGENTS[agentKey];
1940
- console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
1941
- if (agentKey !== 'claude') { // Claude already done above
1942
- setupAgent(agentKey, claudeCommands, skipFiles);
1943
- }
1944
- });
2237
+ setupAgentsWithProgress(selectedAgents, claudeCommands, skipFiles);
1945
2238
 
1946
- // Agent installation success
1947
- console.log('');
1948
- console.log('Agent configuration complete!');
1949
- console.log('');
1950
- console.log('Installed for:');
1951
- selectedAgents.forEach(key => {
1952
- const agent = AGENTS[key];
1953
- console.log(` * ${agent.name}`);
1954
- });
2239
+ // =============================================
2240
+ // STEP 2: Project Tools Setup
2241
+ // =============================================
2242
+ await setupProjectTools(rl, question);
1955
2243
 
1956
2244
  // =============================================
1957
- // STEP 2: External Services Configuration
2245
+ // STEP 3: External Services Configuration
1958
2246
  // =============================================
1959
2247
  console.log('');
1960
- console.log('STEP 2: External Services (Optional)');
2248
+ console.log('STEP 3: External Services (Optional)');
1961
2249
  console.log('=====================================');
1962
2250
 
1963
2251
  await configureExternalServices(rl, question, selectedAgents, projectStatus);
@@ -1968,60 +2256,7 @@ async function interactiveSetup() {
1968
2256
  // =============================================
1969
2257
  // Final Summary
1970
2258
  // =============================================
1971
- console.log('');
1972
- console.log('==============================================');
1973
- console.log(` Forge v${VERSION} Setup Complete!`);
1974
- console.log('==============================================');
1975
- console.log('');
1976
- console.log('What\'s installed:');
1977
- console.log(' - AGENTS.md (universal instructions)');
1978
- console.log(' - docs/WORKFLOW.md (full workflow guide)');
1979
- console.log(' - docs/research/TEMPLATE.md (research template)');
1980
- console.log(' - docs/planning/PROGRESS.md (progress tracking)');
1981
- selectedAgents.forEach(key => {
1982
- const agent = AGENTS[key];
1983
- if (agent.linkFile) {
1984
- console.log(` - ${agent.linkFile} (${agent.name})`);
1985
- }
1986
- if (agent.hasCommands) {
1987
- console.log(` - .claude/commands/ (9 workflow commands)`);
1988
- }
1989
- if (agent.hasSkill) {
1990
- const skillDir = agent.dirs.find(d => d.includes('/skills/'));
1991
- if (skillDir) {
1992
- console.log(` - ${skillDir}/SKILL.md`);
1993
- }
1994
- }
1995
- });
1996
- console.log('');
1997
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1998
- console.log('📋 NEXT STEP - Complete AGENTS.md');
1999
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2000
- console.log('');
2001
- console.log('Ask your AI agent:');
2002
- console.log(' "Fill in the project description in AGENTS.md"');
2003
- console.log('');
2004
- console.log('The agent will:');
2005
- console.log(' ✓ Add one-sentence project description');
2006
- console.log(' ✓ Confirm package manager');
2007
- console.log(' ✓ Verify build commands');
2008
- console.log('');
2009
- console.log('Takes ~30 seconds. Done!');
2010
- console.log('');
2011
- console.log('💡 As you work: Add project patterns to AGENTS.md');
2012
- console.log(' USER:START section. Keep it minimal - budget is');
2013
- console.log(' ~150-200 instructions max.');
2014
- console.log('');
2015
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2016
- console.log('');
2017
- console.log('Optional tools:');
2018
- console.log(` ${PKG_MANAGER} install -g @beads/bd && bd init`);
2019
- console.log(` ${PKG_MANAGER} install -g @fission-ai/openspec`);
2020
- console.log('');
2021
- console.log('Start with: /status');
2022
- console.log('');
2023
- console.log(`Package manager: ${PKG_MANAGER}`);
2024
- console.log('');
2259
+ displaySetupSummary(selectedAgents);
2025
2260
  }
2026
2261
 
2027
2262
  // Parse CLI flags
@@ -2035,39 +2270,51 @@ function parseFlags() {
2035
2270
  path: null
2036
2271
  };
2037
2272
 
2038
- for (let i = 0; i < args.length; i++) {
2273
+ for (let i = 0; i < args.length; ) {
2039
2274
  const arg = args[i];
2040
2275
 
2041
2276
  if (arg === '--quick' || arg === '-q') {
2042
2277
  flags.quick = true;
2278
+ i++;
2043
2279
  } else if (arg === '--skip-external' || arg === '--skip-services') {
2044
2280
  flags.skipExternal = true;
2281
+ i++;
2045
2282
  } else if (arg === '--all') {
2046
2283
  flags.all = true;
2284
+ i++;
2047
2285
  } else if (arg === '--help' || arg === '-h') {
2048
2286
  flags.help = true;
2287
+ i++;
2049
2288
  } else if (arg === '--path' || arg === '-p') {
2050
2289
  // --path <directory> or -p <directory>
2051
2290
  if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
2052
2291
  flags.path = args[i + 1];
2053
- i++; // Skip next arg
2292
+ i += 2; // Skip current and next arg
2293
+ } else {
2294
+ i++;
2054
2295
  }
2055
2296
  } else if (arg.startsWith('--path=')) {
2056
2297
  // --path=/some/dir format
2057
2298
  flags.path = arg.replace('--path=', '');
2299
+ i++;
2058
2300
  } else if (arg === '--agents') {
2059
2301
  // --agents claude cursor format
2060
2302
  const agentList = [];
2061
- for (let j = i + 1; j < args.length; j++) {
2062
- if (args[j].startsWith('-')) break;
2303
+ let j = i + 1;
2304
+ while (j < args.length && !args[j].startsWith('-')) {
2063
2305
  agentList.push(args[j]);
2306
+ j++;
2064
2307
  }
2065
2308
  if (agentList.length > 0) {
2066
2309
  flags.agents = agentList.join(',');
2067
2310
  }
2311
+ i = j; // Skip all consumed arguments
2068
2312
  } else if (arg.startsWith('--agents=')) {
2069
2313
  // --agents=claude,cursor format
2070
2314
  flags.agents = arg.replace('--agents=', '');
2315
+ i++;
2316
+ } else {
2317
+ i++;
2071
2318
  }
2072
2319
  }
2073
2320
 
@@ -2130,6 +2377,374 @@ function showHelp() {
2130
2377
  console.log('');
2131
2378
  }
2132
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
+
2133
2748
  // Quick setup with defaults
2134
2749
  async function quickSetup(selectedAgents, skipExternal) {
2135
2750
  showBanner('Quick Setup');
@@ -2152,6 +2767,55 @@ async function quickSetup(selectedAgents, skipExternal) {
2152
2767
  setupCoreDocs();
2153
2768
  console.log('');
2154
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
+
2155
2819
  // Load Claude commands if needed
2156
2820
  let claudeCommands = {};
2157
2821
  if (selectedAgents.includes('claude')) {
@@ -2187,8 +2851,15 @@ async function quickSetup(selectedAgents, skipExternal) {
2187
2851
  console.log(` * ${agent.name}`);
2188
2852
  });
2189
2853
 
2854
+ // Install git hooks for TDD enforcement
2855
+ console.log('');
2856
+ installGitHooks();
2857
+
2190
2858
  // Configure external services with defaults (unless skipped)
2191
- if (!skipExternal) {
2859
+ if (skipExternal) {
2860
+ console.log('');
2861
+ console.log('Skipping external services configuration...');
2862
+ } else {
2192
2863
  console.log('');
2193
2864
  console.log('Configuring default services...');
2194
2865
  console.log('');
@@ -2205,9 +2876,6 @@ async function quickSetup(selectedAgents, skipExternal) {
2205
2876
  console.log(' * Code Quality: ESLint (built-in)');
2206
2877
  console.log('');
2207
2878
  console.log('Configuration saved to .env.local');
2208
- } else {
2209
- console.log('');
2210
- console.log('Skipping external services configuration...');
2211
2879
  }
2212
2880
 
2213
2881
  // Final summary
@@ -2293,22 +2961,22 @@ async function interactiveSetupWithFlags(flags) {
2293
2961
  // Ask about overwriting AGENTS.md if it exists
2294
2962
  if (projectStatus.hasAgentsMd) {
2295
2963
  const overwriteAgents = await askYesNo(question, 'Found existing AGENTS.md. Overwrite?', true);
2296
- if (!overwriteAgents) {
2964
+ if (overwriteAgents) {
2965
+ console.log(' Will overwrite AGENTS.md');
2966
+ } else {
2297
2967
  skipFiles.agentsMd = true;
2298
2968
  console.log(' Keeping existing AGENTS.md');
2299
- } else {
2300
- console.log(' Will overwrite AGENTS.md');
2301
2969
  }
2302
2970
  }
2303
2971
 
2304
2972
  // Ask about overwriting .claude/commands/ if it exists
2305
2973
  if (projectStatus.hasClaudeCommands) {
2306
2974
  const overwriteCommands = await askYesNo(question, 'Found existing .claude/commands/. Overwrite?', true);
2307
- if (!overwriteCommands) {
2975
+ if (overwriteCommands) {
2976
+ console.log(' Will overwrite .claude/commands/');
2977
+ } else {
2308
2978
  skipFiles.claudeCommands = true;
2309
2979
  console.log(' Keeping existing .claude/commands/');
2310
- } else {
2311
- console.log(' Will overwrite .claude/commands/');
2312
2980
  }
2313
2981
  }
2314
2982
 
@@ -2350,7 +3018,7 @@ async function interactiveSetupWithFlags(flags) {
2350
3018
  if (answer.toLowerCase() === 'all') {
2351
3019
  selectedAgents = agentKeys;
2352
3020
  } else {
2353
- const nums = answer.split(/[\s,]+/).map(n => parseInt(n.trim())).filter(n => !isNaN(n));
3021
+ const nums = answer.split(/[\s,]+/).map(n => Number.parseInt(n.trim())).filter(n => !Number.isNaN(n));
2354
3022
 
2355
3023
  // Validate numbers are in range
2356
3024
  const validNums = nums.filter(n => n >= 1 && n <= agentKeys.length);
@@ -2388,23 +3056,19 @@ async function interactiveSetupWithFlags(flags) {
2388
3056
  if (merged) {
2389
3057
  fs.writeFileSync(agentsDest, merged, 'utf8');
2390
3058
  console.log(' Updated: AGENTS.md (preserved USER sections)');
2391
- } else {
3059
+ } else if (copyFile(agentsSrc, 'AGENTS.md')) {
2392
3060
  // No markers, do normal copy (user already approved overwrite)
2393
- if (copyFile(agentsSrc, 'AGENTS.md')) {
2394
- console.log(' Updated: AGENTS.md (universal standard)');
2395
- }
3061
+ console.log(' Updated: AGENTS.md (universal standard)');
2396
3062
  }
2397
- } else {
3063
+ } else if (copyFile(agentsSrc, 'AGENTS.md')) {
2398
3064
  // New file
2399
- if (copyFile(agentsSrc, 'AGENTS.md')) {
2400
- console.log(' Created: AGENTS.md (universal standard)');
2401
-
2402
- // Detect project type and update AGENTS.md
2403
- const detection = detectProjectType();
2404
- if (detection.hasPackageJson) {
2405
- updateAgentsMdWithProjectType(detection);
2406
- displayProjectType(detection);
2407
- }
3065
+ console.log(' Created: AGENTS.md (universal standard)');
3066
+
3067
+ // Detect project type and update AGENTS.md
3068
+ const detection = detectProjectType();
3069
+ if (detection.hasPackageJson) {
3070
+ updateAgentsMdWithProjectType(detection);
3071
+ displayProjectType(detection);
2408
3072
  }
2409
3073
  }
2410
3074
  }
@@ -2454,15 +3118,15 @@ async function interactiveSetupWithFlags(flags) {
2454
3118
  // =============================================
2455
3119
  // STEP 2: External Services Configuration
2456
3120
  // =============================================
2457
- if (!flags.skipExternal) {
3121
+ if (flags.skipExternal) {
3122
+ console.log('');
3123
+ console.log('Skipping external services configuration...');
3124
+ } else {
2458
3125
  console.log('');
2459
3126
  console.log('STEP 2: External Services (Optional)');
2460
3127
  console.log('=====================================');
2461
3128
 
2462
3129
  await configureExternalServices(rl, question, selectedAgents, projectStatus);
2463
- } else {
2464
- console.log('');
2465
- console.log('Skipping external services configuration...');
2466
3130
  }
2467
3131
 
2468
3132
  setupCompleted = true;
@@ -2517,9 +3181,27 @@ async function interactiveSetupWithFlags(flags) {
2517
3181
  console.log('');
2518
3182
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2519
3183
  console.log('');
2520
- console.log('Optional tools:');
2521
- console.log(` ${PKG_MANAGER} install -g @beads/bd && bd init`);
2522
- 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
+
2523
3205
  console.log('');
2524
3206
  console.log('Start with: /status');
2525
3207
  console.log('');
@@ -2528,6 +3210,128 @@ async function interactiveSetupWithFlags(flags) {
2528
3210
  }
2529
3211
 
2530
3212
  // Main
3213
+ // Helper: Handle --path setup
3214
+ function handlePathSetup(targetPath) {
3215
+ const resolvedPath = path.resolve(targetPath);
3216
+
3217
+ // Create directory if it doesn't exist
3218
+ if (!fs.existsSync(resolvedPath)) {
3219
+ try {
3220
+ fs.mkdirSync(resolvedPath, { recursive: true });
3221
+ console.log(`Created directory: ${resolvedPath}`);
3222
+ } catch (err) {
3223
+ console.error(`Error creating directory: ${err.message}`);
3224
+ process.exit(1);
3225
+ }
3226
+ }
3227
+
3228
+ // Verify it's a directory
3229
+ if (!fs.statSync(resolvedPath).isDirectory()) {
3230
+ console.error(`Error: ${resolvedPath} is not a directory`);
3231
+ process.exit(1);
3232
+ }
3233
+
3234
+ // Change to target directory
3235
+ try {
3236
+ process.chdir(resolvedPath);
3237
+ console.log(`Working directory: ${resolvedPath}`);
3238
+ console.log('');
3239
+ } catch (err) {
3240
+ console.error(`Error changing to directory: ${err.message}`);
3241
+ process.exit(1);
3242
+ }
3243
+ }
3244
+
3245
+ // Helper: Determine selected agents from flags
3246
+ function determineSelectedAgents(flags) {
3247
+ if (flags.all) {
3248
+ return Object.keys(AGENTS);
3249
+ }
3250
+
3251
+ if (flags.agents) {
3252
+ const selectedAgents = validateAgents(flags.agents);
3253
+ if (selectedAgents.length === 0) {
3254
+ console.log('No valid agents specified.');
3255
+ console.log('Available agents:', Object.keys(AGENTS).join(', '));
3256
+ process.exit(1);
3257
+ }
3258
+ return selectedAgents;
3259
+ }
3260
+
3261
+ return [];
3262
+ }
3263
+
3264
+ // Helper: Handle setup command in non-quick mode
3265
+ async function handleSetupCommand(selectedAgents, flags) {
3266
+ showBanner('Installing for specified agents...');
3267
+ console.log('');
3268
+
3269
+ // Check prerequisites
3270
+ checkPrerequisites();
3271
+ console.log('');
3272
+
3273
+ // Copy AGENTS.md
3274
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
3275
+ if (copyFile(agentsSrc, 'AGENTS.md')) {
3276
+ console.log(' Created: AGENTS.md (universal standard)');
3277
+ }
3278
+ console.log('');
3279
+
3280
+ // Setup core documentation
3281
+ setupCoreDocs();
3282
+ console.log('');
3283
+
3284
+ // Load Claude commands if needed
3285
+ const claudeCommands = loadClaudeCommands(selectedAgents);
3286
+
3287
+ // Setup agents
3288
+ selectedAgents.forEach(agentKey => {
3289
+ if (agentKey !== 'claude') {
3290
+ setupAgent(agentKey, claudeCommands);
3291
+ }
3292
+ });
3293
+
3294
+ console.log('');
3295
+ console.log('Agent configuration complete!');
3296
+
3297
+ // Install git hooks for TDD enforcement
3298
+ console.log('');
3299
+ installGitHooks();
3300
+
3301
+ // External services (unless skipped)
3302
+ await handleExternalServices(flags.skipExternal, selectedAgents);
3303
+
3304
+ console.log('');
3305
+ console.log('Done! Get started with: /status');
3306
+ }
3307
+
3308
+ // Helper: Handle external services configuration
3309
+ async function handleExternalServices(skipExternal, selectedAgents) {
3310
+ if (skipExternal) {
3311
+ console.log('');
3312
+ console.log('Skipping external services configuration...');
3313
+ return;
3314
+ }
3315
+
3316
+ const rl = readline.createInterface({
3317
+ input: process.stdin,
3318
+ output: process.stdout
3319
+ });
3320
+
3321
+ let setupCompleted = false;
3322
+ rl.on('close', () => {
3323
+ if (!setupCompleted) {
3324
+ console.log('\n\nSetup cancelled.');
3325
+ process.exit(0);
3326
+ }
3327
+ });
3328
+
3329
+ const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
3330
+ await configureExternalServices(rl, question, selectedAgents);
3331
+ setupCompleted = true;
3332
+ rl.close();
3333
+ }
3334
+
2531
3335
  async function main() {
2532
3336
  const command = args[0];
2533
3337
  const flags = parseFlags();
@@ -2540,50 +3344,12 @@ async function main() {
2540
3344
 
2541
3345
  // Handle --path option: change to target directory
2542
3346
  if (flags.path) {
2543
- const targetPath = path.resolve(flags.path);
2544
-
2545
- // Create directory if it doesn't exist
2546
- if (!fs.existsSync(targetPath)) {
2547
- try {
2548
- fs.mkdirSync(targetPath, { recursive: true });
2549
- console.log(`Created directory: ${targetPath}`);
2550
- } catch (err) {
2551
- console.error(`Error creating directory: ${err.message}`);
2552
- process.exit(1);
2553
- }
2554
- }
2555
-
2556
- // Verify it's a directory
2557
- if (!fs.statSync(targetPath).isDirectory()) {
2558
- console.error(`Error: ${targetPath} is not a directory`);
2559
- process.exit(1);
2560
- }
2561
-
2562
- // Change to target directory
2563
- try {
2564
- process.chdir(targetPath);
2565
- console.log(`Working directory: ${targetPath}`);
2566
- console.log('');
2567
- } catch (err) {
2568
- console.error(`Error changing to directory: ${err.message}`);
2569
- process.exit(1);
2570
- }
3347
+ handlePathSetup(flags.path);
2571
3348
  }
2572
3349
 
2573
3350
  if (command === 'setup') {
2574
3351
  // Determine agents to install
2575
- let selectedAgents = [];
2576
-
2577
- if (flags.all) {
2578
- selectedAgents = Object.keys(AGENTS);
2579
- } else if (flags.agents) {
2580
- selectedAgents = validateAgents(flags.agents);
2581
- if (selectedAgents.length === 0) {
2582
- console.log('No valid agents specified.');
2583
- console.log('Available agents:', Object.keys(AGENTS).join(', '));
2584
- process.exit(1);
2585
- }
2586
- }
3352
+ let selectedAgents = determineSelectedAgents(flags);
2587
3353
 
2588
3354
  // Quick mode
2589
3355
  if (flags.quick) {
@@ -2597,74 +3363,7 @@ async function main() {
2597
3363
 
2598
3364
  // Agents specified via flag (non-quick mode)
2599
3365
  if (selectedAgents.length > 0) {
2600
- showBanner('Installing for specified agents...');
2601
- console.log('');
2602
-
2603
- // Check prerequisites
2604
- checkPrerequisites();
2605
- console.log('');
2606
-
2607
- // Copy AGENTS.md
2608
- const agentsSrc = path.join(packageDir, 'AGENTS.md');
2609
- if (copyFile(agentsSrc, 'AGENTS.md')) {
2610
- console.log(' Created: AGENTS.md (universal standard)');
2611
- }
2612
- console.log('');
2613
-
2614
- // Setup core documentation
2615
- setupCoreDocs();
2616
- console.log('');
2617
-
2618
- // Load Claude commands if needed
2619
- let claudeCommands = {};
2620
- if (selectedAgents.includes('claude')) {
2621
- setupAgent('claude', null);
2622
- }
2623
-
2624
- if (selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands)) {
2625
- COMMANDS.forEach(cmd => {
2626
- const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
2627
- const content = readFile(cmdPath);
2628
- if (content) {
2629
- claudeCommands[`${cmd}.md`] = content;
2630
- }
2631
- });
2632
- }
2633
-
2634
- // Setup agents
2635
- selectedAgents.forEach(agentKey => {
2636
- if (agentKey !== 'claude') {
2637
- setupAgent(agentKey, claudeCommands);
2638
- }
2639
- });
2640
-
2641
- console.log('');
2642
- console.log('Agent configuration complete!');
2643
-
2644
- // External services (unless skipped)
2645
- if (!flags.skipExternal) {
2646
- const rl = readline.createInterface({
2647
- input: process.stdin,
2648
- output: process.stdout
2649
- });
2650
- let setupCompleted = false;
2651
- rl.on('close', () => {
2652
- if (!setupCompleted) {
2653
- console.log('\n\nSetup cancelled.');
2654
- process.exit(0);
2655
- }
2656
- });
2657
- const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
2658
- await configureExternalServices(rl, question, selectedAgents);
2659
- setupCompleted = true;
2660
- rl.close();
2661
- } else {
2662
- console.log('');
2663
- console.log('Skipping external services configuration...');
2664
- }
2665
-
2666
- console.log('');
2667
- console.log('Done! Get started with: /status');
3366
+ await handleSetupCommand(selectedAgents, flags);
2668
3367
  return;
2669
3368
  }
2670
3369
 
@@ -2738,13 +3437,9 @@ function validateRollbackInput(method, target) {
2738
3437
  }
2739
3438
 
2740
3439
  // Extract USER sections before rollback
2741
- function extractUserSections(filePath) {
2742
- if (!fs.existsSync(filePath)) return {};
2743
-
2744
- const content = fs.readFileSync(filePath, 'utf-8');
3440
+ // Helper: Extract USER:START/END marker sections from content
3441
+ function extractUserMarkerSections(content) {
2745
3442
  const sections = {};
2746
-
2747
- // Extract USER sections
2748
3443
  const userRegex = /<!-- USER:START -->([\s\S]*?)<!-- USER:END -->/g;
2749
3444
  let match;
2750
3445
  let index = 0;
@@ -2754,15 +3449,35 @@ function extractUserSections(filePath) {
2754
3449
  index++;
2755
3450
  }
2756
3451
 
2757
- // Extract custom commands
3452
+ return sections;
3453
+ }
3454
+
3455
+ // Helper: Extract custom commands from directory
3456
+ function extractCustomCommands(filePath) {
2758
3457
  const customCommandsDir = path.join(path.dirname(filePath), '.claude', 'commands', 'custom');
2759
- if (fs.existsSync(customCommandsDir)) {
2760
- sections.customCommands = fs.readdirSync(customCommandsDir)
2761
- .filter(f => f.endsWith('.md'))
2762
- .map(f => ({
2763
- name: f,
2764
- content: fs.readFileSync(path.join(customCommandsDir, f), 'utf-8')
2765
- }));
3458
+
3459
+ if (!fs.existsSync(customCommandsDir)) {
3460
+ return null;
3461
+ }
3462
+
3463
+ return fs.readdirSync(customCommandsDir)
3464
+ .filter(f => f.endsWith('.md'))
3465
+ .map(f => ({
3466
+ name: f,
3467
+ content: fs.readFileSync(path.join(customCommandsDir, f), 'utf-8')
3468
+ }));
3469
+ }
3470
+
3471
+ function extractUserSections(filePath) {
3472
+ if (!fs.existsSync(filePath)) return {};
3473
+
3474
+ const content = fs.readFileSync(filePath, 'utf-8');
3475
+ const sections = extractUserMarkerSections(content);
3476
+
3477
+ // Extract custom commands
3478
+ const customCommands = extractCustomCommands(filePath);
3479
+ if (customCommands) {
3480
+ sections.customCommands = customCommands;
2766
3481
  }
2767
3482
 
2768
3483
  return sections;
@@ -2778,7 +3493,7 @@ function preserveUserSections(filePath, savedSections) {
2778
3493
 
2779
3494
  // Restore USER sections
2780
3495
  let index = 0;
2781
- content = content.replace(
3496
+ content = content.replaceAll(
2782
3497
  /<!-- USER:START -->[\s\S]*?<!-- USER:END -->/g,
2783
3498
  () => {
2784
3499
  const section = savedSections[`user_${index}`];
@@ -2807,6 +3522,110 @@ function preserveUserSections(filePath, savedSections) {
2807
3522
  }
2808
3523
 
2809
3524
  // Perform rollback operation
3525
+ // Helper: Check git working directory is clean
3526
+ function checkGitWorkingDirectory() {
3527
+ try {
3528
+ const { execSync } = require('node:child_process');
3529
+ const status = execSync('git status --porcelain', { encoding: 'utf-8' });
3530
+ if (status.trim() !== '') {
3531
+ console.log(chalk.red(' ❌ Working directory has uncommitted changes'));
3532
+ console.log(' Commit or stash changes before rollback');
3533
+ return false;
3534
+ }
3535
+ return true;
3536
+ } catch (err) {
3537
+ console.log(chalk.red(' ❌ Git error:'), err.message);
3538
+ return false;
3539
+ }
3540
+ }
3541
+
3542
+ // Helper: Update Beads issue after PR rollback
3543
+ function updateBeadsIssue(commitMessage) {
3544
+ const issueMatch = commitMessage.match(/#(\d+)/);
3545
+ if (!issueMatch) return;
3546
+
3547
+ try {
3548
+ const { execSync } = require('node:child_process');
3549
+ execSync(`bd update ${issueMatch[1]} --status reverted --comment "PR reverted"`, { stdio: 'inherit' });
3550
+ console.log(` Updated Beads issue #${issueMatch[1]} to 'reverted'`);
3551
+ } catch {
3552
+ // Beads not installed - silently continue
3553
+ }
3554
+ }
3555
+
3556
+ // Helper: Handle commit rollback
3557
+ function handleCommitRollback(target, dryRun, execSync) {
3558
+ if (dryRun) {
3559
+ console.log(` Would revert: ${target}`);
3560
+ const files = execSync(`git diff-tree --no-commit-id --name-only -r ${target}`, { encoding: 'utf-8' });
3561
+ console.log(' Affected files:');
3562
+ files.trim().split('\n').forEach(f => console.log(` - ${f}`));
3563
+ } else {
3564
+ execSync(`git revert --no-edit ${target}`, { stdio: 'inherit' });
3565
+ }
3566
+ }
3567
+
3568
+ // Helper: Handle PR rollback
3569
+ function handlePrRollback(target, dryRun, execSync) {
3570
+ if (dryRun) {
3571
+ console.log(` Would revert merge: ${target}`);
3572
+ const files = execSync(`git diff-tree --no-commit-id --name-only -r ${target}`, { encoding: 'utf-8' });
3573
+ console.log(' Affected files:');
3574
+ files.trim().split('\n').forEach(f => console.log(` - ${f}`));
3575
+ } else {
3576
+ execSync(`git revert -m 1 --no-edit ${target}`, { stdio: 'inherit' });
3577
+
3578
+ // Update Beads issue if linked
3579
+ const commitMsg = execSync(`git log -1 --format=%B ${target}`, { encoding: 'utf-8' });
3580
+ updateBeadsIssue(commitMsg);
3581
+ }
3582
+ }
3583
+
3584
+ // Helper: Handle partial file rollback
3585
+ function handlePartialRollback(target, dryRun, execSync) {
3586
+ const files = target.split(',').map(f => f.trim());
3587
+ if (dryRun) {
3588
+ console.log(' Would restore files:');
3589
+ files.forEach(f => console.log(` - ${f}`));
3590
+ } else {
3591
+ files.forEach(f => {
3592
+ execSync(`git checkout HEAD~1 -- "${f}"`, { stdio: 'inherit' });
3593
+ });
3594
+ execSync(`git commit -m "chore: rollback ${files.join(', ')}"`, { stdio: 'inherit' });
3595
+ }
3596
+ }
3597
+
3598
+ // Helper: Handle branch range rollback
3599
+ function handleBranchRollback(target, dryRun, execSync) {
3600
+ const [startCommit, endCommit] = target.split('..');
3601
+ if (dryRun) {
3602
+ console.log(` Would revert range: ${startCommit}..${endCommit}`);
3603
+ const commits = execSync(`git log --oneline ${startCommit}..${endCommit}`, { encoding: 'utf-8' });
3604
+ console.log(' Commits to revert:');
3605
+ commits.trim().split('\n').forEach(c => console.log(` ${c}`));
3606
+ } else {
3607
+ execSync(`git revert --no-edit ${startCommit}..${endCommit}`, { stdio: 'inherit' });
3608
+ }
3609
+ }
3610
+
3611
+ // Helper: Finalize rollback by restoring user sections
3612
+ function finalizeRollback(agentsPath, savedSections) {
3613
+ const { execSync } = require('node:child_process');
3614
+
3615
+ console.log(' 📦 Restoring user content...');
3616
+ preserveUserSections(agentsPath, savedSections);
3617
+
3618
+ // Amend commit to include restored USER sections
3619
+ if (fs.existsSync(agentsPath)) {
3620
+ execSync('git add AGENTS.md', { stdio: 'inherit' });
3621
+ execSync('git commit --amend --no-edit', { stdio: 'inherit' });
3622
+ }
3623
+
3624
+ console.log('');
3625
+ console.log(chalk.green(' ✅ Rollback complete'));
3626
+ console.log(' User content preserved');
3627
+ }
3628
+
2810
3629
  async function performRollback(method, target, dryRun = false) {
2811
3630
  console.log('');
2812
3631
  console.log(chalk.cyan(` 🔄 Rollback: ${method}`));
@@ -2824,16 +3643,7 @@ async function performRollback(method, target, dryRun = false) {
2824
3643
  }
2825
3644
 
2826
3645
  // Check for clean working directory
2827
- try {
2828
- const { execSync } = require('child_process');
2829
- const status = execSync('git status --porcelain', { encoding: 'utf-8' });
2830
- if (status.trim() !== '') {
2831
- console.log(chalk.red(' ❌ Working directory has uncommitted changes'));
2832
- console.log(' Commit or stash changes before rollback');
2833
- return false;
2834
- }
2835
- } catch (err) {
2836
- console.log(chalk.red(' ❌ Git error:'), err.message);
3646
+ if (!checkGitWorkingDirectory()) {
2837
3647
  return false;
2838
3648
  }
2839
3649
 
@@ -2846,74 +3656,20 @@ async function performRollback(method, target, dryRun = false) {
2846
3656
  }
2847
3657
 
2848
3658
  try {
2849
- const { execSync } = require('child_process');
3659
+ const { execSync } = require('node:child_process');
2850
3660
 
2851
3661
  if (method === 'commit') {
2852
- if (dryRun) {
2853
- console.log(` Would revert: ${target}`);
2854
- const files = execSync(`git diff-tree --no-commit-id --name-only -r ${target}`, { encoding: 'utf-8' });
2855
- console.log(' Affected files:');
2856
- files.trim().split('\n').forEach(f => console.log(` - ${f}`));
2857
- } else {
2858
- execSync(`git revert --no-edit ${target}`, { stdio: 'inherit' });
2859
- }
3662
+ handleCommitRollback(target, dryRun, execSync);
2860
3663
  } else if (method === 'pr') {
2861
- if (dryRun) {
2862
- console.log(` Would revert merge: ${target}`);
2863
- const files = execSync(`git diff-tree --no-commit-id --name-only -r ${target}`, { encoding: 'utf-8' });
2864
- console.log(' Affected files:');
2865
- files.trim().split('\n').forEach(f => console.log(` - ${f}`));
2866
- } else {
2867
- execSync(`git revert -m 1 --no-edit ${target}`, { stdio: 'inherit' });
2868
-
2869
- // Update Beads issue if linked
2870
- const commitMsg = execSync(`git log -1 --format=%B ${target}`, { encoding: 'utf-8' });
2871
- const issueMatch = commitMsg.match(/#(\d+)/);
2872
- if (issueMatch) {
2873
- try {
2874
- execSync(`bd update ${issueMatch[1]} --status reverted --comment "PR reverted"`, { stdio: 'inherit' });
2875
- console.log(` Updated Beads issue #${issueMatch[1]} to 'reverted'`);
2876
- } catch {
2877
- // Beads not installed - silently continue
2878
- }
2879
- }
2880
- }
3664
+ handlePrRollback(target, dryRun, execSync);
2881
3665
  } else if (method === 'partial') {
2882
- const files = target.split(',').map(f => f.trim());
2883
- if (dryRun) {
2884
- console.log(' Would restore files:');
2885
- files.forEach(f => console.log(` - ${f}`));
2886
- } else {
2887
- files.forEach(f => {
2888
- execSync(`git checkout HEAD~1 -- "${f}"`, { stdio: 'inherit' });
2889
- });
2890
- execSync(`git commit -m "chore: rollback ${files.join(', ')}"`, { stdio: 'inherit' });
2891
- }
3666
+ handlePartialRollback(target, dryRun, execSync);
2892
3667
  } else if (method === 'branch') {
2893
- const [startCommit, endCommit] = target.split('..');
2894
- if (dryRun) {
2895
- console.log(` Would revert range: ${startCommit}..${endCommit}`);
2896
- const commits = execSync(`git log --oneline ${startCommit}..${endCommit}`, { encoding: 'utf-8' });
2897
- console.log(' Commits to revert:');
2898
- commits.trim().split('\n').forEach(c => console.log(` ${c}`));
2899
- } else {
2900
- execSync(`git revert --no-edit ${startCommit}..${endCommit}`, { stdio: 'inherit' });
2901
- }
3668
+ handleBranchRollback(target, dryRun, execSync);
2902
3669
  }
2903
3670
 
2904
3671
  if (!dryRun) {
2905
- console.log(' 📦 Restoring user content...');
2906
- preserveUserSections(agentsPath, savedSections);
2907
-
2908
- // Amend commit to include restored USER sections
2909
- if (fs.existsSync(agentsPath)) {
2910
- execSync('git add AGENTS.md', { stdio: 'inherit' });
2911
- execSync('git commit --amend --no-edit', { stdio: 'inherit' });
2912
- }
2913
-
2914
- console.log('');
2915
- console.log(chalk.green(' ✅ Rollback complete'));
2916
- console.log(' User content preserved');
3672
+ finalizeRollback(agentsPath, savedSections);
2917
3673
  }
2918
3674
 
2919
3675
  return true;
@@ -2952,33 +3708,33 @@ async function showRollbackMenu() {
2952
3708
  let method, target, dryRun = false;
2953
3709
 
2954
3710
  switch (choice.trim()) {
2955
- case '1':
3711
+ case '1': {
2956
3712
  method = 'commit';
2957
3713
  target = 'HEAD';
2958
3714
  break;
2959
-
2960
- case '2':
3715
+ }
3716
+ case '2': {
2961
3717
  target = await new Promise(resolve => {
2962
3718
  rl.question(' Enter commit hash: ', resolve);
2963
3719
  });
2964
3720
  method = 'commit';
2965
3721
  break;
2966
-
2967
- case '3':
3722
+ }
3723
+ case '3': {
2968
3724
  target = await new Promise(resolve => {
2969
3725
  rl.question(' Enter merge commit hash: ', resolve);
2970
3726
  });
2971
3727
  method = 'pr';
2972
3728
  break;
2973
-
2974
- case '4':
3729
+ }
3730
+ case '4': {
2975
3731
  target = await new Promise(resolve => {
2976
3732
  rl.question(' Enter file paths (comma-separated): ', resolve);
2977
3733
  });
2978
3734
  method = 'partial';
2979
3735
  break;
2980
-
2981
- case '5':
3736
+ }
3737
+ case '5': {
2982
3738
  const start = await new Promise(resolve => {
2983
3739
  rl.question(' Enter start commit: ', resolve);
2984
3740
  });
@@ -2988,8 +3744,8 @@ async function showRollbackMenu() {
2988
3744
  target = `${start.trim()}..${end.trim()}`;
2989
3745
  method = 'branch';
2990
3746
  break;
2991
-
2992
- case '6':
3747
+ }
3748
+ case '6': {
2993
3749
  dryRun = true;
2994
3750
  const dryMethod = await new Promise(resolve => {
2995
3751
  rl.question(' Preview method (commit/pr/partial/branch): ', resolve);
@@ -2999,11 +3755,12 @@ async function showRollbackMenu() {
2999
3755
  rl.question(' Enter target (commit/files/range): ', resolve);
3000
3756
  });
3001
3757
  break;
3002
-
3003
- default:
3758
+ }
3759
+ default: {
3004
3760
  console.log(chalk.red(' Invalid choice'));
3005
3761
  rl.close();
3006
3762
  return;
3763
+ }
3007
3764
  }
3008
3765
 
3009
3766
  rl.close();