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/.claude/skills/forge-workflow/SKILL.md +3 -2
- package/bin/forge.js +1162 -825
- package/docs/planning/PROGRESS.md +10 -0
- package/package.json +1 -1
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(
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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(
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
|
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
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
//
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
detection.
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
detection
|
|
781
|
-
|
|
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
|
-
//
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
-
|
|
1171
|
-
|
|
1172
|
-
console.log(' ✓ Keeping existing AGENTS.md');
|
|
1253
|
+
console.log(' ✓ Keeping AGENTS.md');
|
|
1254
|
+
return createInstructionFilesResult(false, false, false, false);
|
|
1173
1255
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1404
|
+
}
|
|
1405
|
+
case '3': {
|
|
1297
1406
|
const greptileKey = await question(' Enter Greptile API key: ');
|
|
1298
|
-
if (greptileKey
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1568
|
-
function
|
|
1569
|
-
|
|
1570
|
-
if (
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
-
//
|
|
1578
|
-
|
|
1579
|
-
|
|
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
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
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
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
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
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
}
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1724
|
+
console.log(' Created: .aider.conf.yml');
|
|
1725
|
+
return true; // Signal early return
|
|
1726
|
+
}
|
|
1620
1727
|
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1733
|
+
if (agent.needsConversion) {
|
|
1734
|
+
targetContent = stripFrontmatter(content);
|
|
1735
|
+
}
|
|
1630
1736
|
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1737
|
+
if (agent.promptFormat) {
|
|
1738
|
+
targetFile = cmd.replace('.md', '.prompt.md');
|
|
1739
|
+
targetContent = stripFrontmatter(content);
|
|
1740
|
+
}
|
|
1635
1741
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
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
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
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
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
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
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
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
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
-
//
|
|
1730
|
-
|
|
1731
|
-
const
|
|
1732
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1747
|
-
|
|
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
|
-
|
|
1865
|
+
// Handle agent-specific setup
|
|
1866
|
+
if (agentKey === 'claude') {
|
|
1867
|
+
setupClaudeAgent(skipFiles);
|
|
1868
|
+
}
|
|
1753
1869
|
|
|
1754
|
-
|
|
1870
|
+
if (agent.customSetup === 'cursor') {
|
|
1871
|
+
setupCursorAgent();
|
|
1872
|
+
}
|
|
1755
1873
|
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1874
|
+
if (agent.customSetup === 'aider') {
|
|
1875
|
+
const shouldReturn = setupAiderAgent();
|
|
1876
|
+
if (shouldReturn) return;
|
|
1877
|
+
}
|
|
1760
1878
|
|
|
1761
|
-
//
|
|
1762
|
-
|
|
1763
|
-
console.log('');
|
|
1879
|
+
// Convert/copy commands
|
|
1880
|
+
copyAgentCommands(agent, claudeCommands);
|
|
1764
1881
|
|
|
1765
|
-
//
|
|
1766
|
-
|
|
1767
|
-
// =============================================
|
|
1768
|
-
const projectStatus = detectProjectStatus();
|
|
1882
|
+
// Copy rules if needed
|
|
1883
|
+
copyAgentRules(agent);
|
|
1769
1884
|
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
console.log(' Existing Installation Detected');
|
|
1773
|
-
console.log('==============================================');
|
|
1774
|
-
console.log('');
|
|
1885
|
+
// Create SKILL.md
|
|
1886
|
+
createAgentSkill(agent);
|
|
1775
1887
|
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
}
|
|
1888
|
+
// Setup MCP configs
|
|
1889
|
+
if (agentKey === 'claude') {
|
|
1890
|
+
setupClaudeMcpConfig();
|
|
1891
|
+
}
|
|
1781
1892
|
|
|
1782
|
-
|
|
1783
|
-
|
|
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
|
-
//
|
|
1790
|
-
|
|
1791
|
-
|
|
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
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
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.
|
|
1818
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1843
|
-
|
|
1844
|
-
|
|
1982
|
+
// Handle "all" selection
|
|
1983
|
+
if (input.toLowerCase() === 'all') {
|
|
1984
|
+
return { valid: true, agents: agentKeys, message: null };
|
|
1985
|
+
}
|
|
1845
1986
|
|
|
1846
|
-
|
|
1847
|
-
|
|
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
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
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
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1994
|
+
if (invalidNums.length > 0) {
|
|
1995
|
+
console.log(` ⚠ Invalid numbers ignored: ${invalidNums.join(', ')} (valid: 1-${agentKeys.length})`);
|
|
1996
|
+
}
|
|
1860
1997
|
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
}
|
|
1998
|
+
// Deduplicate selected agents using Set
|
|
1999
|
+
const selectedAgents = [...new Set(validNums.map(n => agentKeys[n - 1]))].filter(Boolean);
|
|
1864
2000
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2001
|
+
if (selectedAgents.length === 0) {
|
|
2002
|
+
return { valid: false, agents: [], message: 'No valid agents selected. Please try again.' };
|
|
2003
|
+
}
|
|
1868
2004
|
|
|
1869
|
-
|
|
1870
|
-
|
|
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
|
-
|
|
1875
|
-
|
|
2028
|
+
return selectedAgents;
|
|
2029
|
+
}
|
|
1876
2030
|
|
|
1877
|
-
|
|
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
|
-
|
|
1881
|
-
|
|
1882
|
-
const agentsDest = path.join(projectRoot, 'AGENTS.md');
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
1883
2039
|
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
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
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
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
|
-
|
|
2067
|
+
}
|
|
1914
2068
|
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
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
|
-
|
|
1920
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
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;
|
|
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
|
|
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
|
-
|
|
2062
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2742
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|