forge-workflow 1.4.7 → 1.4.9

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,10 +32,10 @@
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 } = require('node:child_process');
39
39
 
40
40
  // Get version from package.json (single source of truth)
41
41
  const packageDir = path.dirname(__dirname);
@@ -188,6 +188,8 @@ function safeExec(cmd) {
188
188
  try {
189
189
  return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
190
190
  } catch (e) {
191
+ // Command execution failure is expected when tool is not installed or fails
192
+ // Returning null allows caller to handle missing tools gracefully
191
193
  return null;
192
194
  }
193
195
  }
@@ -212,7 +214,7 @@ function checkPrerequisites() {
212
214
  // Check GitHub CLI
213
215
  const ghVersion = safeExec('gh --version');
214
216
  if (ghVersion) {
215
- console.log(` ✓ ${ghVersion.split('\\n')[0]}`);
217
+ console.log(` ✓ ${ghVersion.split(String.raw`\n`)[0]}`);
216
218
  // Check if authenticated
217
219
  const authStatus = safeExec('gh auth status');
218
220
  if (!authStatus) {
@@ -223,7 +225,7 @@ function checkPrerequisites() {
223
225
  }
224
226
 
225
227
  // Check Node.js version
226
- const nodeVersion = parseInt(process.version.slice(1).split('.')[0]);
228
+ const nodeVersion = Number.parseInt(process.version.slice(1).split('.')[0]);
227
229
  if (nodeVersion >= 20) {
228
230
  console.log(` ✓ node ${process.version}`);
229
231
  } else {
@@ -434,10 +436,8 @@ function copyFile(src, dest) {
434
436
  }
435
437
  fs.copyFileSync(src, destPath);
436
438
  return true;
437
- } else {
438
- if (process.env.DEBUG) {
439
- console.warn(` ⚠ Source file not found: ${src}`);
440
- }
439
+ } else if (process.env.DEBUG) {
440
+ console.warn(` ⚠ Source file not found: ${src}`);
441
441
  }
442
442
  } catch (err) {
443
443
  console.error(` ✗ Failed to copy ${src} -> ${dest}: ${err.message}`);
@@ -472,7 +472,9 @@ function createSymlinkOrCopy(source, target) {
472
472
  const relPath = path.relative(targetDir, fullSource);
473
473
  fs.symlinkSync(relPath, fullTarget);
474
474
  return 'linked';
475
- } catch (symlinkErr) {
475
+ } catch (error_) {
476
+ // Symlink creation may fail due to permissions or OS limitations (e.g., Windows without admin)
477
+ // Fall back to copying the file instead to ensure operation succeeds
476
478
  fs.copyFileSync(fullSource, fullTarget);
477
479
  return 'copied';
478
480
  }
@@ -494,7 +496,10 @@ function readEnvFile() {
494
496
  if (fs.existsSync(envPath)) {
495
497
  return fs.readFileSync(envPath, 'utf8');
496
498
  }
497
- } catch (err) {}
499
+ } catch (err) {
500
+ // File read failure is acceptable - file may not exist or have permission issues
501
+ // Return empty string to allow caller to proceed with defaults
502
+ }
498
503
  return '';
499
504
  }
500
505
 
@@ -535,7 +540,7 @@ function writeEnvTokens(tokens, preserveExisting = true) {
535
540
 
536
541
  // Add/update tokens - PRESERVE existing values if preserveExisting is true
537
542
  Object.entries(tokens).forEach(([key, value]) => {
538
- if (value && value.trim()) {
543
+ if (value?.trim()) {
539
544
  if (preserveExisting && existingKeys.has(key)) {
540
545
  // Keep existing value, don't overwrite
541
546
  preserved.push(key);
@@ -552,12 +557,14 @@ function writeEnvTokens(tokens, preserveExisting = true) {
552
557
 
553
558
  // Add header if new file
554
559
  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('');
560
+ outputLines.push(
561
+ '# External Service API Keys for Forge Workflow',
562
+ '# Get your keys from:',
563
+ '# Parallel AI: https://platform.parallel.ai',
564
+ '# Greptile: https://app.greptile.com/api',
565
+ '# SonarCloud: https://sonarcloud.io/account/security',
566
+ ''
567
+ );
561
568
  }
562
569
 
563
570
  // Add existing content (preserve order and comments)
@@ -591,7 +598,10 @@ function writeEnvTokens(tokens, preserveExisting = true) {
591
598
  if (!gitignore.includes('.env.local')) {
592
599
  fs.appendFileSync(gitignorePath, '\n# Local environment variables\n.env.local\n');
593
600
  }
594
- } catch (err) {}
601
+ } catch (err) {
602
+ // Gitignore update is optional - failure doesn't prevent .env.local creation
603
+ // User can manually add .env.local to .gitignore if needed
604
+ }
595
605
 
596
606
  return { added, preserved };
597
607
  }
@@ -648,7 +658,7 @@ async function askYesNo(question, prompt, defaultNo = true) {
648
658
  const normalized = answer.trim().toLowerCase();
649
659
 
650
660
  // Handle empty input (use default)
651
- if (normalized === '') return defaultNo ? false : true;
661
+ if (normalized === '') return !defaultNo;
652
662
 
653
663
  // Accept yes variations
654
664
  if (normalized === 'y' || normalized === 'yes') return true;
@@ -709,6 +719,238 @@ function detectProjectStatus() {
709
719
  return status;
710
720
  }
711
721
 
722
+ // Helper: Detect test framework from dependencies
723
+ function detectTestFramework(deps) {
724
+ if (deps.jest) return 'jest';
725
+ if (deps.vitest) return 'vitest';
726
+ if (deps.mocha) return 'mocha';
727
+ if (deps['@playwright/test']) return 'playwright';
728
+ if (deps.cypress) return 'cypress';
729
+ if (deps.karma) return 'karma';
730
+ return null;
731
+ }
732
+
733
+ // Helper: Detect language features (TypeScript, monorepo, Docker, CI/CD)
734
+ function detectLanguageFeatures(pkg) {
735
+ const features = {
736
+ typescript: false,
737
+ monorepo: false,
738
+ docker: false,
739
+ cicd: false
740
+ };
741
+
742
+ // Detect TypeScript
743
+ if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) {
744
+ features.typescript = true;
745
+ }
746
+
747
+ // Detect monorepo
748
+ if (pkg.workspaces ||
749
+ fs.existsSync(path.join(projectRoot, 'pnpm-workspace.yaml')) ||
750
+ fs.existsSync(path.join(projectRoot, 'lerna.json'))) {
751
+ features.monorepo = true;
752
+ }
753
+
754
+ // Detect Docker
755
+ if (fs.existsSync(path.join(projectRoot, 'Dockerfile')) ||
756
+ fs.existsSync(path.join(projectRoot, 'docker-compose.yml'))) {
757
+ features.docker = true;
758
+ }
759
+
760
+ // Detect CI/CD
761
+ if (fs.existsSync(path.join(projectRoot, '.github/workflows')) ||
762
+ fs.existsSync(path.join(projectRoot, '.gitlab-ci.yml')) ||
763
+ fs.existsSync(path.join(projectRoot, 'azure-pipelines.yml')) ||
764
+ fs.existsSync(path.join(projectRoot, '.circleci/config.yml'))) {
765
+ features.cicd = true;
766
+ }
767
+
768
+ return features;
769
+ }
770
+
771
+ // Helper: Detect Next.js framework
772
+ function detectNextJs(deps) {
773
+ if (!deps.next) return null;
774
+
775
+ return {
776
+ framework: 'Next.js',
777
+ frameworkConfidence: 100,
778
+ projectType: 'fullstack',
779
+ buildTool: 'next',
780
+ testFramework: detectTestFramework(deps)
781
+ };
782
+ }
783
+
784
+ // Helper: Detect NestJS framework
785
+ function detectNestJs(deps) {
786
+ if (!deps['@nestjs/core'] && !deps['@nestjs/common']) return null;
787
+
788
+ return {
789
+ framework: 'NestJS',
790
+ frameworkConfidence: 100,
791
+ projectType: 'backend',
792
+ buildTool: 'nest',
793
+ testFramework: 'jest'
794
+ };
795
+ }
796
+
797
+ // Helper: Detect Angular framework
798
+ function detectAngular(deps) {
799
+ if (!deps['@angular/core'] && !deps['@angular/cli']) return null;
800
+
801
+ return {
802
+ framework: 'Angular',
803
+ frameworkConfidence: 100,
804
+ projectType: 'frontend',
805
+ buildTool: 'ng',
806
+ testFramework: 'karma'
807
+ };
808
+ }
809
+
810
+ // Helper: Detect Vue.js framework
811
+ function detectVue(deps) {
812
+ if (!deps.vue) return null;
813
+
814
+ if (deps.nuxt) {
815
+ return {
816
+ framework: 'Nuxt',
817
+ frameworkConfidence: 100,
818
+ projectType: 'fullstack',
819
+ buildTool: 'nuxt',
820
+ testFramework: detectTestFramework(deps)
821
+ };
822
+ }
823
+
824
+ const hasVite = deps.vite;
825
+ const hasWebpack = deps.webpack;
826
+
827
+ return {
828
+ framework: 'Vue.js',
829
+ frameworkConfidence: deps['@vue/cli'] ? 100 : 90,
830
+ projectType: 'frontend',
831
+ buildTool: hasVite ? 'vite' : (hasWebpack ? 'webpack' : 'vue-cli'),
832
+ testFramework: detectTestFramework(deps)
833
+ };
834
+ }
835
+
836
+ // Helper: Detect React framework
837
+ function detectReact(deps) {
838
+ if (!deps.react) return null;
839
+
840
+ const hasVite = deps.vite;
841
+ const hasReactScripts = deps['react-scripts'];
842
+
843
+ return {
844
+ framework: 'React',
845
+ frameworkConfidence: 95,
846
+ projectType: 'frontend',
847
+ buildTool: hasVite ? 'vite' : (hasReactScripts ? 'create-react-app' : 'webpack'),
848
+ testFramework: detectTestFramework(deps)
849
+ };
850
+ }
851
+
852
+ // Helper: Detect Express framework
853
+ function detectExpress(deps, features) {
854
+ if (!deps.express) return null;
855
+
856
+ return {
857
+ framework: 'Express',
858
+ frameworkConfidence: 90,
859
+ projectType: 'backend',
860
+ buildTool: features.typescript ? 'tsc' : 'node',
861
+ testFramework: detectTestFramework(deps)
862
+ };
863
+ }
864
+
865
+ // Helper: Detect Fastify framework
866
+ function detectFastify(deps, features) {
867
+ if (!deps.fastify) return null;
868
+
869
+ return {
870
+ framework: 'Fastify',
871
+ frameworkConfidence: 95,
872
+ projectType: 'backend',
873
+ buildTool: features.typescript ? 'tsc' : 'node',
874
+ testFramework: detectTestFramework(deps)
875
+ };
876
+ }
877
+
878
+ // Helper: Detect Svelte framework
879
+ function detectSvelte(deps) {
880
+ if (!deps.svelte) return null;
881
+
882
+ if (deps['@sveltejs/kit']) {
883
+ return {
884
+ framework: 'SvelteKit',
885
+ frameworkConfidence: 100,
886
+ projectType: 'fullstack',
887
+ buildTool: 'vite',
888
+ testFramework: detectTestFramework(deps)
889
+ };
890
+ }
891
+
892
+ return {
893
+ framework: 'Svelte',
894
+ frameworkConfidence: 95,
895
+ projectType: 'frontend',
896
+ buildTool: 'vite',
897
+ testFramework: detectTestFramework(deps)
898
+ };
899
+ }
900
+
901
+ // Helper: Detect Remix framework
902
+ function detectRemix(deps) {
903
+ if (!deps['@remix-run/react']) return null;
904
+
905
+ return {
906
+ framework: 'Remix',
907
+ frameworkConfidence: 100,
908
+ projectType: 'fullstack',
909
+ buildTool: 'remix',
910
+ testFramework: detectTestFramework(deps)
911
+ };
912
+ }
913
+
914
+ // Helper: Detect Astro framework
915
+ function detectAstro(deps) {
916
+ if (!deps.astro) return null;
917
+
918
+ return {
919
+ framework: 'Astro',
920
+ frameworkConfidence: 100,
921
+ projectType: 'frontend',
922
+ buildTool: 'astro',
923
+ testFramework: detectTestFramework(deps)
924
+ };
925
+ }
926
+
927
+ // Helper: Detect generic Node.js project
928
+ function detectGenericNodeJs(pkg, deps, features) {
929
+ if (!pkg.main && !pkg.scripts?.start) return null;
930
+
931
+ return {
932
+ framework: 'Node.js',
933
+ frameworkConfidence: 70,
934
+ projectType: 'backend',
935
+ buildTool: features.typescript ? 'tsc' : 'node',
936
+ testFramework: detectTestFramework(deps)
937
+ };
938
+ }
939
+
940
+ // Helper: Detect generic JavaScript/TypeScript project (fallback)
941
+ function detectGenericProject(deps, features) {
942
+ const hasVite = deps.vite;
943
+ const hasWebpack = deps.webpack;
944
+
945
+ return {
946
+ framework: features.typescript ? 'TypeScript' : 'JavaScript',
947
+ frameworkConfidence: 60,
948
+ projectType: 'library',
949
+ buildTool: hasVite ? 'vite' : (hasWebpack ? 'webpack' : 'npm'),
950
+ testFramework: detectTestFramework(deps)
951
+ };
952
+ }
953
+
712
954
  // Detect project type from package.json
713
955
  function detectProjectType() {
714
956
  const detection = {
@@ -733,174 +975,34 @@ function detectProjectType() {
733
975
 
734
976
  detection.hasPackageJson = true;
735
977
 
736
- // Detect TypeScript
737
- if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) {
738
- detection.features.typescript = true;
978
+ // Detect language features
979
+ detection.features = detectLanguageFeatures(pkg);
980
+ if (detection.features.typescript) {
739
981
  detection.language = 'typescript';
740
982
  }
741
983
 
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
984
  // Framework detection with confidence scoring
761
985
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
762
986
 
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);
987
+ // Try framework detectors in priority order
988
+ const frameworkResult =
989
+ detectNextJs(deps) ||
990
+ detectNestJs(deps) ||
991
+ detectAngular(deps) ||
992
+ detectVue(deps) ||
993
+ detectReact(deps) ||
994
+ detectExpress(deps, detection.features) ||
995
+ detectFastify(deps, detection.features) ||
996
+ detectSvelte(deps) ||
997
+ detectRemix(deps) ||
998
+ detectAstro(deps) ||
999
+ detectGenericNodeJs(pkg, deps, detection.features) ||
1000
+ detectGenericProject(deps, detection.features);
1001
+
1002
+ // Merge framework detection results
1003
+ if (frameworkResult) {
1004
+ Object.assign(detection, frameworkResult);
1005
+ }
904
1006
 
905
1007
  return detection;
906
1008
  }
@@ -1045,9 +1147,7 @@ function updateAgentsMdWithProjectType(detection) {
1045
1147
  // Add framework-specific tips
1046
1148
  const tips = generateFrameworkTips(detection);
1047
1149
  if (tips.length > 0) {
1048
- metadata.push('');
1049
- metadata.push('**Framework conventions**:');
1050
- metadata.push(...tips);
1150
+ metadata.push('', '**Framework conventions**:', ...tips);
1051
1151
  }
1052
1152
 
1053
1153
  // Insert metadata
@@ -1056,142 +1156,149 @@ function updateAgentsMdWithProjectType(detection) {
1056
1156
  fs.writeFileSync(agentsPath, lines.join('\n'), 'utf-8');
1057
1157
  }
1058
1158
 
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);
1159
+ // Helper: Calculate estimated tokens (rough: ~4 chars per token)
1160
+ function estimateTokens(bytes) {
1161
+ return Math.ceil(bytes / 4);
1162
+ }
1066
1163
 
1067
- const result = {
1068
- createAgentsMd: false,
1069
- createClaudeMd: false,
1070
- skipAgentsMd: false,
1071
- skipClaudeMd: false
1164
+ // Helper: Create instruction files result object
1165
+ function createInstructionFilesResult(createAgentsMd = false, createClaudeMd = false, skipAgentsMd = false, skipClaudeMd = false) {
1166
+ return {
1167
+ createAgentsMd,
1168
+ createClaudeMd,
1169
+ skipAgentsMd,
1170
+ skipClaudeMd
1072
1171
  };
1172
+ }
1073
1173
 
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);
1174
+ // Helper: Handle scenario where both AGENTS.md and CLAUDE.md exist
1175
+ async function handleBothFilesExist(question, projectStatus) {
1176
+ const totalLines = projectStatus.agentsMdLines + projectStatus.claudeMdLines;
1177
+ const totalTokens = estimateTokens(projectStatus.agentsMdSize + projectStatus.claudeMdSize);
1078
1178
 
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('');
1179
+ console.log('');
1180
+ console.log('⚠️ WARNING: Multiple Instruction Files Detected');
1181
+ console.log('='.repeat(60));
1182
+ console.log(` AGENTS.md: ${projectStatus.agentsMdLines} lines (~${estimateTokens(projectStatus.agentsMdSize)} tokens)`);
1183
+ console.log(` CLAUDE.md: ${projectStatus.claudeMdLines} lines (~${estimateTokens(projectStatus.claudeMdSize)} tokens)`);
1184
+ console.log(` Total: ${totalLines} lines (~${totalTokens} tokens)`);
1185
+ console.log('');
1186
+ console.log(' ⚠️ Claude Code reads BOTH files on every request');
1187
+ console.log(' ⚠️ This increases context usage and costs');
1188
+ console.log('');
1189
+ console.log(' Options:');
1190
+ console.log(' 1) Keep CLAUDE.md only (recommended for Claude Code only)');
1191
+ console.log(' 2) Keep AGENTS.md only (recommended for multi-agent users)');
1192
+ console.log(' 3) Keep both (higher context usage)');
1193
+ console.log('');
1094
1194
 
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
- }
1195
+ while (true) {
1196
+ const choice = await question('Your choice (1/2/3) [2]: ');
1197
+ const normalized = choice.trim() || '2';
1198
+
1199
+ if (normalized === '1') {
1200
+ console.log(' ✓ Will keep CLAUDE.md, remove AGENTS.md');
1201
+ return createInstructionFilesResult(false, false, true, false);
1202
+ } else if (normalized === '2') {
1203
+ console.log(' ✓ Will keep AGENTS.md, remove CLAUDE.md');
1204
+ return createInstructionFilesResult(false, false, false, true);
1205
+ } else if (normalized === '3') {
1206
+ console.log(' ✓ Will keep both files (context: ~' + totalTokens + ' tokens)');
1207
+ return createInstructionFilesResult(false, false, false, false);
1208
+ } else {
1209
+ console.log(' Please enter 1, 2, or 3');
1117
1210
  }
1118
-
1119
- return result;
1120
1211
  }
1212
+ }
1121
1213
 
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('');
1214
+ // Helper: Handle scenario where only CLAUDE.md exists
1215
+ async function handleOnlyClaudeMdExists(question, projectStatus, hasOtherAgents) {
1216
+ if (hasOtherAgents) {
1217
+ console.log('');
1218
+ console.log('📋 Found existing CLAUDE.md (' + projectStatus.claudeMdLines + ' lines)');
1219
+ console.log(' You selected multiple agents. Recommendation:');
1220
+ console.log(' → Migrate to AGENTS.md (works with all agents)');
1221
+ console.log('');
1130
1222
 
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
- }
1223
+ const migrate = await askYesNo(question, 'Migrate CLAUDE.md to AGENTS.md?', false);
1224
+ if (migrate) {
1225
+ console.log(' ✓ Will migrate content to AGENTS.md');
1226
+ return createInstructionFilesResult(true, false, false, true);
1141
1227
  } else {
1142
- // Claude Code only - keep CLAUDE.md
1143
- result.createClaudeMd = false; // Keep existing
1144
- console.log(' ✓ Keeping existing CLAUDE.md');
1228
+ console.log(' ✓ Will keep CLAUDE.md and create AGENTS.md');
1229
+ return createInstructionFilesResult(true, false, false, false);
1145
1230
  }
1146
-
1147
- return result;
1231
+ } else {
1232
+ // Claude Code only - keep CLAUDE.md
1233
+ console.log(' ✓ Keeping existing CLAUDE.md');
1234
+ return createInstructionFilesResult(false, false, false, false);
1148
1235
  }
1236
+ }
1149
1237
 
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('');
1238
+ // Helper: Handle scenario where only AGENTS.md exists
1239
+ async function handleOnlyAgentsMdExists(question, projectStatus, hasClaude, hasOtherAgents) {
1240
+ if (hasClaude && !hasOtherAgents) {
1241
+ console.log('');
1242
+ console.log('📋 Found existing AGENTS.md (' + projectStatus.agentsMdLines + ' lines)');
1243
+ console.log(' You selected Claude Code only. Options:');
1244
+ console.log(' 1) Keep AGENTS.md (works fine)');
1245
+ console.log(' 2) Rename to CLAUDE.md (Claude-specific naming)');
1246
+ console.log('');
1159
1247
 
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
- }
1248
+ const rename = await askYesNo(question, 'Rename to CLAUDE.md?', true);
1249
+ if (rename) {
1250
+ console.log(' ✓ Will rename to CLAUDE.md');
1251
+ return createInstructionFilesResult(false, true, true, false);
1169
1252
  } else {
1170
- // Multi-agent or other agents - keep AGENTS.md
1171
- result.createAgentsMd = false; // Keep existing
1172
- console.log(' ✓ Keeping existing AGENTS.md');
1253
+ console.log(' ✓ Keeping AGENTS.md');
1254
+ return createInstructionFilesResult(false, false, false, false);
1173
1255
  }
1174
-
1175
- return result;
1256
+ } else {
1257
+ // Multi-agent or other agents - keep AGENTS.md
1258
+ console.log(' ✓ Keeping existing AGENTS.md');
1259
+ return createInstructionFilesResult(false, false, false, false);
1176
1260
  }
1261
+ }
1177
1262
 
1178
- // Scenario 4: Neither file exists (fresh install)
1263
+ // Helper: Handle scenario where no instruction files exist (fresh install)
1264
+ function handleNoFilesExist(hasClaude, hasOtherAgents) {
1179
1265
  if (hasClaude && !hasOtherAgents) {
1180
1266
  // Claude Code only → create CLAUDE.md
1181
- result.createClaudeMd = true;
1182
1267
  console.log(' ✓ Will create CLAUDE.md (Claude Code specific)');
1268
+ return createInstructionFilesResult(false, true, false, false);
1183
1269
  } else if (!hasClaude && hasOtherAgents) {
1184
1270
  // Other agents only → create AGENTS.md
1185
- result.createAgentsMd = true;
1186
1271
  console.log(' ✓ Will create AGENTS.md (universal)');
1272
+ return createInstructionFilesResult(true, false, false, false);
1187
1273
  } else {
1188
1274
  // Multiple agents including Claude → create AGENTS.md + reference CLAUDE.md
1189
- result.createAgentsMd = true;
1190
- result.createClaudeMd = true; // Will be minimal reference
1191
1275
  console.log(' ✓ Will create AGENTS.md (main) + CLAUDE.md (reference)');
1276
+ return createInstructionFilesResult(true, true, false, false);
1277
+ }
1278
+ }
1279
+
1280
+ // Smart file selection with context warnings
1281
+ async function handleInstructionFiles(rl, question, selectedAgents, projectStatus) {
1282
+ const hasClaude = selectedAgents.some(a => a.key === 'claude');
1283
+ const hasOtherAgents = selectedAgents.some(a => a.key !== 'claude');
1284
+
1285
+ // Scenario 1: Both files exist (potential context bloat)
1286
+ if (projectStatus.hasAgentsMd && projectStatus.hasClaudeMd) {
1287
+ return await handleBothFilesExist(question, projectStatus);
1192
1288
  }
1193
1289
 
1194
- return result;
1290
+ // Scenario 2: Only CLAUDE.md exists
1291
+ if (projectStatus.hasClaudeMd && !projectStatus.hasAgentsMd) {
1292
+ return await handleOnlyClaudeMdExists(question, projectStatus, hasOtherAgents);
1293
+ }
1294
+
1295
+ // Scenario 3: Only AGENTS.md exists
1296
+ if (projectStatus.hasAgentsMd && !projectStatus.hasClaudeMd) {
1297
+ return await handleOnlyAgentsMdExists(question, projectStatus, hasClaude, hasOtherAgents);
1298
+ }
1299
+
1300
+ // Scenario 4: Neither file exists (fresh install)
1301
+ return handleNoFilesExist(hasClaude, hasOtherAgents);
1195
1302
  }
1196
1303
 
1197
1304
  // Create minimal CLAUDE.md that references AGENTS.md
@@ -1284,18 +1391,20 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
1284
1391
  const codeReviewChoice = await question('Select [1]: ') || '1';
1285
1392
 
1286
1393
  switch (codeReviewChoice) {
1287
- case '1':
1394
+ case '1': {
1288
1395
  tokens['CODE_REVIEW_TOOL'] = 'github-code-quality';
1289
1396
  console.log(' ✓ Using GitHub Code Quality (FREE)');
1290
1397
  break;
1291
- case '2':
1398
+ }
1399
+ case '2': {
1292
1400
  tokens['CODE_REVIEW_TOOL'] = 'coderabbit';
1293
1401
  console.log(' ✓ Using CodeRabbit - Install the GitHub App to activate');
1294
1402
  console.log(' https://coderabbit.ai');
1295
1403
  break;
1296
- case '3':
1404
+ }
1405
+ case '3': {
1297
1406
  const greptileKey = await question(' Enter Greptile API key: ');
1298
- if (greptileKey && greptileKey.trim()) {
1407
+ if (greptileKey?.trim()) {
1299
1408
  tokens['CODE_REVIEW_TOOL'] = 'greptile';
1300
1409
  tokens['GREPTILE_API_KEY'] = greptileKey.trim();
1301
1410
  console.log(' ✓ Greptile configured');
@@ -1304,9 +1413,11 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
1304
1413
  console.log(' Skipped - No API key provided');
1305
1414
  }
1306
1415
  break;
1307
- default:
1416
+ }
1417
+ default: {
1308
1418
  tokens['CODE_REVIEW_TOOL'] = 'none';
1309
1419
  console.log(' Skipped code review integration');
1420
+ }
1310
1421
  }
1311
1422
 
1312
1423
  // ============================================
@@ -1332,15 +1443,16 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
1332
1443
  const codeQualityChoice = await question('Select [1]: ') || '1';
1333
1444
 
1334
1445
  switch (codeQualityChoice) {
1335
- case '1':
1446
+ case '1': {
1336
1447
  tokens['CODE_QUALITY_TOOL'] = 'eslint';
1337
1448
  console.log(' ✓ Using ESLint (built-in)');
1338
1449
  break;
1339
- case '2':
1450
+ }
1451
+ case '2': {
1340
1452
  const sonarToken = await question(' Enter SonarCloud token: ');
1341
1453
  const sonarOrg = await question(' Enter SonarCloud organization: ');
1342
1454
  const sonarProject = await question(' Enter SonarCloud project key: ');
1343
- if (sonarToken && sonarToken.trim()) {
1455
+ if (sonarToken?.trim()) {
1344
1456
  tokens['CODE_QUALITY_TOOL'] = 'sonarcloud';
1345
1457
  tokens['SONAR_TOKEN'] = sonarToken.trim();
1346
1458
  if (sonarOrg) tokens['SONAR_ORGANIZATION'] = sonarOrg.trim();
@@ -1351,7 +1463,8 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
1351
1463
  console.log(' Falling back to ESLint');
1352
1464
  }
1353
1465
  break;
1354
- case '3':
1466
+ }
1467
+ case '3': {
1355
1468
  console.log('');
1356
1469
  console.log(' SonarQube Self-Hosted Setup:');
1357
1470
  console.log(' docker run -d --name sonarqube -p 9000:9000 sonarqube:community');
@@ -1361,14 +1474,16 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
1361
1474
  const sqToken = await question(' Enter SonarQube token (optional): ');
1362
1475
  tokens['CODE_QUALITY_TOOL'] = 'sonarqube';
1363
1476
  tokens['SONARQUBE_URL'] = sqUrl;
1364
- if (sqToken && sqToken.trim()) {
1477
+ if (sqToken?.trim()) {
1365
1478
  tokens['SONARQUBE_TOKEN'] = sqToken.trim();
1366
1479
  }
1367
1480
  console.log(' ✓ SonarQube self-hosted configured');
1368
1481
  break;
1369
- default:
1482
+ }
1483
+ default: {
1370
1484
  tokens['CODE_QUALITY_TOOL'] = 'none';
1371
1485
  console.log(' Skipped code quality integration');
1486
+ }
1372
1487
  }
1373
1488
 
1374
1489
  // ============================================
@@ -1390,7 +1505,7 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
1390
1505
 
1391
1506
  if (researchChoice === '2') {
1392
1507
  const parallelKey = await question(' Enter Parallel AI API key: ');
1393
- if (parallelKey && parallelKey.trim()) {
1508
+ if (parallelKey?.trim()) {
1394
1509
  tokens['PARALLEL_API_KEY'] = parallelKey.trim();
1395
1510
  console.log(' ✓ Parallel AI configured');
1396
1511
  } else {
@@ -1564,137 +1679,150 @@ function minimalInstall() {
1564
1679
  console.log('');
1565
1680
  }
1566
1681
 
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));
1682
+ // Helper: Setup Claude agent
1683
+ function setupClaudeAgent(skipFiles = {}) {
1684
+ // Copy commands from package (unless skipped)
1685
+ if (skipFiles.claudeCommands) {
1686
+ console.log(' Skipped: .claude/commands/ (keeping existing)');
1687
+ } else {
1688
+ COMMANDS.forEach(cmd => {
1689
+ const src = path.join(packageDir, `.claude/commands/${cmd}.md`);
1690
+ copyFile(src, `.claude/commands/${cmd}.md`);
1691
+ });
1692
+ console.log(' Copied: 9 workflow commands');
1693
+ }
1576
1694
 
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
- }
1695
+ // Copy rules
1696
+ const rulesSrc = path.join(packageDir, '.claude/rules/workflow.md');
1697
+ copyFile(rulesSrc, '.claude/rules/workflow.md');
1589
1698
 
1590
- // Copy rules
1591
- const rulesSrc = path.join(packageDir, '.claude/rules/workflow.md');
1592
- copyFile(rulesSrc, '.claude/rules/workflow.md');
1699
+ // Copy scripts
1700
+ const scriptSrc = path.join(packageDir, '.claude/scripts/load-env.sh');
1701
+ copyFile(scriptSrc, '.claude/scripts/load-env.sh');
1702
+ }
1593
1703
 
1594
- // Copy scripts
1595
- const scriptSrc = path.join(packageDir, '.claude/scripts/load-env.sh');
1596
- copyFile(scriptSrc, '.claude/scripts/load-env.sh');
1597
- }
1704
+ // Helper: Setup Cursor agent
1705
+ function setupCursorAgent() {
1706
+ writeFile('.cursor/rules/forge-workflow.mdc', CURSOR_RULE);
1707
+ console.log(' Created: .cursor/rules/forge-workflow.mdc');
1708
+ }
1598
1709
 
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');
1710
+ // Helper: Setup Aider agent
1711
+ function setupAiderAgent() {
1712
+ const aiderPath = path.join(projectRoot, '.aider.conf.yml');
1713
+ if (fs.existsSync(aiderPath)) {
1714
+ console.log(' Skipped: .aider.conf.yml already exists');
1715
+ return true; // Signal early return
1603
1716
  }
1604
1717
 
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
1718
+ writeFile('.aider.conf.yml', `# Aider configuration
1609
1719
  # Read AGENTS.md for workflow instructions
1610
1720
  read:
1611
1721
  - AGENTS.md
1612
1722
  - docs/WORKFLOW.md
1613
1723
  `);
1614
- console.log(' Created: .aider.conf.yml');
1615
- } else {
1616
- console.log(' Skipped: .aider.conf.yml already exists');
1617
- }
1618
- return;
1619
- }
1724
+ console.log(' Created: .aider.conf.yml');
1725
+ return true; // Signal early return
1726
+ }
1620
1727
 
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;
1728
+ // Helper: Convert command to agent-specific format
1729
+ function convertCommandToAgentFormat(cmd, content, agent) {
1730
+ let targetContent = content;
1731
+ let targetFile = cmd;
1626
1732
 
1627
- if (agent.needsConversion) {
1628
- targetContent = stripFrontmatter(content);
1629
- }
1733
+ if (agent.needsConversion) {
1734
+ targetContent = stripFrontmatter(content);
1735
+ }
1630
1736
 
1631
- if (agent.promptFormat) {
1632
- targetFile = cmd.replace('.md', '.prompt.md');
1633
- targetContent = stripFrontmatter(content);
1634
- }
1737
+ if (agent.promptFormat) {
1738
+ targetFile = cmd.replace('.md', '.prompt.md');
1739
+ targetContent = stripFrontmatter(content);
1740
+ }
1635
1741
 
1636
- if (agent.continueFormat) {
1637
- const baseName = cmd.replace('.md', '');
1638
- targetFile = `${baseName}.prompt`;
1639
- targetContent = `---
1742
+ if (agent.continueFormat) {
1743
+ const baseName = cmd.replace('.md', '');
1744
+ targetFile = `${baseName}.prompt`;
1745
+ targetContent = `---
1640
1746
  name: ${baseName}
1641
1747
  description: Forge workflow command - ${baseName}
1642
1748
  invokable: true
1643
1749
  ---
1644
1750
 
1645
1751
  ${stripFrontmatter(content)}`;
1646
- }
1752
+ }
1647
1753
 
1648
- const targetDir = agent.dirs[0]; // First dir is commands/workflows
1649
- writeFile(`${targetDir}/${targetFile}`, targetContent);
1650
- });
1651
- console.log(' Converted: 9 workflow commands');
1754
+ return { targetFile, targetContent };
1755
+ }
1756
+
1757
+ // Helper: Copy commands for agent
1758
+ function copyAgentCommands(agent, claudeCommands) {
1759
+ if (!claudeCommands) return;
1760
+ if (!agent.needsConversion && !agent.copyCommands && !agent.promptFormat && !agent.continueFormat) return;
1761
+
1762
+ Object.entries(claudeCommands).forEach(([cmd, content]) => {
1763
+ const { targetFile, targetContent } = convertCommandToAgentFormat(cmd, content, agent);
1764
+ const targetDir = agent.dirs[0]; // First dir is commands/workflows
1765
+ writeFile(`${targetDir}/${targetFile}`, targetContent);
1766
+ });
1767
+ console.log(' Converted: 9 workflow commands');
1768
+ }
1769
+
1770
+ // Helper: Copy rules for agent
1771
+ function copyAgentRules(agent) {
1772
+ if (!agent.needsConversion) return;
1773
+
1774
+ const workflowMdPath = path.join(projectRoot, '.claude/rules/workflow.md');
1775
+ if (!fs.existsSync(workflowMdPath)) return;
1776
+
1777
+ const rulesDir = agent.dirs.find(d => d.includes('/rules'));
1778
+ if (!rulesDir) return;
1779
+
1780
+ const ruleContent = readFile(workflowMdPath);
1781
+ if (ruleContent) {
1782
+ writeFile(`${rulesDir}/workflow.md`, ruleContent);
1652
1783
  }
1784
+ }
1653
1785
 
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
- }
1786
+ // Helper: Create skill file for agent
1787
+ function createAgentSkill(agent) {
1788
+ if (!agent.hasSkill) return;
1789
+
1790
+ const skillDir = agent.dirs.find(d => d.includes('/skills/'));
1791
+ if (skillDir) {
1792
+ writeFile(`${skillDir}/SKILL.md`, SKILL_CONTENT);
1793
+ console.log(' Created: forge-workflow skill');
1663
1794
  }
1795
+ }
1664
1796
 
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
- }
1797
+ // Helper: Setup MCP config for Claude
1798
+ function setupClaudeMcpConfig() {
1799
+ const mcpPath = path.join(projectRoot, '.mcp.json');
1800
+ if (fs.existsSync(mcpPath)) {
1801
+ console.log(' Skipped: .mcp.json already exists');
1802
+ return;
1672
1803
  }
1673
1804
 
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');
1805
+ const mcpConfig = {
1806
+ mcpServers: {
1807
+ context7: {
1808
+ command: 'npx',
1809
+ args: ['-y', '@upstash/context7-mcp@latest']
1810
+ }
1690
1811
  }
1812
+ };
1813
+ writeFile('.mcp.json', JSON.stringify(mcpConfig, null, 2));
1814
+ console.log(' Created: .mcp.json with Context7 MCP');
1815
+ }
1816
+
1817
+ // Helper: Setup MCP config for Continue
1818
+ function setupContinueMcpConfig() {
1819
+ const configPath = path.join(projectRoot, '.continue/config.yaml');
1820
+ if (fs.existsSync(configPath)) {
1821
+ console.log(' Skipped: config.yaml already exists');
1822
+ return;
1691
1823
  }
1692
1824
 
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
1825
+ const continueConfig = `# Continue Configuration
1698
1826
  # https://docs.continue.dev/customize/deep-dives/configuration
1699
1827
 
1700
1828
  name: Forge Workflow
@@ -1710,117 +1838,122 @@ mcpServers:
1710
1838
 
1711
1839
  # Rules loaded from .continuerules
1712
1840
  `;
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
- }
1718
- }
1841
+ writeFile('.continue/config.yaml', continueConfig);
1842
+ console.log(' Created: config.yaml with Context7 MCP');
1843
+ }
1719
1844
 
1720
- // 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}`);
1725
- }
1845
+ // Helper: Create agent link file
1846
+ function createAgentLinkFile(agent) {
1847
+ if (!agent.linkFile) return;
1848
+
1849
+ const result = createSymlinkOrCopy('AGENTS.md', agent.linkFile);
1850
+ if (result) {
1851
+ console.log(` ${result === 'linked' ? 'Linked' : 'Copied'}: ${agent.linkFile}`);
1726
1852
  }
1727
1853
  }
1728
1854
 
1729
- // Interactive setup
1730
- async function interactiveSetup() {
1731
- const rl = readline.createInterface({
1732
- input: process.stdin,
1733
- output: process.stdout
1734
- });
1855
+ // Setup specific agent
1856
+ function setupAgent(agentKey, claudeCommands, skipFiles = {}) {
1857
+ const agent = AGENTS[agentKey];
1858
+ if (!agent) return;
1735
1859
 
1736
- let setupCompleted = false;
1737
-
1738
- // Handle Ctrl+C gracefully
1739
- rl.on('close', () => {
1740
- if (!setupCompleted) {
1741
- console.log('\n\nSetup cancelled.');
1742
- process.exit(0);
1743
- }
1744
- });
1860
+ console.log(`\nSetting up ${agent.name}...`);
1745
1861
 
1746
- // Handle input errors
1747
- rl.on('error', (err) => {
1748
- console.error('Input error:', err.message);
1749
- process.exit(1);
1750
- });
1862
+ // Create directories
1863
+ agent.dirs.forEach(dir => ensureDir(dir));
1751
1864
 
1752
- const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
1865
+ // Handle agent-specific setup
1866
+ if (agentKey === 'claude') {
1867
+ setupClaudeAgent(skipFiles);
1868
+ }
1753
1869
 
1754
- showBanner('Agent Configuration');
1870
+ if (agent.customSetup === 'cursor') {
1871
+ setupCursorAgent();
1872
+ }
1755
1873
 
1756
- // Show target directory
1757
- console.log(` Target directory: ${process.cwd()}`);
1758
- console.log(' (Use --path <dir> to change target directory)');
1759
- console.log('');
1874
+ if (agent.customSetup === 'aider') {
1875
+ const shouldReturn = setupAiderAgent();
1876
+ if (shouldReturn) return;
1877
+ }
1760
1878
 
1761
- // Check prerequisites first
1762
- checkPrerequisites();
1763
- console.log('');
1879
+ // Convert/copy commands
1880
+ copyAgentCommands(agent, claudeCommands);
1764
1881
 
1765
- // =============================================
1766
- // PROJECT DETECTION
1767
- // =============================================
1768
- const projectStatus = detectProjectStatus();
1882
+ // Copy rules if needed
1883
+ copyAgentRules(agent);
1769
1884
 
1770
- if (projectStatus.type !== 'fresh') {
1771
- console.log('==============================================');
1772
- console.log(' Existing Installation Detected');
1773
- console.log('==============================================');
1774
- console.log('');
1885
+ // Create SKILL.md
1886
+ createAgentSkill(agent);
1775
1887
 
1776
- if (projectStatus.type === 'upgrade') {
1777
- console.log('Found existing Forge installation:');
1778
- } else {
1779
- console.log('Found partial installation:');
1780
- }
1888
+ // Setup MCP configs
1889
+ if (agentKey === 'claude') {
1890
+ setupClaudeMcpConfig();
1891
+ }
1781
1892
 
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('');
1893
+ if (agentKey === 'continue') {
1894
+ setupContinueMcpConfig();
1787
1895
  }
1788
1896
 
1789
- // Track which files to skip based on user choices
1790
- const skipFiles = {
1791
- agentsMd: false,
1792
- claudeCommands: false
1793
- };
1897
+ // Create link file
1898
+ createAgentLinkFile(agent);
1899
+ }
1794
1900
 
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
1901
 
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
- }
1902
+ // =============================================
1903
+ // Helper Functions for Interactive Setup
1904
+ // =============================================
1905
+
1906
+ /**
1907
+ * Display existing installation status
1908
+ */
1909
+ function displayInstallationStatus(projectStatus) {
1910
+ if (projectStatus.type === 'fresh') return;
1911
+
1912
+ console.log('==============================================');
1913
+ console.log(' Existing Installation Detected');
1914
+ console.log('==============================================');
1915
+ console.log('');
1916
+
1917
+ if (projectStatus.type === 'upgrade') {
1918
+ console.log('Found existing Forge installation:');
1919
+ } else {
1920
+ console.log('Found partial installation:');
1815
1921
  }
1816
1922
 
1817
- if (projectStatus.type !== 'fresh') {
1818
- console.log('');
1923
+ if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
1924
+ if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
1925
+ if (projectStatus.hasEnvLocal) console.log(' - .env.local');
1926
+ if (projectStatus.hasDocsWorkflow) console.log(' - docs/WORKFLOW.md');
1927
+ console.log('');
1928
+ }
1929
+
1930
+ /**
1931
+ * Prompt for file overwrite and update skipFiles
1932
+ */
1933
+ async function promptForFileOverwrite(question, fileType, exists, skipFiles) {
1934
+ if (!exists) return;
1935
+
1936
+ const fileLabels = {
1937
+ agentsMd: { prompt: 'Found existing AGENTS.md. Overwrite?', message: 'AGENTS.md', key: 'agentsMd' },
1938
+ claudeCommands: { prompt: 'Found existing .claude/commands/. Overwrite?', message: '.claude/commands/', key: 'claudeCommands' }
1939
+ };
1940
+
1941
+ const config = fileLabels[fileType];
1942
+ if (!config) return;
1943
+
1944
+ const overwrite = await askYesNo(question, config.prompt, true);
1945
+ if (overwrite) {
1946
+ console.log(` Will overwrite ${config.message}`);
1947
+ } else {
1948
+ skipFiles[config.key] = true;
1949
+ console.log(` Keeping existing ${config.message}`);
1819
1950
  }
1951
+ }
1820
1952
 
1821
- // =============================================
1822
- // STEP 1: Agent Selection
1823
- // =============================================
1953
+ /**
1954
+ * Display agent selection options
1955
+ */
1956
+ function displayAgentOptions(agentKeys) {
1824
1957
  console.log('STEP 1: Select AI Coding Agents');
1825
1958
  console.log('================================');
1826
1959
  console.log('');
@@ -1828,7 +1961,6 @@ async function interactiveSetup() {
1828
1961
  console.log('(Enter numbers separated by spaces, or "all")');
1829
1962
  console.log('');
1830
1963
 
1831
- const agentKeys = Object.keys(AGENTS);
1832
1964
  agentKeys.forEach((key, index) => {
1833
1965
  const agent = AGENTS[key];
1834
1966
  console.log(` ${(index + 1).toString().padStart(2)}) ${agent.name.padEnd(20)} - ${agent.description}`);
@@ -1836,105 +1968,133 @@ async function interactiveSetup() {
1836
1968
  console.log('');
1837
1969
  console.log(' all) Install for all agents');
1838
1970
  console.log('');
1971
+ }
1839
1972
 
1840
- let selectedAgents = [];
1973
+ /**
1974
+ * Validate and parse agent selection input
1975
+ */
1976
+ function validateAgentSelection(input, agentKeys) {
1977
+ // Handle empty input
1978
+ if (!input || !input.trim()) {
1979
+ return { valid: false, agents: [], message: 'Please enter at least one agent number or "all".' };
1980
+ }
1841
1981
 
1842
- // Loop until valid input is provided
1843
- while (selectedAgents.length === 0) {
1844
- const answer = await question('Your selection: ');
1982
+ // Handle "all" selection
1983
+ if (input.toLowerCase() === 'all') {
1984
+ return { valid: true, agents: agentKeys, message: null };
1985
+ }
1845
1986
 
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
- }
1987
+ // Parse numbers
1988
+ const nums = input.split(/[\s,]+/).map(n => Number.parseInt(n.trim())).filter(n => !Number.isNaN(n));
1851
1989
 
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));
1990
+ // Validate numbers are in range
1991
+ const validNums = nums.filter(n => n >= 1 && n <= agentKeys.length);
1992
+ const invalidNums = nums.filter(n => n < 1 || n > agentKeys.length);
1856
1993
 
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);
1994
+ if (invalidNums.length > 0) {
1995
+ console.log(` ⚠ Invalid numbers ignored: ${invalidNums.join(', ')} (valid: 1-${agentKeys.length})`);
1996
+ }
1860
1997
 
1861
- if (invalidNums.length > 0) {
1862
- console.log(` Invalid numbers ignored: ${invalidNums.join(', ')} (valid: 1-${agentKeys.length})`);
1863
- }
1998
+ // Deduplicate selected agents using Set
1999
+ const selectedAgents = [...new Set(validNums.map(n => agentKeys[n - 1]))].filter(Boolean);
1864
2000
 
1865
- // Deduplicate selected agents using Set
1866
- selectedAgents = [...new Set(validNums.map(n => agentKeys[n - 1]))].filter(Boolean);
1867
- }
2001
+ if (selectedAgents.length === 0) {
2002
+ return { valid: false, agents: [], message: 'No valid agents selected. Please try again.' };
2003
+ }
1868
2004
 
1869
- if (selectedAgents.length === 0) {
1870
- console.log(' No valid agents selected. Please try again.');
2005
+ return { valid: true, agents: selectedAgents, message: null };
2006
+ }
2007
+
2008
+ /**
2009
+ * Prompt for agent selection with validation loop
2010
+ */
2011
+ async function promptForAgentSelection(question, agentKeys) {
2012
+ displayAgentOptions(agentKeys);
2013
+
2014
+ let selectedAgents = [];
2015
+
2016
+ // Loop until valid input is provided
2017
+ while (selectedAgents.length === 0) {
2018
+ const answer = await question('Your selection: ');
2019
+ const result = validateAgentSelection(answer, agentKeys);
2020
+
2021
+ if (result.valid) {
2022
+ selectedAgents = result.agents;
2023
+ } else if (result.message) {
2024
+ console.log(` ${result.message}`);
1871
2025
  }
1872
2026
  }
1873
2027
 
1874
- console.log('');
1875
- console.log('Installing Forge workflow...');
2028
+ return selectedAgents;
2029
+ }
1876
2030
 
1877
- // Copy AGENTS.md unless skipped
2031
+ /**
2032
+ * Handle AGENTS.md installation
2033
+ */
2034
+ async function installAgentsMd(skipFiles) {
1878
2035
  if (skipFiles.agentsMd) {
1879
2036
  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');
2037
+ return;
2038
+ }
1883
2039
 
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);
2040
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
2041
+ const agentsDest = path.join(projectRoot, 'AGENTS.md');
2042
+
2043
+ // Try smart merge if file exists
2044
+ if (fs.existsSync(agentsDest)) {
2045
+ const existingContent = fs.readFileSync(agentsDest, 'utf8');
2046
+ const newContent = fs.readFileSync(agentsSrc, 'utf8');
2047
+ const merged = smartMergeAgentsMd(existingContent, newContent);
2048
+
2049
+ if (merged) {
2050
+ fs.writeFileSync(agentsDest, merged, 'utf8');
2051
+ console.log(' Updated: AGENTS.md (preserved USER sections)');
2052
+ } else if (copyFile(agentsSrc, 'AGENTS.md')) {
2053
+ // No markers, do normal copy (user already approved overwrite)
2054
+ console.log(' Updated: AGENTS.md (universal standard)');
2055
+ }
2056
+ } else if (copyFile(agentsSrc, 'AGENTS.md')) {
2057
+ // New file
2058
+ console.log(' Created: AGENTS.md (universal standard)');
1889
2059
 
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
- }
2060
+ // Detect project type and update AGENTS.md
2061
+ const detection = detectProjectType();
2062
+ if (detection.hasPackageJson) {
2063
+ updateAgentsMdWithProjectType(detection);
2064
+ displayProjectType(detection);
1911
2065
  }
1912
2066
  }
1913
- console.log('');
2067
+ }
1914
2068
 
1915
- // Setup core documentation
1916
- setupCoreDocs();
1917
- console.log('');
2069
+ /**
2070
+ * Load Claude commands for conversion
2071
+ */
2072
+ function loadClaudeCommands(selectedAgents) {
2073
+ const claudeCommands = {};
2074
+ const needsClaudeCommands = selectedAgents.includes('claude') ||
2075
+ selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands);
1918
2076
 
1919
- // 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
- });
2077
+ if (!needsClaudeCommands) {
2078
+ return claudeCommands;
1934
2079
  }
1935
2080
 
1936
- // Setup each selected agent with progress indication
2081
+ COMMANDS.forEach(cmd => {
2082
+ const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
2083
+ const content = readFile(cmdPath);
2084
+ if (content) {
2085
+ claudeCommands[`${cmd}.md`] = content;
2086
+ }
2087
+ });
2088
+
2089
+ return claudeCommands;
2090
+ }
2091
+
2092
+ /**
2093
+ * Setup agents with progress indication
2094
+ */
2095
+ function setupAgentsWithProgress(selectedAgents, claudeCommands, skipFiles) {
1937
2096
  const totalAgents = selectedAgents.length;
2097
+
1938
2098
  selectedAgents.forEach((agentKey, index) => {
1939
2099
  const agent = AGENTS[agentKey];
1940
2100
  console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
@@ -1952,22 +2112,12 @@ async function interactiveSetup() {
1952
2112
  const agent = AGENTS[key];
1953
2113
  console.log(` * ${agent.name}`);
1954
2114
  });
2115
+ }
1955
2116
 
1956
- // =============================================
1957
- // STEP 2: External Services Configuration
1958
- // =============================================
1959
- console.log('');
1960
- console.log('STEP 2: External Services (Optional)');
1961
- console.log('=====================================');
1962
-
1963
- await configureExternalServices(rl, question, selectedAgents, projectStatus);
1964
-
1965
- setupCompleted = true;
1966
- rl.close();
1967
-
1968
- // =============================================
1969
- // Final Summary
1970
- // =============================================
2117
+ /**
2118
+ * Display final setup summary
2119
+ */
2120
+ function displaySetupSummary(selectedAgents) {
1971
2121
  console.log('');
1972
2122
  console.log('==============================================');
1973
2123
  console.log(` Forge v${VERSION} Setup Complete!`);
@@ -1978,6 +2128,7 @@ async function interactiveSetup() {
1978
2128
  console.log(' - docs/WORKFLOW.md (full workflow guide)');
1979
2129
  console.log(' - docs/research/TEMPLATE.md (research template)');
1980
2130
  console.log(' - docs/planning/PROGRESS.md (progress tracking)');
2131
+
1981
2132
  selectedAgents.forEach(key => {
1982
2133
  const agent = AGENTS[key];
1983
2134
  if (agent.linkFile) {
@@ -1993,6 +2144,7 @@ async function interactiveSetup() {
1993
2144
  }
1994
2145
  }
1995
2146
  });
2147
+
1996
2148
  console.log('');
1997
2149
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1998
2150
  console.log('📋 NEXT STEP - Complete AGENTS.md');
@@ -2024,6 +2176,112 @@ async function interactiveSetup() {
2024
2176
  console.log('');
2025
2177
  }
2026
2178
 
2179
+
2180
+ // Interactive setup
2181
+ async function interactiveSetup() {
2182
+ const rl = readline.createInterface({
2183
+ input: process.stdin,
2184
+ output: process.stdout
2185
+ });
2186
+
2187
+ let setupCompleted = false;
2188
+
2189
+ // Handle Ctrl+C gracefully
2190
+ rl.on('close', () => {
2191
+ if (!setupCompleted) {
2192
+ console.log('\n\nSetup cancelled.');
2193
+ process.exit(0);
2194
+ }
2195
+ });
2196
+
2197
+ // Handle input errors
2198
+ rl.on('error', (err) => {
2199
+ console.error('Input error:', err.message);
2200
+ process.exit(1);
2201
+ });
2202
+
2203
+ const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
2204
+
2205
+ showBanner('Agent Configuration');
2206
+
2207
+ // Show target directory
2208
+ console.log(` Target directory: ${process.cwd()}`);
2209
+ console.log(' (Use --path <dir> to change target directory)');
2210
+ console.log('');
2211
+
2212
+ // Check prerequisites first
2213
+ checkPrerequisites();
2214
+ console.log('');
2215
+
2216
+ // =============================================
2217
+ // PROJECT DETECTION
2218
+ // =============================================
2219
+ const projectStatus = detectProjectStatus();
2220
+ displayInstallationStatus(projectStatus);
2221
+
2222
+ // Track which files to skip based on user choices
2223
+ const skipFiles = {
2224
+ agentsMd: false,
2225
+ claudeCommands: false
2226
+ };
2227
+
2228
+ // Ask about overwriting existing files
2229
+ await promptForFileOverwrite(question, 'agentsMd', projectStatus.hasAgentsMd, skipFiles);
2230
+ await promptForFileOverwrite(question, 'claudeCommands', projectStatus.hasClaudeCommands, skipFiles);
2231
+
2232
+ if (projectStatus.type !== 'fresh') {
2233
+ console.log('');
2234
+ }
2235
+
2236
+ // =============================================
2237
+ // STEP 1: Agent Selection
2238
+ // =============================================
2239
+ const agentKeys = Object.keys(AGENTS);
2240
+ const selectedAgents = await promptForAgentSelection(question, agentKeys);
2241
+
2242
+ console.log('');
2243
+ console.log('Installing Forge workflow...');
2244
+
2245
+ // Install AGENTS.md
2246
+ await installAgentsMd(skipFiles);
2247
+ console.log('');
2248
+
2249
+ // Setup core documentation
2250
+ setupCoreDocs();
2251
+ console.log('');
2252
+
2253
+ // Load Claude commands if needed
2254
+ let claudeCommands = {};
2255
+ if (selectedAgents.includes('claude') || selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands)) {
2256
+ // First ensure Claude is set up
2257
+ if (selectedAgents.includes('claude')) {
2258
+ setupAgent('claude', null, skipFiles);
2259
+ }
2260
+ // Then load the commands
2261
+ claudeCommands = loadClaudeCommands(selectedAgents);
2262
+ }
2263
+
2264
+ // Setup each selected agent with progress indication
2265
+ setupAgentsWithProgress(selectedAgents, claudeCommands, skipFiles);
2266
+
2267
+ // =============================================
2268
+ // STEP 2: External Services Configuration
2269
+ // =============================================
2270
+ console.log('');
2271
+ console.log('STEP 2: External Services (Optional)');
2272
+ console.log('=====================================');
2273
+
2274
+ await configureExternalServices(rl, question, selectedAgents, projectStatus);
2275
+
2276
+ setupCompleted = true;
2277
+ rl.close();
2278
+
2279
+ // =============================================
2280
+ // Final Summary
2281
+ // =============================================
2282
+ displaySetupSummary(selectedAgents);
2283
+ }
2284
+
2027
2285
  // Parse CLI flags
2028
2286
  function parseFlags() {
2029
2287
  const flags = {
@@ -2035,39 +2293,51 @@ function parseFlags() {
2035
2293
  path: null
2036
2294
  };
2037
2295
 
2038
- for (let i = 0; i < args.length; i++) {
2296
+ for (let i = 0; i < args.length; ) {
2039
2297
  const arg = args[i];
2040
2298
 
2041
2299
  if (arg === '--quick' || arg === '-q') {
2042
2300
  flags.quick = true;
2301
+ i++;
2043
2302
  } else if (arg === '--skip-external' || arg === '--skip-services') {
2044
2303
  flags.skipExternal = true;
2304
+ i++;
2045
2305
  } else if (arg === '--all') {
2046
2306
  flags.all = true;
2307
+ i++;
2047
2308
  } else if (arg === '--help' || arg === '-h') {
2048
2309
  flags.help = true;
2310
+ i++;
2049
2311
  } else if (arg === '--path' || arg === '-p') {
2050
2312
  // --path <directory> or -p <directory>
2051
2313
  if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
2052
2314
  flags.path = args[i + 1];
2053
- i++; // Skip next arg
2315
+ i += 2; // Skip current and next arg
2316
+ } else {
2317
+ i++;
2054
2318
  }
2055
2319
  } else if (arg.startsWith('--path=')) {
2056
2320
  // --path=/some/dir format
2057
2321
  flags.path = arg.replace('--path=', '');
2322
+ i++;
2058
2323
  } else if (arg === '--agents') {
2059
2324
  // --agents claude cursor format
2060
2325
  const agentList = [];
2061
- for (let j = i + 1; j < args.length; j++) {
2062
- if (args[j].startsWith('-')) break;
2326
+ let j = i + 1;
2327
+ while (j < args.length && !args[j].startsWith('-')) {
2063
2328
  agentList.push(args[j]);
2329
+ j++;
2064
2330
  }
2065
2331
  if (agentList.length > 0) {
2066
2332
  flags.agents = agentList.join(',');
2067
2333
  }
2334
+ i = j; // Skip all consumed arguments
2068
2335
  } else if (arg.startsWith('--agents=')) {
2069
2336
  // --agents=claude,cursor format
2070
2337
  flags.agents = arg.replace('--agents=', '');
2338
+ i++;
2339
+ } else {
2340
+ i++;
2071
2341
  }
2072
2342
  }
2073
2343
 
@@ -2188,7 +2458,10 @@ async function quickSetup(selectedAgents, skipExternal) {
2188
2458
  });
2189
2459
 
2190
2460
  // Configure external services with defaults (unless skipped)
2191
- if (!skipExternal) {
2461
+ if (skipExternal) {
2462
+ console.log('');
2463
+ console.log('Skipping external services configuration...');
2464
+ } else {
2192
2465
  console.log('');
2193
2466
  console.log('Configuring default services...');
2194
2467
  console.log('');
@@ -2205,9 +2478,6 @@ async function quickSetup(selectedAgents, skipExternal) {
2205
2478
  console.log(' * Code Quality: ESLint (built-in)');
2206
2479
  console.log('');
2207
2480
  console.log('Configuration saved to .env.local');
2208
- } else {
2209
- console.log('');
2210
- console.log('Skipping external services configuration...');
2211
2481
  }
2212
2482
 
2213
2483
  // Final summary
@@ -2293,22 +2563,22 @@ async function interactiveSetupWithFlags(flags) {
2293
2563
  // Ask about overwriting AGENTS.md if it exists
2294
2564
  if (projectStatus.hasAgentsMd) {
2295
2565
  const overwriteAgents = await askYesNo(question, 'Found existing AGENTS.md. Overwrite?', true);
2296
- if (!overwriteAgents) {
2566
+ if (overwriteAgents) {
2567
+ console.log(' Will overwrite AGENTS.md');
2568
+ } else {
2297
2569
  skipFiles.agentsMd = true;
2298
2570
  console.log(' Keeping existing AGENTS.md');
2299
- } else {
2300
- console.log(' Will overwrite AGENTS.md');
2301
2571
  }
2302
2572
  }
2303
2573
 
2304
2574
  // Ask about overwriting .claude/commands/ if it exists
2305
2575
  if (projectStatus.hasClaudeCommands) {
2306
2576
  const overwriteCommands = await askYesNo(question, 'Found existing .claude/commands/. Overwrite?', true);
2307
- if (!overwriteCommands) {
2577
+ if (overwriteCommands) {
2578
+ console.log(' Will overwrite .claude/commands/');
2579
+ } else {
2308
2580
  skipFiles.claudeCommands = true;
2309
2581
  console.log(' Keeping existing .claude/commands/');
2310
- } else {
2311
- console.log(' Will overwrite .claude/commands/');
2312
2582
  }
2313
2583
  }
2314
2584
 
@@ -2350,7 +2620,7 @@ async function interactiveSetupWithFlags(flags) {
2350
2620
  if (answer.toLowerCase() === 'all') {
2351
2621
  selectedAgents = agentKeys;
2352
2622
  } else {
2353
- const nums = answer.split(/[\s,]+/).map(n => parseInt(n.trim())).filter(n => !isNaN(n));
2623
+ const nums = answer.split(/[\s,]+/).map(n => Number.parseInt(n.trim())).filter(n => !Number.isNaN(n));
2354
2624
 
2355
2625
  // Validate numbers are in range
2356
2626
  const validNums = nums.filter(n => n >= 1 && n <= agentKeys.length);
@@ -2388,23 +2658,19 @@ async function interactiveSetupWithFlags(flags) {
2388
2658
  if (merged) {
2389
2659
  fs.writeFileSync(agentsDest, merged, 'utf8');
2390
2660
  console.log(' Updated: AGENTS.md (preserved USER sections)');
2391
- } else {
2661
+ } else if (copyFile(agentsSrc, 'AGENTS.md')) {
2392
2662
  // No markers, do normal copy (user already approved overwrite)
2393
- if (copyFile(agentsSrc, 'AGENTS.md')) {
2394
- console.log(' Updated: AGENTS.md (universal standard)');
2395
- }
2663
+ console.log(' Updated: AGENTS.md (universal standard)');
2396
2664
  }
2397
- } else {
2665
+ } else if (copyFile(agentsSrc, 'AGENTS.md')) {
2398
2666
  // 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
- }
2667
+ console.log(' Created: AGENTS.md (universal standard)');
2668
+
2669
+ // Detect project type and update AGENTS.md
2670
+ const detection = detectProjectType();
2671
+ if (detection.hasPackageJson) {
2672
+ updateAgentsMdWithProjectType(detection);
2673
+ displayProjectType(detection);
2408
2674
  }
2409
2675
  }
2410
2676
  }
@@ -2454,15 +2720,15 @@ async function interactiveSetupWithFlags(flags) {
2454
2720
  // =============================================
2455
2721
  // STEP 2: External Services Configuration
2456
2722
  // =============================================
2457
- if (!flags.skipExternal) {
2723
+ if (flags.skipExternal) {
2724
+ console.log('');
2725
+ console.log('Skipping external services configuration...');
2726
+ } else {
2458
2727
  console.log('');
2459
2728
  console.log('STEP 2: External Services (Optional)');
2460
2729
  console.log('=====================================');
2461
2730
 
2462
2731
  await configureExternalServices(rl, question, selectedAgents, projectStatus);
2463
- } else {
2464
- console.log('');
2465
- console.log('Skipping external services configuration...');
2466
2732
  }
2467
2733
 
2468
2734
  setupCompleted = true;
@@ -2528,6 +2794,124 @@ async function interactiveSetupWithFlags(flags) {
2528
2794
  }
2529
2795
 
2530
2796
  // Main
2797
+ // Helper: Handle --path setup
2798
+ function handlePathSetup(targetPath) {
2799
+ const resolvedPath = path.resolve(targetPath);
2800
+
2801
+ // Create directory if it doesn't exist
2802
+ if (!fs.existsSync(resolvedPath)) {
2803
+ try {
2804
+ fs.mkdirSync(resolvedPath, { recursive: true });
2805
+ console.log(`Created directory: ${resolvedPath}`);
2806
+ } catch (err) {
2807
+ console.error(`Error creating directory: ${err.message}`);
2808
+ process.exit(1);
2809
+ }
2810
+ }
2811
+
2812
+ // Verify it's a directory
2813
+ if (!fs.statSync(resolvedPath).isDirectory()) {
2814
+ console.error(`Error: ${resolvedPath} is not a directory`);
2815
+ process.exit(1);
2816
+ }
2817
+
2818
+ // Change to target directory
2819
+ try {
2820
+ process.chdir(resolvedPath);
2821
+ console.log(`Working directory: ${resolvedPath}`);
2822
+ console.log('');
2823
+ } catch (err) {
2824
+ console.error(`Error changing to directory: ${err.message}`);
2825
+ process.exit(1);
2826
+ }
2827
+ }
2828
+
2829
+ // Helper: Determine selected agents from flags
2830
+ function determineSelectedAgents(flags) {
2831
+ if (flags.all) {
2832
+ return Object.keys(AGENTS);
2833
+ }
2834
+
2835
+ if (flags.agents) {
2836
+ const selectedAgents = validateAgents(flags.agents);
2837
+ if (selectedAgents.length === 0) {
2838
+ console.log('No valid agents specified.');
2839
+ console.log('Available agents:', Object.keys(AGENTS).join(', '));
2840
+ process.exit(1);
2841
+ }
2842
+ return selectedAgents;
2843
+ }
2844
+
2845
+ return [];
2846
+ }
2847
+
2848
+ // Helper: Handle setup command in non-quick mode
2849
+ async function handleSetupCommand(selectedAgents, flags) {
2850
+ showBanner('Installing for specified agents...');
2851
+ console.log('');
2852
+
2853
+ // Check prerequisites
2854
+ checkPrerequisites();
2855
+ console.log('');
2856
+
2857
+ // Copy AGENTS.md
2858
+ const agentsSrc = path.join(packageDir, 'AGENTS.md');
2859
+ if (copyFile(agentsSrc, 'AGENTS.md')) {
2860
+ console.log(' Created: AGENTS.md (universal standard)');
2861
+ }
2862
+ console.log('');
2863
+
2864
+ // Setup core documentation
2865
+ setupCoreDocs();
2866
+ console.log('');
2867
+
2868
+ // Load Claude commands if needed
2869
+ const claudeCommands = loadClaudeCommands(selectedAgents);
2870
+
2871
+ // Setup agents
2872
+ selectedAgents.forEach(agentKey => {
2873
+ if (agentKey !== 'claude') {
2874
+ setupAgent(agentKey, claudeCommands);
2875
+ }
2876
+ });
2877
+
2878
+ console.log('');
2879
+ console.log('Agent configuration complete!');
2880
+
2881
+ // External services (unless skipped)
2882
+ await handleExternalServices(flags.skipExternal, selectedAgents);
2883
+
2884
+ console.log('');
2885
+ console.log('Done! Get started with: /status');
2886
+ }
2887
+
2888
+ // Helper: Handle external services configuration
2889
+ async function handleExternalServices(skipExternal, selectedAgents) {
2890
+ if (skipExternal) {
2891
+ console.log('');
2892
+ console.log('Skipping external services configuration...');
2893
+ return;
2894
+ }
2895
+
2896
+ const rl = readline.createInterface({
2897
+ input: process.stdin,
2898
+ output: process.stdout
2899
+ });
2900
+
2901
+ let setupCompleted = false;
2902
+ rl.on('close', () => {
2903
+ if (!setupCompleted) {
2904
+ console.log('\n\nSetup cancelled.');
2905
+ process.exit(0);
2906
+ }
2907
+ });
2908
+
2909
+ const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
2910
+ await configureExternalServices(rl, question, selectedAgents);
2911
+ setupCompleted = true;
2912
+ rl.close();
2913
+ }
2914
+
2531
2915
  async function main() {
2532
2916
  const command = args[0];
2533
2917
  const flags = parseFlags();
@@ -2540,50 +2924,12 @@ async function main() {
2540
2924
 
2541
2925
  // Handle --path option: change to target directory
2542
2926
  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
- }
2927
+ handlePathSetup(flags.path);
2571
2928
  }
2572
2929
 
2573
2930
  if (command === 'setup') {
2574
2931
  // 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
- }
2932
+ let selectedAgents = determineSelectedAgents(flags);
2587
2933
 
2588
2934
  // Quick mode
2589
2935
  if (flags.quick) {
@@ -2597,74 +2943,7 @@ async function main() {
2597
2943
 
2598
2944
  // Agents specified via flag (non-quick mode)
2599
2945
  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');
2946
+ await handleSetupCommand(selectedAgents, flags);
2668
2947
  return;
2669
2948
  }
2670
2949
 
@@ -2738,13 +3017,9 @@ function validateRollbackInput(method, target) {
2738
3017
  }
2739
3018
 
2740
3019
  // Extract USER sections before rollback
2741
- function extractUserSections(filePath) {
2742
- if (!fs.existsSync(filePath)) return {};
2743
-
2744
- const content = fs.readFileSync(filePath, 'utf-8');
3020
+ // Helper: Extract USER:START/END marker sections from content
3021
+ function extractUserMarkerSections(content) {
2745
3022
  const sections = {};
2746
-
2747
- // Extract USER sections
2748
3023
  const userRegex = /<!-- USER:START -->([\s\S]*?)<!-- USER:END -->/g;
2749
3024
  let match;
2750
3025
  let index = 0;
@@ -2754,15 +3029,35 @@ function extractUserSections(filePath) {
2754
3029
  index++;
2755
3030
  }
2756
3031
 
2757
- // Extract custom commands
3032
+ return sections;
3033
+ }
3034
+
3035
+ // Helper: Extract custom commands from directory
3036
+ function extractCustomCommands(filePath) {
2758
3037
  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
- }));
3038
+
3039
+ if (!fs.existsSync(customCommandsDir)) {
3040
+ return null;
3041
+ }
3042
+
3043
+ return fs.readdirSync(customCommandsDir)
3044
+ .filter(f => f.endsWith('.md'))
3045
+ .map(f => ({
3046
+ name: f,
3047
+ content: fs.readFileSync(path.join(customCommandsDir, f), 'utf-8')
3048
+ }));
3049
+ }
3050
+
3051
+ function extractUserSections(filePath) {
3052
+ if (!fs.existsSync(filePath)) return {};
3053
+
3054
+ const content = fs.readFileSync(filePath, 'utf-8');
3055
+ const sections = extractUserMarkerSections(content);
3056
+
3057
+ // Extract custom commands
3058
+ const customCommands = extractCustomCommands(filePath);
3059
+ if (customCommands) {
3060
+ sections.customCommands = customCommands;
2766
3061
  }
2767
3062
 
2768
3063
  return sections;
@@ -2778,7 +3073,7 @@ function preserveUserSections(filePath, savedSections) {
2778
3073
 
2779
3074
  // Restore USER sections
2780
3075
  let index = 0;
2781
- content = content.replace(
3076
+ content = content.replaceAll(
2782
3077
  /<!-- USER:START -->[\s\S]*?<!-- USER:END -->/g,
2783
3078
  () => {
2784
3079
  const section = savedSections[`user_${index}`];
@@ -2807,6 +3102,110 @@ function preserveUserSections(filePath, savedSections) {
2807
3102
  }
2808
3103
 
2809
3104
  // Perform rollback operation
3105
+ // Helper: Check git working directory is clean
3106
+ function checkGitWorkingDirectory() {
3107
+ try {
3108
+ const { execSync } = require('node:child_process');
3109
+ const status = execSync('git status --porcelain', { encoding: 'utf-8' });
3110
+ if (status.trim() !== '') {
3111
+ console.log(chalk.red(' ❌ Working directory has uncommitted changes'));
3112
+ console.log(' Commit or stash changes before rollback');
3113
+ return false;
3114
+ }
3115
+ return true;
3116
+ } catch (err) {
3117
+ console.log(chalk.red(' ❌ Git error:'), err.message);
3118
+ return false;
3119
+ }
3120
+ }
3121
+
3122
+ // Helper: Update Beads issue after PR rollback
3123
+ function updateBeadsIssue(commitMessage) {
3124
+ const issueMatch = commitMessage.match(/#(\d+)/);
3125
+ if (!issueMatch) return;
3126
+
3127
+ try {
3128
+ const { execSync } = require('node:child_process');
3129
+ execSync(`bd update ${issueMatch[1]} --status reverted --comment "PR reverted"`, { stdio: 'inherit' });
3130
+ console.log(` Updated Beads issue #${issueMatch[1]} to 'reverted'`);
3131
+ } catch {
3132
+ // Beads not installed - silently continue
3133
+ }
3134
+ }
3135
+
3136
+ // Helper: Handle commit rollback
3137
+ function handleCommitRollback(target, dryRun, execSync) {
3138
+ if (dryRun) {
3139
+ console.log(` Would revert: ${target}`);
3140
+ const files = execSync(`git diff-tree --no-commit-id --name-only -r ${target}`, { encoding: 'utf-8' });
3141
+ console.log(' Affected files:');
3142
+ files.trim().split('\n').forEach(f => console.log(` - ${f}`));
3143
+ } else {
3144
+ execSync(`git revert --no-edit ${target}`, { stdio: 'inherit' });
3145
+ }
3146
+ }
3147
+
3148
+ // Helper: Handle PR rollback
3149
+ function handlePrRollback(target, dryRun, execSync) {
3150
+ if (dryRun) {
3151
+ console.log(` Would revert merge: ${target}`);
3152
+ const files = execSync(`git diff-tree --no-commit-id --name-only -r ${target}`, { encoding: 'utf-8' });
3153
+ console.log(' Affected files:');
3154
+ files.trim().split('\n').forEach(f => console.log(` - ${f}`));
3155
+ } else {
3156
+ execSync(`git revert -m 1 --no-edit ${target}`, { stdio: 'inherit' });
3157
+
3158
+ // Update Beads issue if linked
3159
+ const commitMsg = execSync(`git log -1 --format=%B ${target}`, { encoding: 'utf-8' });
3160
+ updateBeadsIssue(commitMsg);
3161
+ }
3162
+ }
3163
+
3164
+ // Helper: Handle partial file rollback
3165
+ function handlePartialRollback(target, dryRun, execSync) {
3166
+ const files = target.split(',').map(f => f.trim());
3167
+ if (dryRun) {
3168
+ console.log(' Would restore files:');
3169
+ files.forEach(f => console.log(` - ${f}`));
3170
+ } else {
3171
+ files.forEach(f => {
3172
+ execSync(`git checkout HEAD~1 -- "${f}"`, { stdio: 'inherit' });
3173
+ });
3174
+ execSync(`git commit -m "chore: rollback ${files.join(', ')}"`, { stdio: 'inherit' });
3175
+ }
3176
+ }
3177
+
3178
+ // Helper: Handle branch range rollback
3179
+ function handleBranchRollback(target, dryRun, execSync) {
3180
+ const [startCommit, endCommit] = target.split('..');
3181
+ if (dryRun) {
3182
+ console.log(` Would revert range: ${startCommit}..${endCommit}`);
3183
+ const commits = execSync(`git log --oneline ${startCommit}..${endCommit}`, { encoding: 'utf-8' });
3184
+ console.log(' Commits to revert:');
3185
+ commits.trim().split('\n').forEach(c => console.log(` ${c}`));
3186
+ } else {
3187
+ execSync(`git revert --no-edit ${startCommit}..${endCommit}`, { stdio: 'inherit' });
3188
+ }
3189
+ }
3190
+
3191
+ // Helper: Finalize rollback by restoring user sections
3192
+ function finalizeRollback(agentsPath, savedSections) {
3193
+ const { execSync } = require('node:child_process');
3194
+
3195
+ console.log(' 📦 Restoring user content...');
3196
+ preserveUserSections(agentsPath, savedSections);
3197
+
3198
+ // Amend commit to include restored USER sections
3199
+ if (fs.existsSync(agentsPath)) {
3200
+ execSync('git add AGENTS.md', { stdio: 'inherit' });
3201
+ execSync('git commit --amend --no-edit', { stdio: 'inherit' });
3202
+ }
3203
+
3204
+ console.log('');
3205
+ console.log(chalk.green(' ✅ Rollback complete'));
3206
+ console.log(' User content preserved');
3207
+ }
3208
+
2810
3209
  async function performRollback(method, target, dryRun = false) {
2811
3210
  console.log('');
2812
3211
  console.log(chalk.cyan(` 🔄 Rollback: ${method}`));
@@ -2824,16 +3223,7 @@ async function performRollback(method, target, dryRun = false) {
2824
3223
  }
2825
3224
 
2826
3225
  // 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);
3226
+ if (!checkGitWorkingDirectory()) {
2837
3227
  return false;
2838
3228
  }
2839
3229
 
@@ -2846,74 +3236,20 @@ async function performRollback(method, target, dryRun = false) {
2846
3236
  }
2847
3237
 
2848
3238
  try {
2849
- const { execSync } = require('child_process');
3239
+ const { execSync } = require('node:child_process');
2850
3240
 
2851
3241
  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
- }
3242
+ handleCommitRollback(target, dryRun, execSync);
2860
3243
  } 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
- }
3244
+ handlePrRollback(target, dryRun, execSync);
2881
3245
  } 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
- }
3246
+ handlePartialRollback(target, dryRun, execSync);
2892
3247
  } 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
- }
3248
+ handleBranchRollback(target, dryRun, execSync);
2902
3249
  }
2903
3250
 
2904
3251
  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');
3252
+ finalizeRollback(agentsPath, savedSections);
2917
3253
  }
2918
3254
 
2919
3255
  return true;
@@ -2952,33 +3288,33 @@ async function showRollbackMenu() {
2952
3288
  let method, target, dryRun = false;
2953
3289
 
2954
3290
  switch (choice.trim()) {
2955
- case '1':
3291
+ case '1': {
2956
3292
  method = 'commit';
2957
3293
  target = 'HEAD';
2958
3294
  break;
2959
-
2960
- case '2':
3295
+ }
3296
+ case '2': {
2961
3297
  target = await new Promise(resolve => {
2962
3298
  rl.question(' Enter commit hash: ', resolve);
2963
3299
  });
2964
3300
  method = 'commit';
2965
3301
  break;
2966
-
2967
- case '3':
3302
+ }
3303
+ case '3': {
2968
3304
  target = await new Promise(resolve => {
2969
3305
  rl.question(' Enter merge commit hash: ', resolve);
2970
3306
  });
2971
3307
  method = 'pr';
2972
3308
  break;
2973
-
2974
- case '4':
3309
+ }
3310
+ case '4': {
2975
3311
  target = await new Promise(resolve => {
2976
3312
  rl.question(' Enter file paths (comma-separated): ', resolve);
2977
3313
  });
2978
3314
  method = 'partial';
2979
3315
  break;
2980
-
2981
- case '5':
3316
+ }
3317
+ case '5': {
2982
3318
  const start = await new Promise(resolve => {
2983
3319
  rl.question(' Enter start commit: ', resolve);
2984
3320
  });
@@ -2988,8 +3324,8 @@ async function showRollbackMenu() {
2988
3324
  target = `${start.trim()}..${end.trim()}`;
2989
3325
  method = 'branch';
2990
3326
  break;
2991
-
2992
- case '6':
3327
+ }
3328
+ case '6': {
2993
3329
  dryRun = true;
2994
3330
  const dryMethod = await new Promise(resolve => {
2995
3331
  rl.question(' Preview method (commit/pr/partial/branch): ', resolve);
@@ -2999,11 +3335,12 @@ async function showRollbackMenu() {
2999
3335
  rl.question(' Enter target (commit/files/range): ', resolve);
3000
3336
  });
3001
3337
  break;
3002
-
3003
- default:
3338
+ }
3339
+ default: {
3004
3340
  console.log(chalk.red(' Invalid choice'));
3005
3341
  rl.close();
3006
3342
  return;
3343
+ }
3007
3344
  }
3008
3345
 
3009
3346
  rl.close();