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