@litmers/cursorflow-orchestrator 0.1.29 → 0.1.30
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/CHANGELOG.md +17 -0
- package/dist/cli/clean.js +122 -0
- package/dist/cli/clean.js.map +1 -1
- package/dist/core/auto-recovery.js +3 -1
- package/dist/core/auto-recovery.js.map +1 -1
- package/dist/core/failure-policy.js +7 -1
- package/dist/core/failure-policy.js.map +1 -1
- package/dist/core/orchestrator.js +452 -366
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/utils/config.js +3 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +5 -1
- package/dist/utils/enhanced-logger.js +65 -20
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +25 -0
- package/dist/utils/git.js +97 -2
- package/dist/utils/git.js.map +1 -1
- package/package.json +11 -3
- package/scripts/local-security-gate.sh +37 -7
- package/scripts/release.sh +15 -0
- package/src/cli/clean.ts +146 -0
- package/src/core/auto-recovery.ts +3 -1
- package/src/core/failure-policy.ts +8 -1
- package/src/core/orchestrator.ts +175 -82
- package/src/utils/config.ts +3 -1
- package/src/utils/enhanced-logger.ts +61 -20
- package/src/utils/git.ts +115 -2
package/src/core/orchestrator.ts
CHANGED
|
@@ -86,6 +86,26 @@ interface RunningLaneInfo {
|
|
|
86
86
|
continueSignalsSent: number; // Number of continue signals sent
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Log the tail of a file
|
|
91
|
+
*/
|
|
92
|
+
function logFileTail(filePath: string, lines: number = 10): void {
|
|
93
|
+
if (!fs.existsSync(filePath)) return;
|
|
94
|
+
try {
|
|
95
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
96
|
+
const allLines = content.split('\n');
|
|
97
|
+
const tail = allLines.slice(-lines).filter(l => l.trim());
|
|
98
|
+
if (tail.length > 0) {
|
|
99
|
+
logger.error(` Last ${tail.length} lines of log:`);
|
|
100
|
+
for (const line of tail) {
|
|
101
|
+
logger.error(` ${line}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// Ignore log reading errors
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
89
109
|
/**
|
|
90
110
|
* Spawn a lane process
|
|
91
111
|
*/
|
|
@@ -651,7 +671,28 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
651
671
|
printLaneStatus(lanes, laneRunDirs);
|
|
652
672
|
}, options.pollInterval || 60000);
|
|
653
673
|
|
|
654
|
-
|
|
674
|
+
// Handle process interruption
|
|
675
|
+
const sigIntHandler = () => {
|
|
676
|
+
logger.warn('\n⚠️ Orchestration interrupted! Stopping all lanes...');
|
|
677
|
+
for (const [name, info] of running.entries()) {
|
|
678
|
+
logger.info(`Stopping lane: ${name}`);
|
|
679
|
+
try {
|
|
680
|
+
info.child.kill('SIGTERM');
|
|
681
|
+
} catch {
|
|
682
|
+
// Ignore kill errors
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
printLaneStatus(lanes, laneRunDirs);
|
|
686
|
+
process.exit(130);
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
process.on('SIGINT', sigIntHandler);
|
|
690
|
+
process.on('SIGTERM', sigIntHandler);
|
|
691
|
+
|
|
692
|
+
let lastStallCheck = Date.now();
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
while (completedLanes.size + failedLanes.size + blockedLanes.size < lanes.length || (blockedLanes.size > 0 && running.size === 0)) {
|
|
655
696
|
// 1. Identify lanes ready to start
|
|
656
697
|
const readyToStart = lanes.filter(lane => {
|
|
657
698
|
// Not already running or completed or failed or blocked
|
|
@@ -692,6 +733,23 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
692
733
|
|
|
693
734
|
logger.info(`Lane started: ${lane.name}${lane.startIndex ? ` (resuming from ${lane.startIndex})` : ''}`);
|
|
694
735
|
|
|
736
|
+
const now = Date.now();
|
|
737
|
+
// Pre-register lane in running map so onActivity can find it immediately
|
|
738
|
+
running.set(lane.name, {
|
|
739
|
+
child: {} as any, // Placeholder, will be replaced below
|
|
740
|
+
logManager: undefined,
|
|
741
|
+
logPath: '',
|
|
742
|
+
lastActivity: now,
|
|
743
|
+
lastStateUpdate: now,
|
|
744
|
+
stallPhase: 0,
|
|
745
|
+
taskStartTime: now,
|
|
746
|
+
lastOutput: '',
|
|
747
|
+
statePath: laneStatePath,
|
|
748
|
+
bytesReceived: 0,
|
|
749
|
+
lastBytesCheck: 0,
|
|
750
|
+
continueSignalsSent: 0,
|
|
751
|
+
});
|
|
752
|
+
|
|
695
753
|
let lastOutput = '';
|
|
696
754
|
const spawnResult = spawnLane({
|
|
697
755
|
laneName: lane.name,
|
|
@@ -706,44 +764,45 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
706
764
|
onActivity: () => {
|
|
707
765
|
const info = running.get(lane.name);
|
|
708
766
|
if (info) {
|
|
709
|
-
const
|
|
710
|
-
info.lastActivity =
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
info.lastStateUpdate = now;
|
|
714
|
-
info.stallPhase = 0; // Reset stall phase since agent is responding
|
|
767
|
+
const actNow = Date.now();
|
|
768
|
+
info.lastActivity = actNow;
|
|
769
|
+
info.lastStateUpdate = actNow;
|
|
770
|
+
info.stallPhase = 0;
|
|
715
771
|
}
|
|
716
772
|
}
|
|
717
773
|
});
|
|
718
774
|
|
|
775
|
+
// Update with actual spawn result
|
|
776
|
+
const existingInfo = running.get(lane.name)!;
|
|
777
|
+
Object.assign(existingInfo, spawnResult);
|
|
778
|
+
|
|
719
779
|
// Track last output and bytes received for long operation and stall detection
|
|
720
780
|
if (spawnResult.child.stdout) {
|
|
721
781
|
spawnResult.child.stdout.on('data', (data: Buffer) => {
|
|
722
782
|
const info = running.get(lane.name);
|
|
723
783
|
if (info) {
|
|
724
|
-
|
|
725
|
-
|
|
784
|
+
const output = data.toString();
|
|
785
|
+
const lines = output.split('\n').filter(l => l.trim());
|
|
786
|
+
|
|
787
|
+
// Filter out heartbeats from activity tracking to avoid resetting stall detection
|
|
788
|
+
const realLines = lines.filter(line => !(line.includes('Heartbeat') && line.includes('bytes received')));
|
|
726
789
|
|
|
727
|
-
|
|
728
|
-
|
|
790
|
+
if (realLines.length > 0) {
|
|
791
|
+
// Real activity detected
|
|
792
|
+
const lastRealLine = realLines[realLines.length - 1]!;
|
|
793
|
+
info.lastOutput = lastRealLine;
|
|
794
|
+
info.bytesReceived += data.length;
|
|
795
|
+
|
|
796
|
+
// Update auto-recovery manager with real activity
|
|
797
|
+
autoRecoveryManager.recordActivity(lane.name, data.length, info.lastOutput);
|
|
798
|
+
} else if (lines.length > 0) {
|
|
799
|
+
// Only heartbeats received - update auto-recovery manager with 0 bytes to avoid resetting idle timer
|
|
800
|
+
autoRecoveryManager.recordActivity(lane.name, 0, info.lastOutput);
|
|
801
|
+
}
|
|
729
802
|
}
|
|
730
803
|
});
|
|
731
804
|
}
|
|
732
805
|
|
|
733
|
-
const now = Date.now();
|
|
734
|
-
running.set(lane.name, {
|
|
735
|
-
...spawnResult,
|
|
736
|
-
lastActivity: now,
|
|
737
|
-
lastStateUpdate: now,
|
|
738
|
-
stallPhase: 0,
|
|
739
|
-
taskStartTime: now,
|
|
740
|
-
lastOutput: '',
|
|
741
|
-
statePath: laneStatePath,
|
|
742
|
-
bytesReceived: 0,
|
|
743
|
-
lastBytesCheck: 0,
|
|
744
|
-
continueSignalsSent: 0,
|
|
745
|
-
});
|
|
746
|
-
|
|
747
806
|
// Register lane with auto-recovery manager
|
|
748
807
|
autoRecoveryManager.registerLane(lane.name);
|
|
749
808
|
|
|
@@ -773,13 +832,19 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
773
832
|
const result = await Promise.race([...promises, pollPromise]);
|
|
774
833
|
if (pollTimeout) clearTimeout(pollTimeout);
|
|
775
834
|
|
|
776
|
-
|
|
835
|
+
const now = Date.now();
|
|
836
|
+
if (result.name === '__poll__' || (now - lastStallCheck >= 10000)) {
|
|
837
|
+
lastStallCheck = now;
|
|
838
|
+
|
|
777
839
|
// Periodic stall check with multi-layer detection and escalating recovery
|
|
778
840
|
for (const [laneName, info] of running.entries()) {
|
|
779
|
-
const now = Date.now();
|
|
780
841
|
const idleTime = now - info.lastActivity;
|
|
781
842
|
const lane = lanes.find(l => l.name === laneName)!;
|
|
782
843
|
|
|
844
|
+
if (process.env['DEBUG_STALL']) {
|
|
845
|
+
logger.debug(`[${laneName}] Stall check: idle=${Math.round(idleTime/1000)}s, bytesDelta=${info.bytesReceived - info.lastBytesCheck}, phase=${info.stallPhase}`);
|
|
846
|
+
}
|
|
847
|
+
|
|
783
848
|
// Check state file for progress updates
|
|
784
849
|
let progressTime = 0;
|
|
785
850
|
try {
|
|
@@ -1002,74 +1067,98 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
1002
1067
|
}
|
|
1003
1068
|
}
|
|
1004
1069
|
continue;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
exitCodes[finished.name] = finished.code;
|
|
1011
|
-
|
|
1012
|
-
// Unregister from auto-recovery manager
|
|
1013
|
-
autoRecoveryManager.unregisterLane(finished.name);
|
|
1014
|
-
|
|
1015
|
-
if (finished.code === 0) {
|
|
1016
|
-
completedLanes.add(finished.name);
|
|
1017
|
-
events.emit('lane.completed', {
|
|
1018
|
-
laneName: finished.name,
|
|
1019
|
-
exitCode: finished.code,
|
|
1020
|
-
});
|
|
1021
|
-
} else if (finished.code === 2) {
|
|
1022
|
-
// Blocked by dependency
|
|
1023
|
-
const statePath = safeJoin(laneRunDirs[finished.name]!, 'state.json');
|
|
1024
|
-
const state = loadState<LaneState>(statePath);
|
|
1070
|
+
} else {
|
|
1071
|
+
const finished = result;
|
|
1072
|
+
const info = running.get(finished.name)!;
|
|
1073
|
+
running.delete(finished.name);
|
|
1074
|
+
exitCodes[finished.name] = finished.code;
|
|
1025
1075
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
events.emit('lane.blocked', {
|
|
1076
|
+
// Unregister from auto-recovery manager
|
|
1077
|
+
autoRecoveryManager.unregisterLane(finished.name);
|
|
1078
|
+
|
|
1079
|
+
if (finished.code === 0) {
|
|
1080
|
+
completedLanes.add(finished.name);
|
|
1081
|
+
events.emit('lane.completed', {
|
|
1034
1082
|
laneName: finished.name,
|
|
1035
|
-
|
|
1083
|
+
exitCode: finished.code,
|
|
1036
1084
|
});
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
failedLanes.add(finished.name);
|
|
1040
|
-
logger.error(`Lane ${finished.name} exited with code 2 but no dependency request found`);
|
|
1041
|
-
}
|
|
1042
|
-
} else {
|
|
1043
|
-
// Check if it was a restart request
|
|
1044
|
-
if (info.stallPhase === 2) {
|
|
1045
|
-
logger.info(`🔄 Lane ${finished.name} is being restarted due to stall...`);
|
|
1046
|
-
|
|
1047
|
-
// Update startIndex from current state to resume from the same task
|
|
1085
|
+
} else if (finished.code === 2) {
|
|
1086
|
+
// Blocked by dependency
|
|
1048
1087
|
const statePath = safeJoin(laneRunDirs[finished.name]!, 'state.json');
|
|
1049
1088
|
const state = loadState<LaneState>(statePath);
|
|
1050
|
-
|
|
1089
|
+
|
|
1090
|
+
if (state && state.dependencyRequest) {
|
|
1091
|
+
blockedLanes.set(finished.name, state.dependencyRequest);
|
|
1051
1092
|
const lane = lanes.find(l => l.name === finished.name);
|
|
1052
1093
|
if (lane) {
|
|
1053
|
-
lane.startIndex = state.currentTaskIndex;
|
|
1094
|
+
lane.startIndex = Math.max(0, state.currentTaskIndex - 1); // Task was blocked, retry it
|
|
1054
1095
|
}
|
|
1096
|
+
|
|
1097
|
+
events.emit('lane.blocked', {
|
|
1098
|
+
laneName: finished.name,
|
|
1099
|
+
dependencyRequest: state.dependencyRequest,
|
|
1100
|
+
});
|
|
1101
|
+
logger.warn(`Lane ${finished.name} is blocked on dependency change request`);
|
|
1102
|
+
} else {
|
|
1103
|
+
failedLanes.add(finished.name);
|
|
1104
|
+
logger.error(`Lane ${finished.name} exited with code 2 but no dependency request found`);
|
|
1055
1105
|
}
|
|
1106
|
+
} else {
|
|
1107
|
+
// Check if it was a restart request
|
|
1108
|
+
if (info.stallPhase === 2) {
|
|
1109
|
+
logger.info(`🔄 Lane ${finished.name} is being restarted due to stall...`);
|
|
1110
|
+
|
|
1111
|
+
// Update startIndex from current state to resume from the same task
|
|
1112
|
+
const statePath = safeJoin(laneRunDirs[finished.name]!, 'state.json');
|
|
1113
|
+
const state = loadState<LaneState>(statePath);
|
|
1114
|
+
if (state) {
|
|
1115
|
+
const lane = lanes.find(l => l.name === finished.name);
|
|
1116
|
+
if (lane) {
|
|
1117
|
+
lane.startIndex = state.currentTaskIndex;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Note: we don't add to failedLanes or completedLanes,
|
|
1122
|
+
// so it will be eligible to start again in the next iteration.
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
failedLanes.add(finished.name);
|
|
1056
1127
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1128
|
+
let errorMsg = 'Process exited with non-zero code';
|
|
1129
|
+
if (info.stallPhase === 3) {
|
|
1130
|
+
errorMsg = 'Stopped due to repeated stall';
|
|
1131
|
+
} else if (info.logManager) {
|
|
1132
|
+
const lastError = info.logManager.getLastError();
|
|
1133
|
+
if (lastError) {
|
|
1134
|
+
errorMsg = `Process failed: ${lastError}`;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1061
1137
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1138
|
+
logger.error(`[${finished.name}] Lane failed with exit code ${finished.code}: ${errorMsg}`);
|
|
1139
|
+
|
|
1140
|
+
// Log log tail for visibility
|
|
1141
|
+
if (info.logPath) {
|
|
1142
|
+
logFileTail(info.logPath, 15);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
events.emit('lane.failed', {
|
|
1146
|
+
laneName: finished.name,
|
|
1147
|
+
exitCode: finished.code,
|
|
1148
|
+
error: errorMsg,
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
printLaneStatus(lanes, laneRunDirs);
|
|
1068
1153
|
}
|
|
1069
|
-
|
|
1070
|
-
printLaneStatus(lanes, laneRunDirs);
|
|
1071
1154
|
} else {
|
|
1072
1155
|
// Nothing running. Are we blocked?
|
|
1156
|
+
|
|
1157
|
+
// Wait a bit to avoid busy-spin while waiting for dependencies or new slots
|
|
1158
|
+
if (completedLanes.size + failedLanes.size + blockedLanes.size < lanes.length) {
|
|
1159
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1073
1162
|
if (blockedLanes.size > 0 && autoResolve) {
|
|
1074
1163
|
logger.section('🛠 Auto-Resolving Dependencies');
|
|
1075
1164
|
|
|
@@ -1098,10 +1187,14 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
1098
1187
|
// All finished
|
|
1099
1188
|
break;
|
|
1100
1189
|
}
|
|
1190
|
+
}
|
|
1101
1191
|
}
|
|
1192
|
+
} finally {
|
|
1193
|
+
clearInterval(monitorInterval);
|
|
1194
|
+
process.removeListener('SIGINT', sigIntHandler);
|
|
1195
|
+
process.removeListener('SIGTERM', sigIntHandler);
|
|
1102
1196
|
}
|
|
1103
1197
|
|
|
1104
|
-
clearInterval(monitorInterval);
|
|
1105
1198
|
printLaneStatus(lanes, laneRunDirs);
|
|
1106
1199
|
|
|
1107
1200
|
// Check for failures
|
package/src/utils/config.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import * as fs from 'fs';
|
|
9
|
+
import * as git from './git';
|
|
9
10
|
import { CursorFlowConfig } from './types';
|
|
10
11
|
import { safeJoin } from './path';
|
|
11
12
|
export { CursorFlowConfig };
|
|
@@ -167,6 +168,7 @@ export function validateConfig(config: CursorFlowConfig): boolean {
|
|
|
167
168
|
*/
|
|
168
169
|
export function createDefaultConfig(projectRoot: string, force = false): string {
|
|
169
170
|
const configPath = safeJoin(projectRoot, 'cursorflow.config.js');
|
|
171
|
+
const currentBranch = git.getCurrentBranch() || 'main';
|
|
170
172
|
|
|
171
173
|
const template = `module.exports = {
|
|
172
174
|
// Directory configuration
|
|
@@ -175,7 +177,7 @@ export function createDefaultConfig(projectRoot: string, force = false): string
|
|
|
175
177
|
pofDir: '_cursorflow/pof',
|
|
176
178
|
|
|
177
179
|
// Git configuration
|
|
178
|
-
baseBranch:
|
|
180
|
+
baseBranch: '${currentBranch}',
|
|
179
181
|
branchPrefix: 'feature/',
|
|
180
182
|
|
|
181
183
|
// Execution configuration
|
|
@@ -1024,7 +1024,7 @@ export class EnhancedLogManager {
|
|
|
1024
1024
|
}
|
|
1025
1025
|
|
|
1026
1026
|
/**
|
|
1027
|
-
* Close all log files
|
|
1027
|
+
* Close all log files and ensure all data is flushed to disk
|
|
1028
1028
|
*/
|
|
1029
1029
|
public close(): void {
|
|
1030
1030
|
// Flush transform stream
|
|
@@ -1051,46 +1051,87 @@ export class EnhancedLogManager {
|
|
|
1051
1051
|
`;
|
|
1052
1052
|
|
|
1053
1053
|
if (this.cleanLogFd !== null) {
|
|
1054
|
-
|
|
1055
|
-
|
|
1054
|
+
try {
|
|
1055
|
+
fs.writeSync(this.cleanLogFd, endMarker);
|
|
1056
|
+
fs.fsyncSync(this.cleanLogFd);
|
|
1057
|
+
fs.closeSync(this.cleanLogFd);
|
|
1058
|
+
} catch {}
|
|
1056
1059
|
this.cleanLogFd = null;
|
|
1057
1060
|
}
|
|
1058
1061
|
|
|
1059
1062
|
if (this.rawLogFd !== null) {
|
|
1060
|
-
|
|
1061
|
-
|
|
1063
|
+
try {
|
|
1064
|
+
fs.writeSync(this.rawLogFd, endMarker);
|
|
1065
|
+
fs.fsyncSync(this.rawLogFd);
|
|
1066
|
+
fs.closeSync(this.rawLogFd);
|
|
1067
|
+
} catch {}
|
|
1062
1068
|
this.rawLogFd = null;
|
|
1063
1069
|
}
|
|
1064
1070
|
|
|
1065
1071
|
if (this.absoluteRawLogFd !== null) {
|
|
1066
|
-
|
|
1072
|
+
try {
|
|
1073
|
+
fs.fsyncSync(this.absoluteRawLogFd);
|
|
1074
|
+
fs.closeSync(this.absoluteRawLogFd);
|
|
1075
|
+
} catch {}
|
|
1067
1076
|
this.absoluteRawLogFd = null;
|
|
1068
1077
|
}
|
|
1069
1078
|
|
|
1070
1079
|
if (this.jsonLogFd !== null) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1080
|
+
try {
|
|
1081
|
+
this.writeJsonEntry({
|
|
1082
|
+
timestamp: new Date().toISOString(),
|
|
1083
|
+
level: 'session',
|
|
1084
|
+
source: 'system',
|
|
1085
|
+
lane: this.session.laneName,
|
|
1086
|
+
message: 'Session ended',
|
|
1087
|
+
metadata: {
|
|
1088
|
+
sessionId: this.session.id,
|
|
1089
|
+
duration: Date.now() - this.session.startTime,
|
|
1090
|
+
},
|
|
1091
|
+
});
|
|
1092
|
+
fs.fsyncSync(this.jsonLogFd);
|
|
1093
|
+
fs.closeSync(this.jsonLogFd);
|
|
1094
|
+
} catch {}
|
|
1083
1095
|
this.jsonLogFd = null;
|
|
1084
1096
|
}
|
|
1085
1097
|
|
|
1086
1098
|
// Close readable log
|
|
1087
1099
|
if (this.readableLogFd !== null) {
|
|
1088
|
-
|
|
1089
|
-
|
|
1100
|
+
try {
|
|
1101
|
+
fs.writeSync(this.readableLogFd, endMarker);
|
|
1102
|
+
fs.fsyncSync(this.readableLogFd);
|
|
1103
|
+
fs.closeSync(this.readableLogFd);
|
|
1104
|
+
} catch {}
|
|
1090
1105
|
this.readableLogFd = null;
|
|
1091
1106
|
}
|
|
1092
1107
|
}
|
|
1093
1108
|
|
|
1109
|
+
/**
|
|
1110
|
+
* Extract the last error message from the clean log file
|
|
1111
|
+
*/
|
|
1112
|
+
public getLastError(): string | null {
|
|
1113
|
+
try {
|
|
1114
|
+
if (!fs.existsSync(this.cleanLogPath)) return null;
|
|
1115
|
+
const content = fs.readFileSync(this.cleanLogPath, 'utf8');
|
|
1116
|
+
// Look for lines containing error markers
|
|
1117
|
+
const lines = content.split('\n').filter(l =>
|
|
1118
|
+
l.includes('[ERROR]') ||
|
|
1119
|
+
l.includes('❌') ||
|
|
1120
|
+
l.includes('error:') ||
|
|
1121
|
+
l.includes('Fatal') ||
|
|
1122
|
+
l.includes('fail')
|
|
1123
|
+
);
|
|
1124
|
+
if (lines.length === 0) {
|
|
1125
|
+
// Fallback to last 5 lines if no specific error marker found
|
|
1126
|
+
const allLines = content.split('\n').filter(l => l.trim());
|
|
1127
|
+
return allLines.slice(-5).join('\n');
|
|
1128
|
+
}
|
|
1129
|
+
return lines[lines.length - 1]!.trim();
|
|
1130
|
+
} catch {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1094
1135
|
/**
|
|
1095
1136
|
* Format duration for display
|
|
1096
1137
|
*/
|
package/src/utils/git.ts
CHANGED
|
@@ -132,6 +132,10 @@ function filterGitStderr(stderr: string): string {
|
|
|
132
132
|
export function runGit(args: string[], options: GitRunOptions = {}): string {
|
|
133
133
|
const { cwd, silent = false } = options;
|
|
134
134
|
|
|
135
|
+
if (process.env['DEBUG_GIT']) {
|
|
136
|
+
console.log(`[DEBUG_GIT] Running: git ${args.join(' ')} (cwd: ${cwd || process.cwd()})`);
|
|
137
|
+
}
|
|
138
|
+
|
|
135
139
|
try {
|
|
136
140
|
const stdioMode = silent ? 'pipe' : ['inherit', 'inherit', 'pipe'];
|
|
137
141
|
|
|
@@ -141,6 +145,13 @@ export function runGit(args: string[], options: GitRunOptions = {}): string {
|
|
|
141
145
|
stdio: stdioMode as any,
|
|
142
146
|
});
|
|
143
147
|
|
|
148
|
+
if (result.error) {
|
|
149
|
+
if (!silent) {
|
|
150
|
+
console.error(`[ERROR_GIT] Failed to execute git command: ${result.error.message}`);
|
|
151
|
+
}
|
|
152
|
+
throw result.error;
|
|
153
|
+
}
|
|
154
|
+
|
|
144
155
|
if (!silent && result.stderr) {
|
|
145
156
|
const filteredStderr = filterGitStderr(result.stderr);
|
|
146
157
|
if (filteredStderr) {
|
|
@@ -149,12 +160,16 @@ export function runGit(args: string[], options: GitRunOptions = {}): string {
|
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
if (result.status !== 0 && !silent) {
|
|
152
|
-
|
|
163
|
+
const errorMsg = `Git command failed (exit code ${result.status}): git ${args.join(' ')}\n${result.stderr || ''}`;
|
|
164
|
+
throw new Error(errorMsg);
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
return result.stdout ? result.stdout.trim() : '';
|
|
156
168
|
} catch (error) {
|
|
157
169
|
if (silent) {
|
|
170
|
+
if (process.env['DEBUG_GIT']) {
|
|
171
|
+
console.error(`[DEBUG_GIT] Command failed (silent mode): ${error instanceof Error ? error.message : String(error)}`);
|
|
172
|
+
}
|
|
158
173
|
return '';
|
|
159
174
|
}
|
|
160
175
|
throw error;
|
|
@@ -167,18 +182,28 @@ export function runGit(args: string[], options: GitRunOptions = {}): string {
|
|
|
167
182
|
export function runGitResult(args: string[], options: GitRunOptions = {}): GitResult {
|
|
168
183
|
const { cwd } = options;
|
|
169
184
|
|
|
185
|
+
if (process.env['DEBUG_GIT']) {
|
|
186
|
+
console.log(`[DEBUG_GIT] Running: git ${args.join(' ')} (result mode, cwd: ${cwd || process.cwd()})`);
|
|
187
|
+
}
|
|
188
|
+
|
|
170
189
|
const result = spawnSync('git', args, {
|
|
171
190
|
cwd: cwd || process.cwd(),
|
|
172
191
|
encoding: 'utf8',
|
|
173
192
|
stdio: 'pipe',
|
|
174
193
|
});
|
|
175
194
|
|
|
176
|
-
|
|
195
|
+
const gitResult = {
|
|
177
196
|
exitCode: result.status ?? 1,
|
|
178
197
|
stdout: (result.stdout || '').toString().trim(),
|
|
179
198
|
stderr: (result.stderr || '').toString().trim(),
|
|
180
199
|
success: result.status === 0,
|
|
181
200
|
};
|
|
201
|
+
|
|
202
|
+
if (process.env['DEBUG_GIT'] && !gitResult.success) {
|
|
203
|
+
console.error(`[DEBUG_GIT] Command failed: git ${args.join(' ')}\nstderr: ${gitResult.stderr}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return gitResult;
|
|
182
207
|
}
|
|
183
208
|
|
|
184
209
|
/**
|
|
@@ -829,6 +854,94 @@ export function getWorktreeForPath(targetPath: string, cwd?: string): WorktreeIn
|
|
|
829
854
|
/**
|
|
830
855
|
* Sync branch with remote (fetch + merge or rebase)
|
|
831
856
|
*/
|
|
857
|
+
/**
|
|
858
|
+
* Push branch to remote, creating it if it doesn't exist
|
|
859
|
+
* Returns success status and any error message
|
|
860
|
+
*/
|
|
861
|
+
export function pushBranchSafe(branchName: string, options: { cwd?: string; force?: boolean } = {}): { success: boolean; error?: string } {
|
|
862
|
+
const { cwd, force = false } = options;
|
|
863
|
+
|
|
864
|
+
// Check if origin exists
|
|
865
|
+
if (!remoteExists('origin', { cwd })) {
|
|
866
|
+
return { success: false, error: 'No remote "origin" configured' };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const args = ['push'];
|
|
870
|
+
if (force) {
|
|
871
|
+
args.push('--force');
|
|
872
|
+
}
|
|
873
|
+
args.push('-u', 'origin', branchName);
|
|
874
|
+
|
|
875
|
+
const result = runGitResult(args, { cwd });
|
|
876
|
+
|
|
877
|
+
if (result.success) {
|
|
878
|
+
return { success: true };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return { success: false, error: result.stderr };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Auto-commit any uncommitted changes and push to remote
|
|
886
|
+
* Used for checkpoint before destructive operations
|
|
887
|
+
*/
|
|
888
|
+
export function checkpointAndPush(options: {
|
|
889
|
+
cwd?: string;
|
|
890
|
+
message?: string;
|
|
891
|
+
branchName?: string;
|
|
892
|
+
} = {}): { success: boolean; committed: boolean; pushed: boolean; error?: string } {
|
|
893
|
+
const { cwd, message = '[cursorflow] checkpoint before clean' } = options;
|
|
894
|
+
|
|
895
|
+
let committed = false;
|
|
896
|
+
let pushed = false;
|
|
897
|
+
|
|
898
|
+
// Get current branch if not specified
|
|
899
|
+
let branchName = options.branchName;
|
|
900
|
+
if (!branchName) {
|
|
901
|
+
try {
|
|
902
|
+
branchName = getCurrentBranch(cwd);
|
|
903
|
+
} catch {
|
|
904
|
+
return { success: false, committed: false, pushed: false, error: 'Failed to get current branch' };
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Check for uncommitted changes
|
|
909
|
+
if (hasUncommittedChanges(cwd)) {
|
|
910
|
+
// Stage all changes
|
|
911
|
+
const addResult = runGitResult(['add', '-A'], { cwd });
|
|
912
|
+
if (!addResult.success) {
|
|
913
|
+
return { success: false, committed: false, pushed: false, error: `Failed to stage changes: ${addResult.stderr}` };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Commit
|
|
917
|
+
const commitResult = runGitResult(['commit', '-m', message], { cwd });
|
|
918
|
+
if (!commitResult.success) {
|
|
919
|
+
return { success: false, committed: false, pushed: false, error: `Failed to commit: ${commitResult.stderr}` };
|
|
920
|
+
}
|
|
921
|
+
committed = true;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Push to remote
|
|
925
|
+
const pushResult = pushBranchSafe(branchName, { cwd });
|
|
926
|
+
if (pushResult.success) {
|
|
927
|
+
pushed = true;
|
|
928
|
+
} else {
|
|
929
|
+
// Push failed but commit succeeded - partial success
|
|
930
|
+
if (committed) {
|
|
931
|
+
return { success: true, committed: true, pushed: false, error: `Commit succeeded but push failed: ${pushResult.error}` };
|
|
932
|
+
}
|
|
933
|
+
// Nothing to commit and push failed - check if there's anything to push
|
|
934
|
+
const localCommits = runGitResult(['rev-list', `origin/${branchName}..HEAD`], { cwd });
|
|
935
|
+
if (localCommits.success && localCommits.stdout.trim()) {
|
|
936
|
+
return { success: false, committed: false, pushed: false, error: `Push failed: ${pushResult.error}` };
|
|
937
|
+
}
|
|
938
|
+
// Nothing to push
|
|
939
|
+
pushed = true;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return { success: true, committed, pushed };
|
|
943
|
+
}
|
|
944
|
+
|
|
832
945
|
export function syncWithRemote(branch: string, options: {
|
|
833
946
|
cwd?: string;
|
|
834
947
|
strategy?: 'merge' | 'rebase';
|