forge-workflow 1.3.0 → 1.4.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/.claude/commands/check.md +8 -0
- package/.claude/commands/dev.md +8 -0
- package/.claude/commands/plan.md +8 -0
- package/.claude/commands/rollback.md +724 -0
- package/.claude/settings.json +7 -0
- package/.claude/settings.local.json +11 -1
- package/.mcp.json.example +12 -0
- package/AGENTS.md +87 -9
- package/CLAUDE.md +108 -0
- package/bin/forge.js +885 -2
- package/docs/TOOLCHAIN.md +92 -3
- package/docs/WORKFLOW.md +23 -0
- package/package.json +4 -2
package/bin/forge.js
CHANGED
|
@@ -133,7 +133,7 @@ const AGENTS = {
|
|
|
133
133
|
Object.freeze(AGENTS);
|
|
134
134
|
Object.values(AGENTS).forEach(agent => Object.freeze(agent));
|
|
135
135
|
|
|
136
|
-
const COMMANDS = ['status', 'research', 'plan', 'dev', 'check', 'ship', 'review', 'merge', 'verify'];
|
|
136
|
+
const COMMANDS = ['status', 'research', 'plan', 'dev', 'check', 'ship', 'review', 'merge', 'verify', 'rollback'];
|
|
137
137
|
|
|
138
138
|
// Code review tool options
|
|
139
139
|
const CODE_REVIEW_TOOLS = {
|
|
@@ -659,12 +659,34 @@ function detectProjectStatus() {
|
|
|
659
659
|
const status = {
|
|
660
660
|
type: 'fresh', // 'fresh', 'upgrade', or 'partial'
|
|
661
661
|
hasAgentsMd: fs.existsSync(path.join(projectRoot, 'AGENTS.md')),
|
|
662
|
+
hasClaudeMd: fs.existsSync(path.join(projectRoot, 'CLAUDE.md')),
|
|
662
663
|
hasClaudeCommands: fs.existsSync(path.join(projectRoot, '.claude/commands')),
|
|
663
664
|
hasEnvLocal: fs.existsSync(path.join(projectRoot, '.env.local')),
|
|
664
665
|
hasDocsWorkflow: fs.existsSync(path.join(projectRoot, 'docs/WORKFLOW.md')),
|
|
665
|
-
existingEnvVars: {}
|
|
666
|
+
existingEnvVars: {},
|
|
667
|
+
agentsMdSize: 0,
|
|
668
|
+
claudeMdSize: 0,
|
|
669
|
+
agentsMdLines: 0,
|
|
670
|
+
claudeMdLines: 0
|
|
666
671
|
};
|
|
667
672
|
|
|
673
|
+
// Get file sizes and line counts for context warnings
|
|
674
|
+
if (status.hasAgentsMd) {
|
|
675
|
+
const agentsPath = path.join(projectRoot, 'AGENTS.md');
|
|
676
|
+
const stats = fs.statSync(agentsPath);
|
|
677
|
+
const content = fs.readFileSync(agentsPath, 'utf8');
|
|
678
|
+
status.agentsMdSize = stats.size;
|
|
679
|
+
status.agentsMdLines = content.split('\n').length;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (status.hasClaudeMd) {
|
|
683
|
+
const claudePath = path.join(projectRoot, 'CLAUDE.md');
|
|
684
|
+
const stats = fs.statSync(claudePath);
|
|
685
|
+
const content = fs.readFileSync(claudePath, 'utf8');
|
|
686
|
+
status.claudeMdSize = stats.size;
|
|
687
|
+
status.claudeMdLines = content.split('\n').length;
|
|
688
|
+
}
|
|
689
|
+
|
|
668
690
|
// Determine installation type
|
|
669
691
|
if (status.hasAgentsMd && status.hasClaudeCommands && status.hasDocsWorkflow) {
|
|
670
692
|
status.type = 'upgrade'; // Full forge installation exists
|
|
@@ -681,6 +703,511 @@ function detectProjectStatus() {
|
|
|
681
703
|
return status;
|
|
682
704
|
}
|
|
683
705
|
|
|
706
|
+
// Detect project type from package.json
|
|
707
|
+
function detectProjectType() {
|
|
708
|
+
const detection = {
|
|
709
|
+
hasPackageJson: false,
|
|
710
|
+
framework: null,
|
|
711
|
+
frameworkConfidence: 0,
|
|
712
|
+
language: 'javascript',
|
|
713
|
+
languageConfidence: 100,
|
|
714
|
+
projectType: null,
|
|
715
|
+
buildTool: null,
|
|
716
|
+
testFramework: null,
|
|
717
|
+
features: {
|
|
718
|
+
typescript: false,
|
|
719
|
+
monorepo: false,
|
|
720
|
+
docker: false,
|
|
721
|
+
cicd: false
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
const pkg = readPackageJson();
|
|
726
|
+
if (!pkg) return detection;
|
|
727
|
+
|
|
728
|
+
detection.hasPackageJson = true;
|
|
729
|
+
|
|
730
|
+
// Detect TypeScript
|
|
731
|
+
if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) {
|
|
732
|
+
detection.features.typescript = true;
|
|
733
|
+
detection.language = 'typescript';
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Detect monorepo
|
|
737
|
+
if (pkg.workspaces || fs.existsSync(path.join(projectRoot, 'pnpm-workspace.yaml')) || fs.existsSync(path.join(projectRoot, 'lerna.json'))) {
|
|
738
|
+
detection.features.monorepo = true;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Detect Docker
|
|
742
|
+
if (fs.existsSync(path.join(projectRoot, 'Dockerfile')) || fs.existsSync(path.join(projectRoot, 'docker-compose.yml'))) {
|
|
743
|
+
detection.features.docker = true;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Detect CI/CD
|
|
747
|
+
if (fs.existsSync(path.join(projectRoot, '.github/workflows')) ||
|
|
748
|
+
fs.existsSync(path.join(projectRoot, '.gitlab-ci.yml')) ||
|
|
749
|
+
fs.existsSync(path.join(projectRoot, 'azure-pipelines.yml')) ||
|
|
750
|
+
fs.existsSync(path.join(projectRoot, '.circleci/config.yml'))) {
|
|
751
|
+
detection.features.cicd = true;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Framework detection with confidence scoring
|
|
755
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
756
|
+
|
|
757
|
+
// Helper function for test framework detection
|
|
758
|
+
const detectTestFramework = (deps) => {
|
|
759
|
+
if (deps.jest) return 'jest';
|
|
760
|
+
if (deps.vitest) return 'vitest';
|
|
761
|
+
if (deps.mocha) return 'mocha';
|
|
762
|
+
if (deps['@playwright/test']) return 'playwright';
|
|
763
|
+
if (deps.cypress) return 'cypress';
|
|
764
|
+
if (deps.karma) return 'karma';
|
|
765
|
+
return null;
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// Next.js (highest priority for React projects)
|
|
769
|
+
if (deps.next) {
|
|
770
|
+
detection.framework = 'Next.js';
|
|
771
|
+
detection.frameworkConfidence = 100;
|
|
772
|
+
detection.projectType = 'fullstack';
|
|
773
|
+
detection.buildTool = 'next';
|
|
774
|
+
detection.testFramework = detectTestFramework(deps);
|
|
775
|
+
return detection;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// NestJS (backend framework)
|
|
779
|
+
if (deps['@nestjs/core'] || deps['@nestjs/common']) {
|
|
780
|
+
detection.framework = 'NestJS';
|
|
781
|
+
detection.frameworkConfidence = 100;
|
|
782
|
+
detection.projectType = 'backend';
|
|
783
|
+
detection.buildTool = 'nest';
|
|
784
|
+
detection.testFramework = 'jest';
|
|
785
|
+
return detection;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Angular
|
|
789
|
+
if (deps['@angular/core'] || deps['@angular/cli']) {
|
|
790
|
+
detection.framework = 'Angular';
|
|
791
|
+
detection.frameworkConfidence = 100;
|
|
792
|
+
detection.projectType = 'frontend';
|
|
793
|
+
detection.buildTool = 'ng';
|
|
794
|
+
detection.testFramework = 'karma';
|
|
795
|
+
return detection;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Vue.js
|
|
799
|
+
if (deps.vue) {
|
|
800
|
+
if (deps.nuxt) {
|
|
801
|
+
detection.framework = 'Nuxt';
|
|
802
|
+
detection.frameworkConfidence = 100;
|
|
803
|
+
detection.projectType = 'fullstack';
|
|
804
|
+
detection.buildTool = 'nuxt';
|
|
805
|
+
} else {
|
|
806
|
+
detection.framework = 'Vue.js';
|
|
807
|
+
detection.frameworkConfidence = deps['@vue/cli'] ? 100 : 90;
|
|
808
|
+
detection.projectType = 'frontend';
|
|
809
|
+
detection.buildTool = deps.vite ? 'vite' : deps.webpack ? 'webpack' : 'vue-cli';
|
|
810
|
+
}
|
|
811
|
+
detection.testFramework = detectTestFramework(deps);
|
|
812
|
+
return detection;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// React (without Next.js)
|
|
816
|
+
if (deps.react) {
|
|
817
|
+
detection.framework = 'React';
|
|
818
|
+
detection.frameworkConfidence = 95;
|
|
819
|
+
detection.projectType = 'frontend';
|
|
820
|
+
detection.buildTool = deps.vite ? 'vite' : deps['react-scripts'] ? 'create-react-app' : 'webpack';
|
|
821
|
+
detection.testFramework = detectTestFramework(deps);
|
|
822
|
+
return detection;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Express (backend)
|
|
826
|
+
if (deps.express) {
|
|
827
|
+
detection.framework = 'Express';
|
|
828
|
+
detection.frameworkConfidence = 90;
|
|
829
|
+
detection.projectType = 'backend';
|
|
830
|
+
detection.buildTool = detection.features.typescript ? 'tsc' : 'node';
|
|
831
|
+
detection.testFramework = detectTestFramework(deps);
|
|
832
|
+
return detection;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Fastify (backend)
|
|
836
|
+
if (deps.fastify) {
|
|
837
|
+
detection.framework = 'Fastify';
|
|
838
|
+
detection.frameworkConfidence = 95;
|
|
839
|
+
detection.projectType = 'backend';
|
|
840
|
+
detection.buildTool = detection.features.typescript ? 'tsc' : 'node';
|
|
841
|
+
detection.testFramework = detectTestFramework(deps);
|
|
842
|
+
return detection;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Svelte
|
|
846
|
+
if (deps.svelte) {
|
|
847
|
+
if (deps['@sveltejs/kit']) {
|
|
848
|
+
detection.framework = 'SvelteKit';
|
|
849
|
+
detection.frameworkConfidence = 100;
|
|
850
|
+
detection.projectType = 'fullstack';
|
|
851
|
+
detection.buildTool = 'vite';
|
|
852
|
+
} else {
|
|
853
|
+
detection.framework = 'Svelte';
|
|
854
|
+
detection.frameworkConfidence = 95;
|
|
855
|
+
detection.projectType = 'frontend';
|
|
856
|
+
detection.buildTool = 'vite';
|
|
857
|
+
}
|
|
858
|
+
detection.testFramework = detectTestFramework(deps);
|
|
859
|
+
return detection;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Remix
|
|
863
|
+
if (deps['@remix-run/react']) {
|
|
864
|
+
detection.framework = 'Remix';
|
|
865
|
+
detection.frameworkConfidence = 100;
|
|
866
|
+
detection.projectType = 'fullstack';
|
|
867
|
+
detection.buildTool = 'remix';
|
|
868
|
+
detection.testFramework = detectTestFramework(deps);
|
|
869
|
+
return detection;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Astro
|
|
873
|
+
if (deps.astro) {
|
|
874
|
+
detection.framework = 'Astro';
|
|
875
|
+
detection.frameworkConfidence = 100;
|
|
876
|
+
detection.projectType = 'frontend';
|
|
877
|
+
detection.buildTool = 'astro';
|
|
878
|
+
detection.testFramework = detectTestFramework(deps);
|
|
879
|
+
return detection;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Generic Node.js project
|
|
883
|
+
if (pkg.main || pkg.scripts?.start) {
|
|
884
|
+
detection.framework = 'Node.js';
|
|
885
|
+
detection.frameworkConfidence = 70;
|
|
886
|
+
detection.projectType = 'backend';
|
|
887
|
+
detection.buildTool = detection.features.typescript ? 'tsc' : 'node';
|
|
888
|
+
detection.testFramework = detectTestFramework(deps);
|
|
889
|
+
return detection;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Fallback: generic JavaScript/TypeScript project
|
|
893
|
+
detection.framework = detection.features.typescript ? 'TypeScript' : 'JavaScript';
|
|
894
|
+
detection.frameworkConfidence = 60;
|
|
895
|
+
detection.projectType = 'library';
|
|
896
|
+
detection.buildTool = deps.vite ? 'vite' : deps.webpack ? 'webpack' : 'npm';
|
|
897
|
+
detection.testFramework = detectTestFramework(deps);
|
|
898
|
+
|
|
899
|
+
return detection;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Display project detection results
|
|
903
|
+
function displayProjectType(detection) {
|
|
904
|
+
if (!detection.hasPackageJson) return;
|
|
905
|
+
|
|
906
|
+
console.log('');
|
|
907
|
+
console.log(chalk.cyan(' 📦 Project Detection:'));
|
|
908
|
+
|
|
909
|
+
if (detection.framework) {
|
|
910
|
+
const confidence = detection.frameworkConfidence >= 90 ? '✓' : '~';
|
|
911
|
+
console.log(` Framework: ${chalk.bold(detection.framework)} ${confidence}`);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (detection.projectType) {
|
|
915
|
+
console.log(` Type: ${detection.projectType}`);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (detection.buildTool) {
|
|
919
|
+
console.log(` Build: ${detection.buildTool}`);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (detection.testFramework) {
|
|
923
|
+
console.log(` Tests: ${detection.testFramework}`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const features = [];
|
|
927
|
+
if (detection.features.typescript) features.push('TypeScript');
|
|
928
|
+
if (detection.features.monorepo) features.push('Monorepo');
|
|
929
|
+
if (detection.features.docker) features.push('Docker');
|
|
930
|
+
if (detection.features.cicd) features.push('CI/CD');
|
|
931
|
+
|
|
932
|
+
if (features.length > 0) {
|
|
933
|
+
console.log(` Features: ${features.join(', ')}`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Generate framework-specific tips
|
|
938
|
+
function generateFrameworkTips(detection) {
|
|
939
|
+
const tips = {
|
|
940
|
+
'Next.js': [
|
|
941
|
+
'- Use `npm run dev` for development with hot reload',
|
|
942
|
+
'- Server components are default in App Router',
|
|
943
|
+
'- API routes live in `app/api/` or `pages/api/`'
|
|
944
|
+
],
|
|
945
|
+
'React': [
|
|
946
|
+
'- Prefer functional components with hooks',
|
|
947
|
+
'- Use `React.memo()` for expensive components',
|
|
948
|
+
'- State management: Context API or external library'
|
|
949
|
+
],
|
|
950
|
+
'Vue.js': [
|
|
951
|
+
'- Use Composition API for better TypeScript support',
|
|
952
|
+
'- `<script setup>` is the recommended syntax',
|
|
953
|
+
'- Pinia is the official state management'
|
|
954
|
+
],
|
|
955
|
+
'Angular': [
|
|
956
|
+
'- Use standalone components (Angular 14+)',
|
|
957
|
+
'- Signals for reactive state (Angular 16+)',
|
|
958
|
+
'- RxJS for async operations'
|
|
959
|
+
],
|
|
960
|
+
'NestJS': [
|
|
961
|
+
'- Dependency injection via decorators',
|
|
962
|
+
'- Use `@nestjs/config` for environment variables',
|
|
963
|
+
'- Guards for authentication, Interceptors for logging'
|
|
964
|
+
],
|
|
965
|
+
'Express': [
|
|
966
|
+
'- Use middleware for cross-cutting concerns',
|
|
967
|
+
'- Error handling with next(err)',
|
|
968
|
+
'- Consider Helmet.js for security headers'
|
|
969
|
+
],
|
|
970
|
+
'Fastify': [
|
|
971
|
+
'- Schema-based validation with JSON Schema',
|
|
972
|
+
'- Plugins for reusable functionality',
|
|
973
|
+
'- Async/await by default'
|
|
974
|
+
],
|
|
975
|
+
'SvelteKit': [
|
|
976
|
+
'- File-based routing in `src/routes/`',
|
|
977
|
+
'- Server-side rendering by default',
|
|
978
|
+
'- Form actions for mutations'
|
|
979
|
+
],
|
|
980
|
+
'Nuxt': [
|
|
981
|
+
'- Auto-imports for components and composables',
|
|
982
|
+
'- `useAsyncData()` for data fetching',
|
|
983
|
+
'- Nitro server engine for deployment'
|
|
984
|
+
],
|
|
985
|
+
'Remix': [
|
|
986
|
+
'- Loaders for data fetching',
|
|
987
|
+
'- Actions for mutations',
|
|
988
|
+
'- Progressive enhancement by default'
|
|
989
|
+
],
|
|
990
|
+
'Astro': [
|
|
991
|
+
'- Zero JS by default',
|
|
992
|
+
'- Use client:* directives for interactivity',
|
|
993
|
+
'- Content collections for type-safe content'
|
|
994
|
+
]
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
return tips[detection.framework] || [];
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Update AGENTS.md with project type metadata
|
|
1001
|
+
function updateAgentsMdWithProjectType(detection) {
|
|
1002
|
+
const agentsPath = path.join(projectRoot, 'AGENTS.md');
|
|
1003
|
+
if (!fs.existsSync(agentsPath)) return;
|
|
1004
|
+
|
|
1005
|
+
let content = fs.readFileSync(agentsPath, 'utf-8');
|
|
1006
|
+
|
|
1007
|
+
// Find the project description line (line 3)
|
|
1008
|
+
const lines = content.split('\n');
|
|
1009
|
+
let insertIndex = -1;
|
|
1010
|
+
|
|
1011
|
+
for (let i = 0; i < Math.min(lines.length, 10); i++) {
|
|
1012
|
+
if (lines[i].startsWith('This is a ')) {
|
|
1013
|
+
insertIndex = i + 1;
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (insertIndex === -1) return;
|
|
1019
|
+
|
|
1020
|
+
// Build metadata section
|
|
1021
|
+
const metadata = [];
|
|
1022
|
+
metadata.push('');
|
|
1023
|
+
if (detection.framework) {
|
|
1024
|
+
metadata.push(`**Framework**: ${detection.framework}`);
|
|
1025
|
+
}
|
|
1026
|
+
if (detection.language && detection.language !== 'javascript') {
|
|
1027
|
+
metadata.push(`**Language**: ${detection.language}`);
|
|
1028
|
+
}
|
|
1029
|
+
if (detection.projectType) {
|
|
1030
|
+
metadata.push(`**Type**: ${detection.projectType}`);
|
|
1031
|
+
}
|
|
1032
|
+
if (detection.buildTool) {
|
|
1033
|
+
metadata.push(`**Build**: \`${detection.buildTool}\``);
|
|
1034
|
+
}
|
|
1035
|
+
if (detection.testFramework) {
|
|
1036
|
+
metadata.push(`**Tests**: ${detection.testFramework}`);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Add framework-specific tips
|
|
1040
|
+
const tips = generateFrameworkTips(detection);
|
|
1041
|
+
if (tips.length > 0) {
|
|
1042
|
+
metadata.push('');
|
|
1043
|
+
metadata.push('**Framework conventions**:');
|
|
1044
|
+
metadata.push(...tips);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Insert metadata
|
|
1048
|
+
lines.splice(insertIndex, 0, ...metadata);
|
|
1049
|
+
|
|
1050
|
+
fs.writeFileSync(agentsPath, lines.join('\n'), 'utf-8');
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Smart file selection with context warnings
|
|
1054
|
+
async function handleInstructionFiles(rl, question, selectedAgents, projectStatus) {
|
|
1055
|
+
const hasClaude = selectedAgents.some(a => a.key === 'claude');
|
|
1056
|
+
const hasOtherAgents = selectedAgents.some(a => a.key !== 'claude');
|
|
1057
|
+
|
|
1058
|
+
// Calculate estimated tokens (rough: ~4 chars per token)
|
|
1059
|
+
const estimateTokens = (bytes) => Math.ceil(bytes / 4);
|
|
1060
|
+
|
|
1061
|
+
const result = {
|
|
1062
|
+
createAgentsMd: false,
|
|
1063
|
+
createClaudeMd: false,
|
|
1064
|
+
skipAgentsMd: false,
|
|
1065
|
+
skipClaudeMd: false
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
// Scenario 1: Both files exist (potential context bloat)
|
|
1069
|
+
if (projectStatus.hasAgentsMd && projectStatus.hasClaudeMd) {
|
|
1070
|
+
const totalLines = projectStatus.agentsMdLines + projectStatus.claudeMdLines;
|
|
1071
|
+
const totalTokens = estimateTokens(projectStatus.agentsMdSize + projectStatus.claudeMdSize);
|
|
1072
|
+
|
|
1073
|
+
console.log('');
|
|
1074
|
+
console.log('⚠️ WARNING: Multiple Instruction Files Detected');
|
|
1075
|
+
console.log('='.repeat(60));
|
|
1076
|
+
console.log(` AGENTS.md: ${projectStatus.agentsMdLines} lines (~${estimateTokens(projectStatus.agentsMdSize)} tokens)`);
|
|
1077
|
+
console.log(` CLAUDE.md: ${projectStatus.claudeMdLines} lines (~${estimateTokens(projectStatus.claudeMdSize)} tokens)`);
|
|
1078
|
+
console.log(` Total: ${totalLines} lines (~${totalTokens} tokens)`);
|
|
1079
|
+
console.log('');
|
|
1080
|
+
console.log(' ⚠️ Claude Code reads BOTH files on every request');
|
|
1081
|
+
console.log(' ⚠️ This increases context usage and costs');
|
|
1082
|
+
console.log('');
|
|
1083
|
+
console.log(' Options:');
|
|
1084
|
+
console.log(' 1) Keep CLAUDE.md only (recommended for Claude Code only)');
|
|
1085
|
+
console.log(' 2) Keep AGENTS.md only (recommended for multi-agent users)');
|
|
1086
|
+
console.log(' 3) Keep both (higher context usage)');
|
|
1087
|
+
console.log('');
|
|
1088
|
+
|
|
1089
|
+
while (true) {
|
|
1090
|
+
const choice = await question('Your choice (1/2/3) [2]: ');
|
|
1091
|
+
const normalized = choice.trim() || '2';
|
|
1092
|
+
|
|
1093
|
+
if (normalized === '1') {
|
|
1094
|
+
result.skipAgentsMd = true;
|
|
1095
|
+
result.createClaudeMd = false; // Keep existing
|
|
1096
|
+
console.log(' ✓ Will keep CLAUDE.md, remove AGENTS.md');
|
|
1097
|
+
break;
|
|
1098
|
+
} else if (normalized === '2') {
|
|
1099
|
+
result.skipClaudeMd = true;
|
|
1100
|
+
result.createAgentsMd = false; // Keep existing
|
|
1101
|
+
console.log(' ✓ Will keep AGENTS.md, remove CLAUDE.md');
|
|
1102
|
+
break;
|
|
1103
|
+
} else if (normalized === '3') {
|
|
1104
|
+
result.createAgentsMd = false; // Keep existing
|
|
1105
|
+
result.createClaudeMd = false; // Keep existing
|
|
1106
|
+
console.log(' ✓ Will keep both files (context: ~' + totalTokens + ' tokens)');
|
|
1107
|
+
break;
|
|
1108
|
+
} else {
|
|
1109
|
+
console.log(' Please enter 1, 2, or 3');
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
return result;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Scenario 2: Only CLAUDE.md exists
|
|
1117
|
+
if (projectStatus.hasClaudeMd && !projectStatus.hasAgentsMd) {
|
|
1118
|
+
if (hasOtherAgents) {
|
|
1119
|
+
console.log('');
|
|
1120
|
+
console.log('📋 Found existing CLAUDE.md (' + projectStatus.claudeMdLines + ' lines)');
|
|
1121
|
+
console.log(' You selected multiple agents. Recommendation:');
|
|
1122
|
+
console.log(' → Migrate to AGENTS.md (works with all agents)');
|
|
1123
|
+
console.log('');
|
|
1124
|
+
|
|
1125
|
+
const migrate = await askYesNo(question, 'Migrate CLAUDE.md to AGENTS.md?', false);
|
|
1126
|
+
if (migrate) {
|
|
1127
|
+
result.createAgentsMd = true;
|
|
1128
|
+
result.skipClaudeMd = true;
|
|
1129
|
+
console.log(' ✓ Will migrate content to AGENTS.md');
|
|
1130
|
+
} else {
|
|
1131
|
+
result.createAgentsMd = true;
|
|
1132
|
+
result.createClaudeMd = false; // Keep existing
|
|
1133
|
+
console.log(' ✓ Will keep CLAUDE.md and create AGENTS.md');
|
|
1134
|
+
}
|
|
1135
|
+
} else {
|
|
1136
|
+
// Claude Code only - keep CLAUDE.md
|
|
1137
|
+
result.createClaudeMd = false; // Keep existing
|
|
1138
|
+
console.log(' ✓ Keeping existing CLAUDE.md');
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return result;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Scenario 3: Only AGENTS.md exists
|
|
1145
|
+
if (projectStatus.hasAgentsMd && !projectStatus.hasClaudeMd) {
|
|
1146
|
+
if (hasClaude && !hasOtherAgents) {
|
|
1147
|
+
console.log('');
|
|
1148
|
+
console.log('📋 Found existing AGENTS.md (' + projectStatus.agentsMdLines + ' lines)');
|
|
1149
|
+
console.log(' You selected Claude Code only. Options:');
|
|
1150
|
+
console.log(' 1) Keep AGENTS.md (works fine)');
|
|
1151
|
+
console.log(' 2) Rename to CLAUDE.md (Claude-specific naming)');
|
|
1152
|
+
console.log('');
|
|
1153
|
+
|
|
1154
|
+
const rename = await askYesNo(question, 'Rename to CLAUDE.md?', true);
|
|
1155
|
+
if (rename) {
|
|
1156
|
+
result.createClaudeMd = true;
|
|
1157
|
+
result.skipAgentsMd = true;
|
|
1158
|
+
console.log(' ✓ Will rename to CLAUDE.md');
|
|
1159
|
+
} else {
|
|
1160
|
+
result.createAgentsMd = false; // Keep existing
|
|
1161
|
+
console.log(' ✓ Keeping AGENTS.md');
|
|
1162
|
+
}
|
|
1163
|
+
} else {
|
|
1164
|
+
// Multi-agent or other agents - keep AGENTS.md
|
|
1165
|
+
result.createAgentsMd = false; // Keep existing
|
|
1166
|
+
console.log(' ✓ Keeping existing AGENTS.md');
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
return result;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Scenario 4: Neither file exists (fresh install)
|
|
1173
|
+
if (hasClaude && !hasOtherAgents) {
|
|
1174
|
+
// Claude Code only → create CLAUDE.md
|
|
1175
|
+
result.createClaudeMd = true;
|
|
1176
|
+
console.log(' ✓ Will create CLAUDE.md (Claude Code specific)');
|
|
1177
|
+
} else if (!hasClaude && hasOtherAgents) {
|
|
1178
|
+
// Other agents only → create AGENTS.md
|
|
1179
|
+
result.createAgentsMd = true;
|
|
1180
|
+
console.log(' ✓ Will create AGENTS.md (universal)');
|
|
1181
|
+
} else {
|
|
1182
|
+
// Multiple agents including Claude → create AGENTS.md + reference CLAUDE.md
|
|
1183
|
+
result.createAgentsMd = true;
|
|
1184
|
+
result.createClaudeMd = true; // Will be minimal reference
|
|
1185
|
+
console.log(' ✓ Will create AGENTS.md (main) + CLAUDE.md (reference)');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return result;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Create minimal CLAUDE.md that references AGENTS.md
|
|
1192
|
+
function createClaudeReference(destPath) {
|
|
1193
|
+
const content = `# Claude Code Instructions
|
|
1194
|
+
|
|
1195
|
+
See [AGENTS.md](AGENTS.md) for all project instructions.
|
|
1196
|
+
|
|
1197
|
+
This file exists to avoid Claude Code reading both CLAUDE.md and AGENTS.md (which doubles context usage). Keep project-level instructions in AGENTS.md.
|
|
1198
|
+
|
|
1199
|
+
---
|
|
1200
|
+
|
|
1201
|
+
<!-- Add Claude Code-specific instructions below (if needed) -->
|
|
1202
|
+
<!-- Examples: MCP server setup, custom commands, Claude-only workflows -->
|
|
1203
|
+
|
|
1204
|
+
💡 **Keep this minimal** - Main instructions are in AGENTS.md
|
|
1205
|
+
`;
|
|
1206
|
+
|
|
1207
|
+
fs.writeFileSync(destPath, content, 'utf8');
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
684
1211
|
// Configure external services interactively
|
|
685
1212
|
async function configureExternalServices(rl, question, selectedAgents = [], projectStatus = null) {
|
|
686
1213
|
console.log('');
|
|
@@ -972,6 +1499,13 @@ function minimalInstall() {
|
|
|
972
1499
|
const agentsSrc = path.join(packageDir, 'AGENTS.md');
|
|
973
1500
|
if (copyFile(agentsSrc, 'AGENTS.md')) {
|
|
974
1501
|
console.log(' Created: AGENTS.md (universal standard)');
|
|
1502
|
+
|
|
1503
|
+
// Detect project type and update AGENTS.md
|
|
1504
|
+
const detection = detectProjectType();
|
|
1505
|
+
if (detection.hasPackageJson) {
|
|
1506
|
+
updateAgentsMdWithProjectType(detection);
|
|
1507
|
+
displayProjectType(detection);
|
|
1508
|
+
}
|
|
975
1509
|
}
|
|
976
1510
|
}
|
|
977
1511
|
|
|
@@ -1353,6 +1887,13 @@ async function interactiveSetup() {
|
|
|
1353
1887
|
// New file
|
|
1354
1888
|
if (copyFile(agentsSrc, 'AGENTS.md')) {
|
|
1355
1889
|
console.log(' Created: AGENTS.md (universal standard)');
|
|
1890
|
+
|
|
1891
|
+
// Detect project type and update AGENTS.md
|
|
1892
|
+
const detection = detectProjectType();
|
|
1893
|
+
if (detection.hasPackageJson) {
|
|
1894
|
+
updateAgentsMdWithProjectType(detection);
|
|
1895
|
+
displayProjectType(detection);
|
|
1896
|
+
}
|
|
1356
1897
|
}
|
|
1357
1898
|
}
|
|
1358
1899
|
}
|
|
@@ -1834,6 +2375,13 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
1834
2375
|
// New file
|
|
1835
2376
|
if (copyFile(agentsSrc, 'AGENTS.md')) {
|
|
1836
2377
|
console.log(' Created: AGENTS.md (universal standard)');
|
|
2378
|
+
|
|
2379
|
+
// Detect project type and update AGENTS.md
|
|
2380
|
+
const detection = detectProjectType();
|
|
2381
|
+
if (detection.hasPackageJson) {
|
|
2382
|
+
updateAgentsMdWithProjectType(detection);
|
|
2383
|
+
displayProjectType(detection);
|
|
2384
|
+
}
|
|
1837
2385
|
}
|
|
1838
2386
|
}
|
|
1839
2387
|
}
|
|
@@ -2089,10 +2637,345 @@ async function main() {
|
|
|
2089
2637
|
|
|
2090
2638
|
// Interactive setup (skip-external still applies)
|
|
2091
2639
|
await interactiveSetupWithFlags(flags);
|
|
2640
|
+
} else if (command === 'rollback') {
|
|
2641
|
+
// Execute rollback menu
|
|
2642
|
+
await showRollbackMenu();
|
|
2092
2643
|
} else {
|
|
2093
2644
|
// Default: minimal install (postinstall behavior)
|
|
2094
2645
|
minimalInstall();
|
|
2095
2646
|
}
|
|
2096
2647
|
}
|
|
2097
2648
|
|
|
2649
|
+
// ============================================================================
|
|
2650
|
+
// ROLLBACK SYSTEM - TDD Validated
|
|
2651
|
+
// ============================================================================
|
|
2652
|
+
// Security: All inputs validated before use in git commands
|
|
2653
|
+
// See test/rollback-validation.test.js for validation test coverage
|
|
2654
|
+
|
|
2655
|
+
// Validate rollback inputs (security-critical)
|
|
2656
|
+
function validateRollbackInput(method, target) {
|
|
2657
|
+
const validMethods = ['commit', 'pr', 'partial', 'branch'];
|
|
2658
|
+
if (!validMethods.includes(method)) {
|
|
2659
|
+
return { valid: false, error: 'Invalid method' };
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
// Validate commit hash (git allows 4-40 char abbreviations)
|
|
2663
|
+
if (method === 'commit' || method === 'pr') {
|
|
2664
|
+
if (target !== 'HEAD' && !/^[0-9a-f]{4,40}$/i.test(target)) {
|
|
2665
|
+
return { valid: false, error: 'Invalid commit hash format' };
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
// Validate file paths
|
|
2670
|
+
if (method === 'partial') {
|
|
2671
|
+
const files = target.split(',').map(f => f.trim());
|
|
2672
|
+
for (const file of files) {
|
|
2673
|
+
// Reject shell metacharacters
|
|
2674
|
+
if (/[;|&$`()<>\r\n]/.test(file)) {
|
|
2675
|
+
return { valid: false, error: `Invalid characters in path: ${file}` };
|
|
2676
|
+
}
|
|
2677
|
+
// Reject URL-encoded path traversal attempts
|
|
2678
|
+
if (/%2[eE]|%2[fF]|%5[cC]/.test(file)) {
|
|
2679
|
+
return { valid: false, error: `URL-encoded characters not allowed: ${file}` };
|
|
2680
|
+
}
|
|
2681
|
+
// Reject non-ASCII/unicode characters
|
|
2682
|
+
if (!/^[\x20-\x7E]+$/.test(file)) {
|
|
2683
|
+
return { valid: false, error: `Only ASCII characters allowed in path: ${file}` };
|
|
2684
|
+
}
|
|
2685
|
+
// Prevent path traversal
|
|
2686
|
+
const resolved = path.resolve(projectRoot, file);
|
|
2687
|
+
if (!resolved.startsWith(projectRoot)) {
|
|
2688
|
+
return { valid: false, error: `Path outside project: ${file}` };
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
// Validate branch range
|
|
2694
|
+
if (method === 'branch') {
|
|
2695
|
+
if (!target.includes('..')) {
|
|
2696
|
+
return { valid: false, error: 'Branch range must use format: start..end' };
|
|
2697
|
+
}
|
|
2698
|
+
const [start, end] = target.split('..');
|
|
2699
|
+
if (!/^[0-9a-f]{4,40}$/i.test(start) || !/^[0-9a-f]{4,40}$/i.test(end)) {
|
|
2700
|
+
return { valid: false, error: 'Invalid commit hashes in range' };
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
return { valid: true };
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
// Extract USER sections before rollback
|
|
2708
|
+
function extractUserSections(filePath) {
|
|
2709
|
+
if (!fs.existsSync(filePath)) return {};
|
|
2710
|
+
|
|
2711
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
2712
|
+
const sections = {};
|
|
2713
|
+
|
|
2714
|
+
// Extract USER sections
|
|
2715
|
+
const userRegex = /<!-- USER:START -->([\s\S]*?)<!-- USER:END -->/g;
|
|
2716
|
+
let match;
|
|
2717
|
+
let index = 0;
|
|
2718
|
+
|
|
2719
|
+
while ((match = userRegex.exec(content)) !== null) {
|
|
2720
|
+
sections[`user_${index}`] = match[1];
|
|
2721
|
+
index++;
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
// Extract custom commands
|
|
2725
|
+
const customCommandsDir = path.join(path.dirname(filePath), '.claude', 'commands', 'custom');
|
|
2726
|
+
if (fs.existsSync(customCommandsDir)) {
|
|
2727
|
+
sections.customCommands = fs.readdirSync(customCommandsDir)
|
|
2728
|
+
.filter(f => f.endsWith('.md'))
|
|
2729
|
+
.map(f => ({
|
|
2730
|
+
name: f,
|
|
2731
|
+
content: fs.readFileSync(path.join(customCommandsDir, f), 'utf-8')
|
|
2732
|
+
}));
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
return sections;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
// Restore USER sections after rollback
|
|
2739
|
+
function preserveUserSections(filePath, savedSections) {
|
|
2740
|
+
if (!fs.existsSync(filePath) || Object.keys(savedSections).length === 0) {
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
2745
|
+
|
|
2746
|
+
// Restore USER sections
|
|
2747
|
+
let index = 0;
|
|
2748
|
+
content = content.replace(
|
|
2749
|
+
/<!-- USER:START -->[\s\S]*?<!-- USER:END -->/g,
|
|
2750
|
+
() => {
|
|
2751
|
+
const section = savedSections[`user_${index}`];
|
|
2752
|
+
index++;
|
|
2753
|
+
return section ? `<!-- USER:START -->${section}<!-- USER:END -->` : '';
|
|
2754
|
+
}
|
|
2755
|
+
);
|
|
2756
|
+
|
|
2757
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
2758
|
+
|
|
2759
|
+
// Restore custom commands
|
|
2760
|
+
if (savedSections.customCommands) {
|
|
2761
|
+
const customCommandsDir = path.join(path.dirname(filePath), '.claude', 'commands', 'custom');
|
|
2762
|
+
if (!fs.existsSync(customCommandsDir)) {
|
|
2763
|
+
fs.mkdirSync(customCommandsDir, { recursive: true });
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
savedSections.customCommands.forEach(cmd => {
|
|
2767
|
+
fs.writeFileSync(
|
|
2768
|
+
path.join(customCommandsDir, cmd.name),
|
|
2769
|
+
cmd.content,
|
|
2770
|
+
'utf-8'
|
|
2771
|
+
);
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
// Perform rollback operation
|
|
2777
|
+
async function performRollback(method, target, dryRun = false) {
|
|
2778
|
+
console.log('');
|
|
2779
|
+
console.log(chalk.cyan(` 🔄 Rollback: ${method}`));
|
|
2780
|
+
console.log(` Target: ${target}`);
|
|
2781
|
+
if (dryRun) {
|
|
2782
|
+
console.log(chalk.yellow(' Mode: DRY RUN (preview only)'));
|
|
2783
|
+
}
|
|
2784
|
+
console.log('');
|
|
2785
|
+
|
|
2786
|
+
// Validate inputs BEFORE any git operations
|
|
2787
|
+
const validation = validateRollbackInput(method, target);
|
|
2788
|
+
if (!validation.valid) {
|
|
2789
|
+
console.log(chalk.red(` ❌ ${validation.error}`));
|
|
2790
|
+
return false;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
// Check for clean working directory
|
|
2794
|
+
try {
|
|
2795
|
+
const { execSync } = require('child_process');
|
|
2796
|
+
const status = execSync('git status --porcelain', { encoding: 'utf-8' });
|
|
2797
|
+
if (status.trim() !== '') {
|
|
2798
|
+
console.log(chalk.red(' ❌ Working directory has uncommitted changes'));
|
|
2799
|
+
console.log(' Commit or stash changes before rollback');
|
|
2800
|
+
return false;
|
|
2801
|
+
}
|
|
2802
|
+
} catch (err) {
|
|
2803
|
+
console.log(chalk.red(' ❌ Git error:'), err.message);
|
|
2804
|
+
return false;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// Extract USER sections before rollback
|
|
2808
|
+
const agentsPath = path.join(projectRoot, 'AGENTS.md');
|
|
2809
|
+
const savedSections = extractUserSections(agentsPath);
|
|
2810
|
+
|
|
2811
|
+
if (!dryRun) {
|
|
2812
|
+
console.log(' 📦 Backing up user content...');
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
try {
|
|
2816
|
+
const { execSync } = require('child_process');
|
|
2817
|
+
|
|
2818
|
+
if (method === 'commit') {
|
|
2819
|
+
if (dryRun) {
|
|
2820
|
+
console.log(` Would revert: ${target}`);
|
|
2821
|
+
const files = execSync(`git diff-tree --no-commit-id --name-only -r ${target}`, { encoding: 'utf-8' });
|
|
2822
|
+
console.log(' Affected files:');
|
|
2823
|
+
files.trim().split('\n').forEach(f => console.log(` - ${f}`));
|
|
2824
|
+
} else {
|
|
2825
|
+
execSync(`git revert --no-edit ${target}`, { stdio: 'inherit' });
|
|
2826
|
+
}
|
|
2827
|
+
} else if (method === 'pr') {
|
|
2828
|
+
if (dryRun) {
|
|
2829
|
+
console.log(` Would revert merge: ${target}`);
|
|
2830
|
+
const files = execSync(`git diff-tree --no-commit-id --name-only -r ${target}`, { encoding: 'utf-8' });
|
|
2831
|
+
console.log(' Affected files:');
|
|
2832
|
+
files.trim().split('\n').forEach(f => console.log(` - ${f}`));
|
|
2833
|
+
} else {
|
|
2834
|
+
execSync(`git revert -m 1 --no-edit ${target}`, { stdio: 'inherit' });
|
|
2835
|
+
|
|
2836
|
+
// Update Beads issue if linked
|
|
2837
|
+
const commitMsg = execSync(`git log -1 --format=%B ${target}`, { encoding: 'utf-8' });
|
|
2838
|
+
const issueMatch = commitMsg.match(/#(\d+)/);
|
|
2839
|
+
if (issueMatch) {
|
|
2840
|
+
try {
|
|
2841
|
+
execSync(`bd update ${issueMatch[1]} --status reverted --comment "PR reverted"`, { stdio: 'inherit' });
|
|
2842
|
+
console.log(` Updated Beads issue #${issueMatch[1]} to 'reverted'`);
|
|
2843
|
+
} catch {
|
|
2844
|
+
// Beads not installed - silently continue
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
} else if (method === 'partial') {
|
|
2849
|
+
const files = target.split(',').map(f => f.trim());
|
|
2850
|
+
if (dryRun) {
|
|
2851
|
+
console.log(' Would restore files:');
|
|
2852
|
+
files.forEach(f => console.log(` - ${f}`));
|
|
2853
|
+
} else {
|
|
2854
|
+
files.forEach(f => {
|
|
2855
|
+
execSync(`git checkout HEAD~1 -- "${f}"`, { stdio: 'inherit' });
|
|
2856
|
+
});
|
|
2857
|
+
execSync(`git commit -m "chore: rollback ${files.join(', ')}"`, { stdio: 'inherit' });
|
|
2858
|
+
}
|
|
2859
|
+
} else if (method === 'branch') {
|
|
2860
|
+
const [startCommit, endCommit] = target.split('..');
|
|
2861
|
+
if (dryRun) {
|
|
2862
|
+
console.log(` Would revert range: ${startCommit}..${endCommit}`);
|
|
2863
|
+
const commits = execSync(`git log --oneline ${startCommit}..${endCommit}`, { encoding: 'utf-8' });
|
|
2864
|
+
console.log(' Commits to revert:');
|
|
2865
|
+
commits.trim().split('\n').forEach(c => console.log(` ${c}`));
|
|
2866
|
+
} else {
|
|
2867
|
+
execSync(`git revert --no-edit ${startCommit}..${endCommit}`, { stdio: 'inherit' });
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
if (!dryRun) {
|
|
2872
|
+
console.log(' 📦 Restoring user content...');
|
|
2873
|
+
preserveUserSections(agentsPath, savedSections);
|
|
2874
|
+
|
|
2875
|
+
// Amend commit to include restored USER sections
|
|
2876
|
+
if (fs.existsSync(agentsPath)) {
|
|
2877
|
+
execSync('git add AGENTS.md', { stdio: 'inherit' });
|
|
2878
|
+
execSync('git commit --amend --no-edit', { stdio: 'inherit' });
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
console.log('');
|
|
2882
|
+
console.log(chalk.green(' ✅ Rollback complete'));
|
|
2883
|
+
console.log(' User content preserved');
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
return true;
|
|
2887
|
+
} catch (err) {
|
|
2888
|
+
console.log('');
|
|
2889
|
+
console.log(chalk.red(' ❌ Rollback failed:'), err.message);
|
|
2890
|
+
console.log(' Try manual rollback with: git revert <commit>');
|
|
2891
|
+
return false;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
// Interactive rollback menu
|
|
2896
|
+
async function showRollbackMenu() {
|
|
2897
|
+
console.log('');
|
|
2898
|
+
console.log(chalk.cyan.bold(' 🔄 Forge Rollback'));
|
|
2899
|
+
console.log('');
|
|
2900
|
+
console.log(' Choose rollback method:');
|
|
2901
|
+
console.log('');
|
|
2902
|
+
console.log(' 1. Rollback last commit');
|
|
2903
|
+
console.log(' 2. Rollback specific commit');
|
|
2904
|
+
console.log(' 3. Rollback merged PR');
|
|
2905
|
+
console.log(' 4. Rollback specific files only');
|
|
2906
|
+
console.log(' 5. Rollback entire branch');
|
|
2907
|
+
console.log(' 6. Preview rollback (dry run)');
|
|
2908
|
+
console.log('');
|
|
2909
|
+
|
|
2910
|
+
const rl = readline.createInterface({
|
|
2911
|
+
input: process.stdin,
|
|
2912
|
+
output: process.stdout
|
|
2913
|
+
});
|
|
2914
|
+
|
|
2915
|
+
const choice = await new Promise(resolve => {
|
|
2916
|
+
rl.question(' Enter choice (1-6): ', resolve);
|
|
2917
|
+
});
|
|
2918
|
+
|
|
2919
|
+
let method, target, dryRun = false;
|
|
2920
|
+
|
|
2921
|
+
switch (choice.trim()) {
|
|
2922
|
+
case '1':
|
|
2923
|
+
method = 'commit';
|
|
2924
|
+
target = 'HEAD';
|
|
2925
|
+
break;
|
|
2926
|
+
|
|
2927
|
+
case '2':
|
|
2928
|
+
target = await new Promise(resolve => {
|
|
2929
|
+
rl.question(' Enter commit hash: ', resolve);
|
|
2930
|
+
});
|
|
2931
|
+
method = 'commit';
|
|
2932
|
+
break;
|
|
2933
|
+
|
|
2934
|
+
case '3':
|
|
2935
|
+
target = await new Promise(resolve => {
|
|
2936
|
+
rl.question(' Enter merge commit hash: ', resolve);
|
|
2937
|
+
});
|
|
2938
|
+
method = 'pr';
|
|
2939
|
+
break;
|
|
2940
|
+
|
|
2941
|
+
case '4':
|
|
2942
|
+
target = await new Promise(resolve => {
|
|
2943
|
+
rl.question(' Enter file paths (comma-separated): ', resolve);
|
|
2944
|
+
});
|
|
2945
|
+
method = 'partial';
|
|
2946
|
+
break;
|
|
2947
|
+
|
|
2948
|
+
case '5':
|
|
2949
|
+
const start = await new Promise(resolve => {
|
|
2950
|
+
rl.question(' Enter start commit: ', resolve);
|
|
2951
|
+
});
|
|
2952
|
+
const end = await new Promise(resolve => {
|
|
2953
|
+
rl.question(' Enter end commit: ', resolve);
|
|
2954
|
+
});
|
|
2955
|
+
target = `${start.trim()}..${end.trim()}`;
|
|
2956
|
+
method = 'branch';
|
|
2957
|
+
break;
|
|
2958
|
+
|
|
2959
|
+
case '6':
|
|
2960
|
+
dryRun = true;
|
|
2961
|
+
const dryMethod = await new Promise(resolve => {
|
|
2962
|
+
rl.question(' Preview method (commit/pr/partial/branch): ', resolve);
|
|
2963
|
+
});
|
|
2964
|
+
method = dryMethod.trim();
|
|
2965
|
+
target = await new Promise(resolve => {
|
|
2966
|
+
rl.question(' Enter target (commit/files/range): ', resolve);
|
|
2967
|
+
});
|
|
2968
|
+
break;
|
|
2969
|
+
|
|
2970
|
+
default:
|
|
2971
|
+
console.log(chalk.red(' Invalid choice'));
|
|
2972
|
+
rl.close();
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
rl.close();
|
|
2977
|
+
|
|
2978
|
+
await performRollback(method, target, dryRun);
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2098
2981
|
main().catch(console.error);
|