episoda 0.2.19 → 0.2.21
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/dist/daemon/daemon-process.js +1442 -272
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +1964 -135
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -301,8 +301,8 @@ var require_git_executor = __commonJS({
|
|
|
301
301
|
var util_1 = require("util");
|
|
302
302
|
var git_validator_1 = require_git_validator();
|
|
303
303
|
var git_parser_1 = require_git_parser();
|
|
304
|
-
var
|
|
305
|
-
var
|
|
304
|
+
var execAsync2 = (0, util_1.promisify)(child_process_1.exec);
|
|
305
|
+
var GitExecutor3 = class {
|
|
306
306
|
/**
|
|
307
307
|
* Execute a git command
|
|
308
308
|
* @param command - The git command to execute
|
|
@@ -388,6 +388,19 @@ var require_git_executor = __commonJS({
|
|
|
388
388
|
return await this.executeRebaseContinue(cwd, options);
|
|
389
389
|
case "rebase_status":
|
|
390
390
|
return await this.executeRebaseStatus(cwd, options);
|
|
391
|
+
// EP944: Worktree operations
|
|
392
|
+
case "worktree_add":
|
|
393
|
+
return await this.executeWorktreeAdd(command, cwd, options);
|
|
394
|
+
case "worktree_remove":
|
|
395
|
+
return await this.executeWorktreeRemove(command, cwd, options);
|
|
396
|
+
case "worktree_list":
|
|
397
|
+
return await this.executeWorktreeList(cwd, options);
|
|
398
|
+
case "worktree_prune":
|
|
399
|
+
return await this.executeWorktreePrune(cwd, options);
|
|
400
|
+
case "clone_bare":
|
|
401
|
+
return await this.executeCloneBare(command, options);
|
|
402
|
+
case "project_info":
|
|
403
|
+
return await this.executeProjectInfo(cwd, options);
|
|
391
404
|
default:
|
|
392
405
|
return {
|
|
393
406
|
success: false,
|
|
@@ -613,7 +626,7 @@ var require_git_executor = __commonJS({
|
|
|
613
626
|
let isLocal = false;
|
|
614
627
|
let isRemote = false;
|
|
615
628
|
try {
|
|
616
|
-
const { stdout: localBranches } = await
|
|
629
|
+
const { stdout: localBranches } = await execAsync2("git branch --list", { cwd, timeout: options?.timeout || 1e4 });
|
|
617
630
|
isLocal = localBranches.split("\n").some((line) => {
|
|
618
631
|
const branchName = line.replace(/^\*?\s*/, "").trim();
|
|
619
632
|
return branchName === command.branch;
|
|
@@ -621,7 +634,7 @@ var require_git_executor = __commonJS({
|
|
|
621
634
|
} catch {
|
|
622
635
|
}
|
|
623
636
|
try {
|
|
624
|
-
const { stdout: remoteBranches } = await
|
|
637
|
+
const { stdout: remoteBranches } = await execAsync2(`git ls-remote --heads origin ${command.branch}`, { cwd, timeout: options?.timeout || 1e4 });
|
|
625
638
|
isRemote = remoteBranches.trim().length > 0;
|
|
626
639
|
} catch {
|
|
627
640
|
}
|
|
@@ -658,7 +671,7 @@ var require_git_executor = __commonJS({
|
|
|
658
671
|
}
|
|
659
672
|
const baseBranch = command.baseBranch || "main";
|
|
660
673
|
try {
|
|
661
|
-
const { stdout } = await
|
|
674
|
+
const { stdout } = await execAsync2(`git cherry origin/${baseBranch} ${command.branch}`, { cwd, timeout: options?.timeout || 1e4 });
|
|
662
675
|
const uniqueCommits = stdout.trim().split("\n").filter((line) => line.startsWith("+"));
|
|
663
676
|
const hasCommits = uniqueCommits.length > 0;
|
|
664
677
|
return {
|
|
@@ -671,7 +684,7 @@ var require_git_executor = __commonJS({
|
|
|
671
684
|
};
|
|
672
685
|
} catch (error) {
|
|
673
686
|
try {
|
|
674
|
-
const { stdout } = await
|
|
687
|
+
const { stdout } = await execAsync2(`git rev-list --count origin/${baseBranch}..${command.branch}`, { cwd, timeout: options?.timeout || 1e4 });
|
|
675
688
|
const commitCount = parseInt(stdout.trim(), 10);
|
|
676
689
|
const hasCommits = commitCount > 0;
|
|
677
690
|
return {
|
|
@@ -700,7 +713,7 @@ var require_git_executor = __commonJS({
|
|
|
700
713
|
*/
|
|
701
714
|
async executeFindBranchByPrefix(command, cwd, options) {
|
|
702
715
|
try {
|
|
703
|
-
const { stdout } = await
|
|
716
|
+
const { stdout } = await execAsync2("git branch -a", { cwd, timeout: options?.timeout || 1e4 });
|
|
704
717
|
const prefix = command.prefix;
|
|
705
718
|
const branches = stdout.split("\n").map((line) => line.replace(/^[\s*]*/, "").replace("remotes/origin/", "").trim()).filter((branch) => branch && !branch.includes("->"));
|
|
706
719
|
const matchingBranch = branches.find((branch) => branch.startsWith(prefix));
|
|
@@ -724,7 +737,7 @@ var require_git_executor = __commonJS({
|
|
|
724
737
|
try {
|
|
725
738
|
let currentBranch = "";
|
|
726
739
|
try {
|
|
727
|
-
const { stdout } = await
|
|
740
|
+
const { stdout } = await execAsync2("git branch --show-current", { cwd, timeout: options?.timeout || 1e4 });
|
|
728
741
|
currentBranch = stdout.trim();
|
|
729
742
|
} catch (error) {
|
|
730
743
|
return {
|
|
@@ -735,7 +748,7 @@ var require_git_executor = __commonJS({
|
|
|
735
748
|
}
|
|
736
749
|
let uncommittedFiles = [];
|
|
737
750
|
try {
|
|
738
|
-
const { stdout } = await
|
|
751
|
+
const { stdout } = await execAsync2("git status --porcelain", { cwd, timeout: options?.timeout || 1e4 });
|
|
739
752
|
if (stdout) {
|
|
740
753
|
uncommittedFiles = stdout.split("\n").filter((line) => line.trim()).map((line) => {
|
|
741
754
|
const parts = line.trim().split(/\s+/);
|
|
@@ -747,7 +760,7 @@ var require_git_executor = __commonJS({
|
|
|
747
760
|
let localCommits = [];
|
|
748
761
|
if (currentBranch === "main") {
|
|
749
762
|
try {
|
|
750
|
-
const { stdout } = await
|
|
763
|
+
const { stdout } = await execAsync2('git log origin/main..HEAD --format="%H|%s|%an"', { cwd, timeout: options?.timeout || 1e4 });
|
|
751
764
|
if (stdout) {
|
|
752
765
|
localCommits = stdout.split("\n").filter((line) => line.trim()).map((line) => {
|
|
753
766
|
const [sha, message, author] = line.split("|");
|
|
@@ -795,11 +808,11 @@ var require_git_executor = __commonJS({
|
|
|
795
808
|
try {
|
|
796
809
|
let stdout;
|
|
797
810
|
try {
|
|
798
|
-
const result = await
|
|
811
|
+
const result = await execAsync2(`git log ${baseBranch}.."${command.branch}" --pretty=format:"%H|%an|%ae|%aI|%s" -n ${limit} --`, { cwd, timeout: options?.timeout || 1e4 });
|
|
799
812
|
stdout = result.stdout;
|
|
800
813
|
} catch (error) {
|
|
801
814
|
try {
|
|
802
|
-
const result = await
|
|
815
|
+
const result = await execAsync2(`git log "${command.branch}" --pretty=format:"%H|%an|%ae|%aI|%s" -n ${limit} --`, { cwd, timeout: options?.timeout || 1e4 });
|
|
803
816
|
stdout = result.stdout;
|
|
804
817
|
} catch (branchError) {
|
|
805
818
|
return {
|
|
@@ -821,7 +834,7 @@ var require_git_executor = __commonJS({
|
|
|
821
834
|
const commitLines = stdout.trim().split("\n");
|
|
822
835
|
let remoteShas = /* @__PURE__ */ new Set();
|
|
823
836
|
try {
|
|
824
|
-
const { stdout: remoteCommits } = await
|
|
837
|
+
const { stdout: remoteCommits } = await execAsync2(`git log "origin/${command.branch}" --pretty=format:"%H" -n ${limit} --`, { cwd, timeout: options?.timeout || 1e4 });
|
|
825
838
|
remoteShas = new Set(remoteCommits.trim().split("\n").filter(Boolean));
|
|
826
839
|
} catch {
|
|
827
840
|
}
|
|
@@ -1056,52 +1069,52 @@ var require_git_executor = __commonJS({
|
|
|
1056
1069
|
let hasStash = false;
|
|
1057
1070
|
const cherryPickedCommits = [];
|
|
1058
1071
|
try {
|
|
1059
|
-
const { stdout: currentBranchOut } = await
|
|
1072
|
+
const { stdout: currentBranchOut } = await execAsync2("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
1060
1073
|
const currentBranch = currentBranchOut.trim();
|
|
1061
|
-
const { stdout: statusOutput } = await
|
|
1074
|
+
const { stdout: statusOutput } = await execAsync2("git status --porcelain", { cwd });
|
|
1062
1075
|
if (statusOutput.trim()) {
|
|
1063
1076
|
try {
|
|
1064
|
-
await
|
|
1065
|
-
const { stdout: stashHash } = await
|
|
1077
|
+
await execAsync2("git add -A", { cwd });
|
|
1078
|
+
const { stdout: stashHash } = await execAsync2('git stash create -m "episoda-move-to-module"', { cwd });
|
|
1066
1079
|
if (stashHash && stashHash.trim()) {
|
|
1067
|
-
await
|
|
1068
|
-
await
|
|
1080
|
+
await execAsync2(`git stash store -m "episoda-move-to-module" ${stashHash.trim()}`, { cwd });
|
|
1081
|
+
await execAsync2("git reset --hard HEAD", { cwd });
|
|
1069
1082
|
hasStash = true;
|
|
1070
1083
|
}
|
|
1071
1084
|
} catch (stashError) {
|
|
1072
1085
|
}
|
|
1073
1086
|
}
|
|
1074
1087
|
if (commitShas && commitShas.length > 0 && currentBranch !== "main" && currentBranch !== "master") {
|
|
1075
|
-
await
|
|
1088
|
+
await execAsync2("git checkout main", { cwd });
|
|
1076
1089
|
}
|
|
1077
1090
|
let branchExists = false;
|
|
1078
1091
|
try {
|
|
1079
|
-
await
|
|
1092
|
+
await execAsync2(`git rev-parse --verify ${targetBranch}`, { cwd });
|
|
1080
1093
|
branchExists = true;
|
|
1081
1094
|
} catch {
|
|
1082
1095
|
branchExists = false;
|
|
1083
1096
|
}
|
|
1084
1097
|
if (!branchExists) {
|
|
1085
|
-
await
|
|
1098
|
+
await execAsync2(`git checkout -b ${targetBranch}`, { cwd });
|
|
1086
1099
|
} else {
|
|
1087
|
-
await
|
|
1100
|
+
await execAsync2(`git checkout ${targetBranch}`, { cwd });
|
|
1088
1101
|
if (currentBranch === "main" || currentBranch === "master") {
|
|
1089
1102
|
try {
|
|
1090
1103
|
const mergeStrategy = conflictResolution === "ours" ? "--strategy=ours" : conflictResolution === "theirs" ? "--strategy-option=theirs" : "";
|
|
1091
|
-
await
|
|
1104
|
+
await execAsync2(`git merge ${currentBranch} ${mergeStrategy} --no-edit`, { cwd });
|
|
1092
1105
|
} catch (mergeError) {
|
|
1093
|
-
const { stdout: conflictStatus } = await
|
|
1106
|
+
const { stdout: conflictStatus } = await execAsync2("git status --porcelain", { cwd });
|
|
1094
1107
|
if (conflictStatus.includes("UU ") || conflictStatus.includes("AA ") || conflictStatus.includes("DD ")) {
|
|
1095
|
-
const { stdout: conflictFiles } = await
|
|
1108
|
+
const { stdout: conflictFiles } = await execAsync2("git diff --name-only --diff-filter=U", { cwd });
|
|
1096
1109
|
const conflictedFiles = conflictFiles.trim().split("\n").filter(Boolean);
|
|
1097
1110
|
if (conflictResolution) {
|
|
1098
1111
|
for (const file of conflictedFiles) {
|
|
1099
|
-
await
|
|
1100
|
-
await
|
|
1112
|
+
await execAsync2(`git checkout --${conflictResolution} "${file}"`, { cwd });
|
|
1113
|
+
await execAsync2(`git add "${file}"`, { cwd });
|
|
1101
1114
|
}
|
|
1102
|
-
await
|
|
1115
|
+
await execAsync2("git commit --no-edit", { cwd });
|
|
1103
1116
|
} else {
|
|
1104
|
-
await
|
|
1117
|
+
await execAsync2("git merge --abort", { cwd });
|
|
1105
1118
|
return {
|
|
1106
1119
|
success: false,
|
|
1107
1120
|
error: "MERGE_CONFLICT",
|
|
@@ -1120,32 +1133,32 @@ var require_git_executor = __commonJS({
|
|
|
1120
1133
|
if (commitShas && commitShas.length > 0 && (currentBranch === "main" || currentBranch === "master")) {
|
|
1121
1134
|
for (const sha of commitShas) {
|
|
1122
1135
|
try {
|
|
1123
|
-
const { stdout: logOutput } = await
|
|
1136
|
+
const { stdout: logOutput } = await execAsync2(`git log --format=%H ${targetBranch} | grep ${sha}`, { cwd }).catch(() => ({ stdout: "" }));
|
|
1124
1137
|
if (!logOutput.trim()) {
|
|
1125
|
-
await
|
|
1138
|
+
await execAsync2(`git cherry-pick ${sha}`, { cwd });
|
|
1126
1139
|
cherryPickedCommits.push(sha);
|
|
1127
1140
|
}
|
|
1128
1141
|
} catch (err) {
|
|
1129
|
-
await
|
|
1142
|
+
await execAsync2("git cherry-pick --abort", { cwd }).catch(() => {
|
|
1130
1143
|
});
|
|
1131
1144
|
}
|
|
1132
1145
|
}
|
|
1133
|
-
await
|
|
1134
|
-
await
|
|
1135
|
-
await
|
|
1146
|
+
await execAsync2("git checkout main", { cwd });
|
|
1147
|
+
await execAsync2("git reset --hard origin/main", { cwd });
|
|
1148
|
+
await execAsync2(`git checkout ${targetBranch}`, { cwd });
|
|
1136
1149
|
}
|
|
1137
1150
|
if (hasStash) {
|
|
1138
1151
|
try {
|
|
1139
|
-
await
|
|
1152
|
+
await execAsync2("git stash pop", { cwd });
|
|
1140
1153
|
} catch (stashError) {
|
|
1141
|
-
const { stdout: conflictStatus } = await
|
|
1154
|
+
const { stdout: conflictStatus } = await execAsync2("git status --porcelain", { cwd });
|
|
1142
1155
|
if (conflictStatus.includes("UU ") || conflictStatus.includes("AA ")) {
|
|
1143
1156
|
if (conflictResolution) {
|
|
1144
|
-
const { stdout: conflictFiles } = await
|
|
1157
|
+
const { stdout: conflictFiles } = await execAsync2("git diff --name-only --diff-filter=U", { cwd });
|
|
1145
1158
|
const conflictedFiles = conflictFiles.trim().split("\n").filter(Boolean);
|
|
1146
1159
|
for (const file of conflictedFiles) {
|
|
1147
|
-
await
|
|
1148
|
-
await
|
|
1160
|
+
await execAsync2(`git checkout --${conflictResolution} "${file}"`, { cwd });
|
|
1161
|
+
await execAsync2(`git add "${file}"`, { cwd });
|
|
1149
1162
|
}
|
|
1150
1163
|
}
|
|
1151
1164
|
}
|
|
@@ -1163,9 +1176,9 @@ var require_git_executor = __commonJS({
|
|
|
1163
1176
|
} catch (error) {
|
|
1164
1177
|
if (hasStash) {
|
|
1165
1178
|
try {
|
|
1166
|
-
const { stdout: stashList } = await
|
|
1179
|
+
const { stdout: stashList } = await execAsync2("git stash list", { cwd });
|
|
1167
1180
|
if (stashList.includes("episoda-move-to-module")) {
|
|
1168
|
-
await
|
|
1181
|
+
await execAsync2("git stash pop", { cwd });
|
|
1169
1182
|
}
|
|
1170
1183
|
} catch (e) {
|
|
1171
1184
|
}
|
|
@@ -1183,7 +1196,7 @@ var require_git_executor = __commonJS({
|
|
|
1183
1196
|
*/
|
|
1184
1197
|
async executeDiscardMainChanges(cwd, options) {
|
|
1185
1198
|
try {
|
|
1186
|
-
const { stdout: currentBranchOut } = await
|
|
1199
|
+
const { stdout: currentBranchOut } = await execAsync2("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
1187
1200
|
const branch = currentBranchOut.trim();
|
|
1188
1201
|
if (branch !== "main" && branch !== "master") {
|
|
1189
1202
|
return {
|
|
@@ -1193,22 +1206,22 @@ var require_git_executor = __commonJS({
|
|
|
1193
1206
|
};
|
|
1194
1207
|
}
|
|
1195
1208
|
let discardedFiles = 0;
|
|
1196
|
-
const { stdout: statusOutput } = await
|
|
1209
|
+
const { stdout: statusOutput } = await execAsync2("git status --porcelain", { cwd });
|
|
1197
1210
|
if (statusOutput.trim()) {
|
|
1198
1211
|
try {
|
|
1199
|
-
await
|
|
1212
|
+
await execAsync2("git stash --include-untracked", { cwd });
|
|
1200
1213
|
discardedFiles = statusOutput.trim().split("\n").length;
|
|
1201
1214
|
} catch (stashError) {
|
|
1202
1215
|
}
|
|
1203
1216
|
}
|
|
1204
|
-
await
|
|
1205
|
-
await
|
|
1217
|
+
await execAsync2("git fetch origin", { cwd });
|
|
1218
|
+
await execAsync2(`git reset --hard origin/${branch}`, { cwd });
|
|
1206
1219
|
try {
|
|
1207
|
-
await
|
|
1220
|
+
await execAsync2("git clean -fd", { cwd });
|
|
1208
1221
|
} catch (cleanError) {
|
|
1209
1222
|
}
|
|
1210
1223
|
try {
|
|
1211
|
-
await
|
|
1224
|
+
await execAsync2("git stash drop", { cwd });
|
|
1212
1225
|
} catch (dropError) {
|
|
1213
1226
|
}
|
|
1214
1227
|
return {
|
|
@@ -1244,7 +1257,7 @@ var require_git_executor = __commonJS({
|
|
|
1244
1257
|
};
|
|
1245
1258
|
}
|
|
1246
1259
|
try {
|
|
1247
|
-
await
|
|
1260
|
+
await execAsync2("git fetch origin", { cwd, timeout: options?.timeout || 3e4 });
|
|
1248
1261
|
} catch (fetchError) {
|
|
1249
1262
|
return {
|
|
1250
1263
|
success: false,
|
|
@@ -1255,13 +1268,13 @@ var require_git_executor = __commonJS({
|
|
|
1255
1268
|
let commitsBehind = 0;
|
|
1256
1269
|
let commitsAhead = 0;
|
|
1257
1270
|
try {
|
|
1258
|
-
const { stdout: behindOutput } = await
|
|
1271
|
+
const { stdout: behindOutput } = await execAsync2(`git rev-list --count ${command.branch}..origin/main`, { cwd, timeout: options?.timeout || 1e4 });
|
|
1259
1272
|
commitsBehind = parseInt(behindOutput.trim(), 10) || 0;
|
|
1260
1273
|
} catch {
|
|
1261
1274
|
commitsBehind = 0;
|
|
1262
1275
|
}
|
|
1263
1276
|
try {
|
|
1264
|
-
const { stdout: aheadOutput } = await
|
|
1277
|
+
const { stdout: aheadOutput } = await execAsync2(`git rev-list --count origin/main..${command.branch}`, { cwd, timeout: options?.timeout || 1e4 });
|
|
1265
1278
|
commitsAhead = parseInt(aheadOutput.trim(), 10) || 0;
|
|
1266
1279
|
} catch {
|
|
1267
1280
|
commitsAhead = 0;
|
|
@@ -1297,12 +1310,12 @@ var require_git_executor = __commonJS({
|
|
|
1297
1310
|
try {
|
|
1298
1311
|
let currentBranch = "";
|
|
1299
1312
|
try {
|
|
1300
|
-
const { stdout } = await
|
|
1313
|
+
const { stdout } = await execAsync2("git branch --show-current", { cwd, timeout: 5e3 });
|
|
1301
1314
|
currentBranch = stdout.trim();
|
|
1302
1315
|
} catch {
|
|
1303
1316
|
}
|
|
1304
1317
|
try {
|
|
1305
|
-
await
|
|
1318
|
+
await execAsync2("git fetch origin main", { cwd, timeout: options?.timeout || 3e4 });
|
|
1306
1319
|
} catch (fetchError) {
|
|
1307
1320
|
return {
|
|
1308
1321
|
success: false,
|
|
@@ -1312,7 +1325,7 @@ var require_git_executor = __commonJS({
|
|
|
1312
1325
|
}
|
|
1313
1326
|
const needsSwitch = currentBranch !== "main" && currentBranch !== "";
|
|
1314
1327
|
if (needsSwitch) {
|
|
1315
|
-
const { stdout: statusOutput } = await
|
|
1328
|
+
const { stdout: statusOutput } = await execAsync2("git status --porcelain", { cwd, timeout: 5e3 });
|
|
1316
1329
|
if (statusOutput.trim()) {
|
|
1317
1330
|
return {
|
|
1318
1331
|
success: false,
|
|
@@ -1323,13 +1336,13 @@ var require_git_executor = __commonJS({
|
|
|
1323
1336
|
}
|
|
1324
1337
|
};
|
|
1325
1338
|
}
|
|
1326
|
-
await
|
|
1339
|
+
await execAsync2("git checkout main", { cwd, timeout: options?.timeout || 1e4 });
|
|
1327
1340
|
}
|
|
1328
1341
|
try {
|
|
1329
|
-
await
|
|
1342
|
+
await execAsync2("git pull origin main", { cwd, timeout: options?.timeout || 3e4 });
|
|
1330
1343
|
} catch (pullError) {
|
|
1331
1344
|
if (pullError.message?.includes("CONFLICT") || pullError.stderr?.includes("CONFLICT")) {
|
|
1332
|
-
await
|
|
1345
|
+
await execAsync2("git merge --abort", { cwd, timeout: 5e3 }).catch(() => {
|
|
1333
1346
|
});
|
|
1334
1347
|
return {
|
|
1335
1348
|
success: false,
|
|
@@ -1340,7 +1353,7 @@ var require_git_executor = __commonJS({
|
|
|
1340
1353
|
throw pullError;
|
|
1341
1354
|
}
|
|
1342
1355
|
if (needsSwitch && currentBranch) {
|
|
1343
|
-
await
|
|
1356
|
+
await execAsync2(`git checkout "${currentBranch}"`, { cwd, timeout: options?.timeout || 1e4 });
|
|
1344
1357
|
}
|
|
1345
1358
|
return {
|
|
1346
1359
|
success: true,
|
|
@@ -1370,7 +1383,7 @@ var require_git_executor = __commonJS({
|
|
|
1370
1383
|
error: validation.error || "UNKNOWN_ERROR"
|
|
1371
1384
|
};
|
|
1372
1385
|
}
|
|
1373
|
-
const { stdout: statusOutput } = await
|
|
1386
|
+
const { stdout: statusOutput } = await execAsync2("git status --porcelain", { cwd, timeout: 5e3 });
|
|
1374
1387
|
if (statusOutput.trim()) {
|
|
1375
1388
|
return {
|
|
1376
1389
|
success: false,
|
|
@@ -1381,24 +1394,24 @@ var require_git_executor = __commonJS({
|
|
|
1381
1394
|
}
|
|
1382
1395
|
};
|
|
1383
1396
|
}
|
|
1384
|
-
const { stdout: currentBranchOut } = await
|
|
1397
|
+
const { stdout: currentBranchOut } = await execAsync2("git branch --show-current", { cwd, timeout: 5e3 });
|
|
1385
1398
|
const currentBranch = currentBranchOut.trim();
|
|
1386
1399
|
if (currentBranch !== command.branch) {
|
|
1387
|
-
await
|
|
1400
|
+
await execAsync2(`git checkout "${command.branch}"`, { cwd, timeout: options?.timeout || 1e4 });
|
|
1388
1401
|
}
|
|
1389
|
-
await
|
|
1402
|
+
await execAsync2("git fetch origin main", { cwd, timeout: options?.timeout || 3e4 });
|
|
1390
1403
|
try {
|
|
1391
|
-
await
|
|
1404
|
+
await execAsync2("git rebase origin/main", { cwd, timeout: options?.timeout || 6e4 });
|
|
1392
1405
|
} catch (rebaseError) {
|
|
1393
1406
|
const errorOutput = (rebaseError.stderr || "") + (rebaseError.stdout || "");
|
|
1394
1407
|
if (errorOutput.includes("CONFLICT") || errorOutput.includes("could not apply")) {
|
|
1395
1408
|
let conflictFiles = [];
|
|
1396
1409
|
try {
|
|
1397
|
-
const { stdout: conflictOutput } = await
|
|
1410
|
+
const { stdout: conflictOutput } = await execAsync2("git diff --name-only --diff-filter=U", { cwd, timeout: 5e3 });
|
|
1398
1411
|
conflictFiles = conflictOutput.trim().split("\n").filter(Boolean);
|
|
1399
1412
|
} catch {
|
|
1400
1413
|
try {
|
|
1401
|
-
const { stdout: statusOut } = await
|
|
1414
|
+
const { stdout: statusOut } = await execAsync2("git status --porcelain", { cwd, timeout: 5e3 });
|
|
1402
1415
|
conflictFiles = statusOut.trim().split("\n").filter((line) => line.startsWith("UU ") || line.startsWith("AA ") || line.startsWith("DD ")).map((line) => line.slice(3));
|
|
1403
1416
|
} catch {
|
|
1404
1417
|
}
|
|
@@ -1439,7 +1452,7 @@ var require_git_executor = __commonJS({
|
|
|
1439
1452
|
*/
|
|
1440
1453
|
async executeRebaseAbort(cwd, options) {
|
|
1441
1454
|
try {
|
|
1442
|
-
await
|
|
1455
|
+
await execAsync2("git rebase --abort", { cwd, timeout: options?.timeout || 1e4 });
|
|
1443
1456
|
return {
|
|
1444
1457
|
success: true,
|
|
1445
1458
|
output: "Rebase aborted. Your branch has been restored to its previous state.",
|
|
@@ -1469,8 +1482,8 @@ var require_git_executor = __commonJS({
|
|
|
1469
1482
|
*/
|
|
1470
1483
|
async executeRebaseContinue(cwd, options) {
|
|
1471
1484
|
try {
|
|
1472
|
-
await
|
|
1473
|
-
await
|
|
1485
|
+
await execAsync2("git add -A", { cwd, timeout: 5e3 });
|
|
1486
|
+
await execAsync2("git rebase --continue", { cwd, timeout: options?.timeout || 6e4 });
|
|
1474
1487
|
return {
|
|
1475
1488
|
success: true,
|
|
1476
1489
|
output: "Rebase continued successfully.",
|
|
@@ -1483,7 +1496,7 @@ var require_git_executor = __commonJS({
|
|
|
1483
1496
|
if (errorOutput.includes("CONFLICT") || errorOutput.includes("could not apply")) {
|
|
1484
1497
|
let conflictFiles = [];
|
|
1485
1498
|
try {
|
|
1486
|
-
const { stdout: conflictOutput } = await
|
|
1499
|
+
const { stdout: conflictOutput } = await execAsync2("git diff --name-only --diff-filter=U", { cwd, timeout: 5e3 });
|
|
1487
1500
|
conflictFiles = conflictOutput.trim().split("\n").filter(Boolean);
|
|
1488
1501
|
} catch {
|
|
1489
1502
|
}
|
|
@@ -1523,17 +1536,17 @@ var require_git_executor = __commonJS({
|
|
|
1523
1536
|
let inRebase = false;
|
|
1524
1537
|
let rebaseConflicts = [];
|
|
1525
1538
|
try {
|
|
1526
|
-
const { stdout: gitDir } = await
|
|
1539
|
+
const { stdout: gitDir } = await execAsync2("git rev-parse --git-dir", { cwd, timeout: 5e3 });
|
|
1527
1540
|
const gitDirPath = gitDir.trim();
|
|
1528
|
-
const
|
|
1541
|
+
const fs14 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1529
1542
|
const rebaseMergePath = `${gitDirPath}/rebase-merge`;
|
|
1530
1543
|
const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
|
|
1531
1544
|
try {
|
|
1532
|
-
await
|
|
1545
|
+
await fs14.access(rebaseMergePath);
|
|
1533
1546
|
inRebase = true;
|
|
1534
1547
|
} catch {
|
|
1535
1548
|
try {
|
|
1536
|
-
await
|
|
1549
|
+
await fs14.access(rebaseApplyPath);
|
|
1537
1550
|
inRebase = true;
|
|
1538
1551
|
} catch {
|
|
1539
1552
|
inRebase = false;
|
|
@@ -1541,7 +1554,7 @@ var require_git_executor = __commonJS({
|
|
|
1541
1554
|
}
|
|
1542
1555
|
} catch {
|
|
1543
1556
|
try {
|
|
1544
|
-
const { stdout: statusOutput } = await
|
|
1557
|
+
const { stdout: statusOutput } = await execAsync2("git status", { cwd, timeout: 5e3 });
|
|
1545
1558
|
inRebase = statusOutput.includes("rebase in progress") || statusOutput.includes("interactive rebase in progress") || statusOutput.includes("You are currently rebasing");
|
|
1546
1559
|
} catch {
|
|
1547
1560
|
inRebase = false;
|
|
@@ -1549,7 +1562,7 @@ var require_git_executor = __commonJS({
|
|
|
1549
1562
|
}
|
|
1550
1563
|
if (inRebase) {
|
|
1551
1564
|
try {
|
|
1552
|
-
const { stdout: conflictOutput } = await
|
|
1565
|
+
const { stdout: conflictOutput } = await execAsync2("git diff --name-only --diff-filter=U", { cwd, timeout: 5e3 });
|
|
1553
1566
|
rebaseConflicts = conflictOutput.trim().split("\n").filter(Boolean);
|
|
1554
1567
|
} catch {
|
|
1555
1568
|
}
|
|
@@ -1571,6 +1584,328 @@ var require_git_executor = __commonJS({
|
|
|
1571
1584
|
};
|
|
1572
1585
|
}
|
|
1573
1586
|
}
|
|
1587
|
+
// ========================================
|
|
1588
|
+
// EP944: Worktree operations
|
|
1589
|
+
// ========================================
|
|
1590
|
+
/**
|
|
1591
|
+
* EP944: Add a new worktree for a branch
|
|
1592
|
+
* Creates a new working tree at the specified path
|
|
1593
|
+
*/
|
|
1594
|
+
async executeWorktreeAdd(command, cwd, options) {
|
|
1595
|
+
try {
|
|
1596
|
+
const validation = (0, git_validator_1.validateBranchName)(command.branch);
|
|
1597
|
+
if (!validation.valid) {
|
|
1598
|
+
return {
|
|
1599
|
+
success: false,
|
|
1600
|
+
error: validation.error || "UNKNOWN_ERROR"
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
const fs14 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1604
|
+
try {
|
|
1605
|
+
await fs14.access(command.path);
|
|
1606
|
+
return {
|
|
1607
|
+
success: false,
|
|
1608
|
+
error: "WORKTREE_EXISTS",
|
|
1609
|
+
output: `Worktree already exists at path: ${command.path}`
|
|
1610
|
+
};
|
|
1611
|
+
} catch {
|
|
1612
|
+
}
|
|
1613
|
+
try {
|
|
1614
|
+
await this.runGitCommand(["fetch", "--all", "--prune"], cwd, options);
|
|
1615
|
+
} catch {
|
|
1616
|
+
}
|
|
1617
|
+
const args = ["worktree", "add"];
|
|
1618
|
+
if (command.create) {
|
|
1619
|
+
args.push("-b", command.branch, command.path);
|
|
1620
|
+
} else {
|
|
1621
|
+
args.push(command.path, command.branch);
|
|
1622
|
+
}
|
|
1623
|
+
const result = await this.runGitCommand(args, cwd, options);
|
|
1624
|
+
if (result.success) {
|
|
1625
|
+
return {
|
|
1626
|
+
success: true,
|
|
1627
|
+
output: `Created worktree at ${command.path} for branch ${command.branch}`,
|
|
1628
|
+
details: {
|
|
1629
|
+
worktreePath: command.path,
|
|
1630
|
+
branchName: command.branch
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
if (result.output?.includes("already checked out")) {
|
|
1635
|
+
return {
|
|
1636
|
+
success: false,
|
|
1637
|
+
error: "BRANCH_IN_USE",
|
|
1638
|
+
output: `Branch '${command.branch}' is already checked out in another worktree`
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
return result;
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
return {
|
|
1644
|
+
success: false,
|
|
1645
|
+
error: "UNKNOWN_ERROR",
|
|
1646
|
+
output: error.message || "Failed to add worktree"
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* EP944: Remove a worktree
|
|
1652
|
+
* Removes the working tree at the specified path
|
|
1653
|
+
*/
|
|
1654
|
+
async executeWorktreeRemove(command, cwd, options) {
|
|
1655
|
+
try {
|
|
1656
|
+
const fs14 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1657
|
+
try {
|
|
1658
|
+
await fs14.access(command.path);
|
|
1659
|
+
} catch {
|
|
1660
|
+
return {
|
|
1661
|
+
success: false,
|
|
1662
|
+
error: "WORKTREE_NOT_FOUND",
|
|
1663
|
+
output: `Worktree not found at path: ${command.path}`
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
if (!command.force) {
|
|
1667
|
+
try {
|
|
1668
|
+
const { stdout } = await execAsync2("git status --porcelain", {
|
|
1669
|
+
cwd: command.path,
|
|
1670
|
+
timeout: options?.timeout || 1e4
|
|
1671
|
+
});
|
|
1672
|
+
if (stdout.trim()) {
|
|
1673
|
+
return {
|
|
1674
|
+
success: false,
|
|
1675
|
+
error: "UNCOMMITTED_CHANGES",
|
|
1676
|
+
output: "Worktree has uncommitted changes. Use force to remove anyway.",
|
|
1677
|
+
details: {
|
|
1678
|
+
uncommittedFiles: stdout.trim().split("\n").map((line) => line.slice(3))
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
} catch {
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
const args = ["worktree", "remove"];
|
|
1686
|
+
if (command.force) {
|
|
1687
|
+
args.push("--force");
|
|
1688
|
+
}
|
|
1689
|
+
args.push(command.path);
|
|
1690
|
+
const result = await this.runGitCommand(args, cwd, options);
|
|
1691
|
+
if (result.success) {
|
|
1692
|
+
return {
|
|
1693
|
+
success: true,
|
|
1694
|
+
output: `Removed worktree at ${command.path}`,
|
|
1695
|
+
details: {
|
|
1696
|
+
worktreePath: command.path
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
if (result.output?.includes("locked")) {
|
|
1701
|
+
return {
|
|
1702
|
+
success: false,
|
|
1703
|
+
error: "WORKTREE_LOCKED",
|
|
1704
|
+
output: `Worktree at ${command.path} is locked`
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
return result;
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
return {
|
|
1710
|
+
success: false,
|
|
1711
|
+
error: "UNKNOWN_ERROR",
|
|
1712
|
+
output: error.message || "Failed to remove worktree"
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* EP944: List all worktrees
|
|
1718
|
+
* Returns information about all worktrees in the repository
|
|
1719
|
+
*/
|
|
1720
|
+
async executeWorktreeList(cwd, options) {
|
|
1721
|
+
try {
|
|
1722
|
+
const { stdout } = await execAsync2("git worktree list --porcelain", {
|
|
1723
|
+
cwd,
|
|
1724
|
+
timeout: options?.timeout || 1e4
|
|
1725
|
+
});
|
|
1726
|
+
const worktrees = [];
|
|
1727
|
+
const lines = stdout.trim().split("\n");
|
|
1728
|
+
let current = {};
|
|
1729
|
+
for (const line of lines) {
|
|
1730
|
+
if (line.startsWith("worktree ")) {
|
|
1731
|
+
current.path = line.slice(9);
|
|
1732
|
+
} else if (line.startsWith("HEAD ")) {
|
|
1733
|
+
current.commit = line.slice(5);
|
|
1734
|
+
} else if (line.startsWith("branch ")) {
|
|
1735
|
+
const refPath = line.slice(7);
|
|
1736
|
+
current.branch = refPath.replace("refs/heads/", "");
|
|
1737
|
+
} else if (line === "locked") {
|
|
1738
|
+
current.locked = true;
|
|
1739
|
+
} else if (line === "prunable") {
|
|
1740
|
+
current.prunable = true;
|
|
1741
|
+
} else if (line.startsWith("detached")) {
|
|
1742
|
+
current.branch = "HEAD (detached)";
|
|
1743
|
+
} else if (line === "" && current.path) {
|
|
1744
|
+
worktrees.push({
|
|
1745
|
+
path: current.path,
|
|
1746
|
+
branch: current.branch || "unknown",
|
|
1747
|
+
commit: current.commit || "",
|
|
1748
|
+
locked: current.locked,
|
|
1749
|
+
prunable: current.prunable
|
|
1750
|
+
});
|
|
1751
|
+
current = {};
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
if (current.path) {
|
|
1755
|
+
worktrees.push({
|
|
1756
|
+
path: current.path,
|
|
1757
|
+
branch: current.branch || "unknown",
|
|
1758
|
+
commit: current.commit || "",
|
|
1759
|
+
locked: current.locked,
|
|
1760
|
+
prunable: current.prunable
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
return {
|
|
1764
|
+
success: true,
|
|
1765
|
+
output: `Found ${worktrees.length} worktree(s)`,
|
|
1766
|
+
details: {
|
|
1767
|
+
worktrees
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
} catch (error) {
|
|
1771
|
+
return {
|
|
1772
|
+
success: false,
|
|
1773
|
+
error: "UNKNOWN_ERROR",
|
|
1774
|
+
output: error.message || "Failed to list worktrees"
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* EP944: Prune stale worktrees
|
|
1780
|
+
* Removes worktree administrative files for worktrees whose directories are missing
|
|
1781
|
+
*/
|
|
1782
|
+
async executeWorktreePrune(cwd, options) {
|
|
1783
|
+
try {
|
|
1784
|
+
const listResult = await this.executeWorktreeList(cwd, options);
|
|
1785
|
+
const prunableCount = listResult.details?.worktrees?.filter((w) => w.prunable).length || 0;
|
|
1786
|
+
const result = await this.runGitCommand(["worktree", "prune"], cwd, options);
|
|
1787
|
+
if (result.success) {
|
|
1788
|
+
return {
|
|
1789
|
+
success: true,
|
|
1790
|
+
output: prunableCount > 0 ? `Pruned ${prunableCount} stale worktree(s)` : "No stale worktrees to prune",
|
|
1791
|
+
details: {
|
|
1792
|
+
prunedCount: prunableCount
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
return result;
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
return {
|
|
1799
|
+
success: false,
|
|
1800
|
+
error: "UNKNOWN_ERROR",
|
|
1801
|
+
output: error.message || "Failed to prune worktrees"
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* EP944: Clone a repository as a bare repository
|
|
1807
|
+
* Used for worktree-based development setup
|
|
1808
|
+
*/
|
|
1809
|
+
async executeCloneBare(command, options) {
|
|
1810
|
+
try {
|
|
1811
|
+
const fs14 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1812
|
+
const path15 = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1813
|
+
try {
|
|
1814
|
+
await fs14.access(command.path);
|
|
1815
|
+
return {
|
|
1816
|
+
success: false,
|
|
1817
|
+
error: "BRANCH_ALREADY_EXISTS",
|
|
1818
|
+
// Reusing for path exists
|
|
1819
|
+
output: `Directory already exists at path: ${command.path}`
|
|
1820
|
+
};
|
|
1821
|
+
} catch {
|
|
1822
|
+
}
|
|
1823
|
+
const parentDir = path15.dirname(command.path);
|
|
1824
|
+
try {
|
|
1825
|
+
await fs14.mkdir(parentDir, { recursive: true });
|
|
1826
|
+
} catch {
|
|
1827
|
+
}
|
|
1828
|
+
const { stdout, stderr } = await execAsync2(
|
|
1829
|
+
`git clone --bare "${command.url}" "${command.path}"`,
|
|
1830
|
+
{ timeout: options?.timeout || 12e4 }
|
|
1831
|
+
// 2 minutes for clone
|
|
1832
|
+
);
|
|
1833
|
+
return {
|
|
1834
|
+
success: true,
|
|
1835
|
+
output: `Cloned bare repository to ${command.path}`,
|
|
1836
|
+
details: {
|
|
1837
|
+
worktreePath: command.path
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
if (error.message?.includes("Authentication") || error.message?.includes("Permission denied")) {
|
|
1842
|
+
return {
|
|
1843
|
+
success: false,
|
|
1844
|
+
error: "AUTH_FAILURE",
|
|
1845
|
+
output: "Authentication failed. Please check your credentials."
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
if (error.message?.includes("Could not resolve") || error.message?.includes("unable to access")) {
|
|
1849
|
+
return {
|
|
1850
|
+
success: false,
|
|
1851
|
+
error: "NETWORK_ERROR",
|
|
1852
|
+
output: "Network error. Please check your connection."
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
return {
|
|
1856
|
+
success: false,
|
|
1857
|
+
error: "UNKNOWN_ERROR",
|
|
1858
|
+
output: error.message || "Failed to clone repository"
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* EP944: Get project info including worktree mode
|
|
1864
|
+
* Returns information about the project configuration
|
|
1865
|
+
*/
|
|
1866
|
+
async executeProjectInfo(cwd, options) {
|
|
1867
|
+
try {
|
|
1868
|
+
const fs14 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1869
|
+
const path15 = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1870
|
+
let currentPath = cwd;
|
|
1871
|
+
let worktreeMode = false;
|
|
1872
|
+
let projectPath = cwd;
|
|
1873
|
+
let bareRepoPath;
|
|
1874
|
+
for (let i = 0; i < 10; i++) {
|
|
1875
|
+
const bareDir = path15.join(currentPath, ".bare");
|
|
1876
|
+
const episodaDir = path15.join(currentPath, ".episoda");
|
|
1877
|
+
try {
|
|
1878
|
+
await fs14.access(bareDir);
|
|
1879
|
+
await fs14.access(episodaDir);
|
|
1880
|
+
worktreeMode = true;
|
|
1881
|
+
projectPath = currentPath;
|
|
1882
|
+
bareRepoPath = bareDir;
|
|
1883
|
+
break;
|
|
1884
|
+
} catch {
|
|
1885
|
+
const parentPath = path15.dirname(currentPath);
|
|
1886
|
+
if (parentPath === currentPath) {
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
currentPath = parentPath;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
success: true,
|
|
1894
|
+
output: worktreeMode ? "Worktree mode project" : "Standard git project",
|
|
1895
|
+
details: {
|
|
1896
|
+
worktreeMode,
|
|
1897
|
+
projectPath,
|
|
1898
|
+
bareRepoPath
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
return {
|
|
1903
|
+
success: false,
|
|
1904
|
+
error: "UNKNOWN_ERROR",
|
|
1905
|
+
output: error.message || "Failed to get project info"
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1574
1909
|
/**
|
|
1575
1910
|
* Run a git command and return structured result
|
|
1576
1911
|
*/
|
|
@@ -1586,7 +1921,7 @@ var require_git_executor = __commonJS({
|
|
|
1586
1921
|
maxBuffer: 1024 * 1024 * 10
|
|
1587
1922
|
// 10MB buffer
|
|
1588
1923
|
};
|
|
1589
|
-
const { stdout, stderr } = await
|
|
1924
|
+
const { stdout, stderr } = await execAsync2(command, execOptions);
|
|
1590
1925
|
const output = (stdout + stderr).trim();
|
|
1591
1926
|
const details = {};
|
|
1592
1927
|
const branchName = (0, git_parser_1.extractBranchName)(output);
|
|
@@ -1637,7 +1972,7 @@ var require_git_executor = __commonJS({
|
|
|
1637
1972
|
*/
|
|
1638
1973
|
async validateGitInstalled() {
|
|
1639
1974
|
try {
|
|
1640
|
-
await
|
|
1975
|
+
await execAsync2("git --version", { timeout: 5e3 });
|
|
1641
1976
|
return true;
|
|
1642
1977
|
} catch {
|
|
1643
1978
|
return false;
|
|
@@ -1648,7 +1983,7 @@ var require_git_executor = __commonJS({
|
|
|
1648
1983
|
*/
|
|
1649
1984
|
async isGitRepository(cwd) {
|
|
1650
1985
|
try {
|
|
1651
|
-
await
|
|
1986
|
+
await execAsync2("git rev-parse --git-dir", { cwd, timeout: 5e3 });
|
|
1652
1987
|
return true;
|
|
1653
1988
|
} catch {
|
|
1654
1989
|
return false;
|
|
@@ -1660,7 +1995,7 @@ var require_git_executor = __commonJS({
|
|
|
1660
1995
|
*/
|
|
1661
1996
|
async detectWorkingDirectory(startPath) {
|
|
1662
1997
|
try {
|
|
1663
|
-
const { stdout } = await
|
|
1998
|
+
const { stdout } = await execAsync2("git rev-parse --show-toplevel", {
|
|
1664
1999
|
cwd: startPath || process.cwd(),
|
|
1665
2000
|
timeout: 5e3
|
|
1666
2001
|
});
|
|
@@ -1670,7 +2005,7 @@ var require_git_executor = __commonJS({
|
|
|
1670
2005
|
}
|
|
1671
2006
|
}
|
|
1672
2007
|
};
|
|
1673
|
-
exports2.GitExecutor =
|
|
2008
|
+
exports2.GitExecutor = GitExecutor3;
|
|
1674
2009
|
}
|
|
1675
2010
|
});
|
|
1676
2011
|
|
|
@@ -1761,7 +2096,7 @@ var require_websocket_client = __commonJS({
|
|
|
1761
2096
|
clearTimeout(this.reconnectTimeout);
|
|
1762
2097
|
this.reconnectTimeout = void 0;
|
|
1763
2098
|
}
|
|
1764
|
-
return new Promise((
|
|
2099
|
+
return new Promise((resolve3, reject) => {
|
|
1765
2100
|
const connectionTimeout = setTimeout(() => {
|
|
1766
2101
|
if (this.ws) {
|
|
1767
2102
|
this.ws.terminate();
|
|
@@ -1788,7 +2123,7 @@ var require_websocket_client = __commonJS({
|
|
|
1788
2123
|
daemonPid: this.daemonPid
|
|
1789
2124
|
});
|
|
1790
2125
|
this.startHeartbeat();
|
|
1791
|
-
|
|
2126
|
+
resolve3();
|
|
1792
2127
|
});
|
|
1793
2128
|
this.ws.on("pong", () => {
|
|
1794
2129
|
if (this.heartbeatTimeoutTimer) {
|
|
@@ -1904,13 +2239,13 @@ var require_websocket_client = __commonJS({
|
|
|
1904
2239
|
if (!this.ws || !this.isConnected) {
|
|
1905
2240
|
throw new Error("WebSocket not connected");
|
|
1906
2241
|
}
|
|
1907
|
-
return new Promise((
|
|
2242
|
+
return new Promise((resolve3, reject) => {
|
|
1908
2243
|
this.ws.send(JSON.stringify(message), (error) => {
|
|
1909
2244
|
if (error) {
|
|
1910
2245
|
console.error("[EpisodaClient] Failed to send message:", error);
|
|
1911
2246
|
reject(error);
|
|
1912
2247
|
} else {
|
|
1913
|
-
|
|
2248
|
+
resolve3();
|
|
1914
2249
|
}
|
|
1915
2250
|
});
|
|
1916
2251
|
});
|
|
@@ -2023,12 +2358,13 @@ var require_websocket_client = __commonJS({
|
|
|
2023
2358
|
console.log(`[EpisodaClient] Server restarting, reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/7)`);
|
|
2024
2359
|
}
|
|
2025
2360
|
} else {
|
|
2026
|
-
|
|
2027
|
-
|
|
2361
|
+
const MAX_NON_GRACEFUL_RETRIES = 5;
|
|
2362
|
+
if (this.reconnectAttempts >= MAX_NON_GRACEFUL_RETRIES) {
|
|
2363
|
+
console.error(`[EpisodaClient] Connection lost. Reconnection failed after ${MAX_NON_GRACEFUL_RETRIES} attempts. Check server status or restart with "episoda dev".`);
|
|
2028
2364
|
shouldRetry = false;
|
|
2029
2365
|
} else {
|
|
2030
|
-
delay = 1e3;
|
|
2031
|
-
console.log(
|
|
2366
|
+
delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 16e3);
|
|
2367
|
+
console.log(`[EpisodaClient] Connection lost, retrying in ${delay / 1e3}s... (attempt ${this.reconnectAttempts + 1}/${MAX_NON_GRACEFUL_RETRIES})`);
|
|
2032
2368
|
}
|
|
2033
2369
|
}
|
|
2034
2370
|
if (!shouldRetry) {
|
|
@@ -2138,34 +2474,34 @@ var require_auth = __commonJS({
|
|
|
2138
2474
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
2139
2475
|
exports2.getConfigDir = getConfigDir6;
|
|
2140
2476
|
exports2.getConfigPath = getConfigPath;
|
|
2141
|
-
exports2.loadConfig =
|
|
2477
|
+
exports2.loadConfig = loadConfig5;
|
|
2142
2478
|
exports2.saveConfig = saveConfig2;
|
|
2143
2479
|
exports2.validateToken = validateToken;
|
|
2144
|
-
var
|
|
2145
|
-
var
|
|
2146
|
-
var
|
|
2480
|
+
var fs14 = __importStar(require("fs"));
|
|
2481
|
+
var path15 = __importStar(require("path"));
|
|
2482
|
+
var os6 = __importStar(require("os"));
|
|
2147
2483
|
var child_process_1 = require("child_process");
|
|
2148
2484
|
var DEFAULT_CONFIG_FILE = "config.json";
|
|
2149
2485
|
function getConfigDir6() {
|
|
2150
|
-
return process.env.EPISODA_CONFIG_DIR ||
|
|
2486
|
+
return process.env.EPISODA_CONFIG_DIR || path15.join(os6.homedir(), ".episoda");
|
|
2151
2487
|
}
|
|
2152
2488
|
function getConfigPath(configPath) {
|
|
2153
2489
|
if (configPath) {
|
|
2154
2490
|
return configPath;
|
|
2155
2491
|
}
|
|
2156
|
-
return
|
|
2492
|
+
return path15.join(getConfigDir6(), DEFAULT_CONFIG_FILE);
|
|
2157
2493
|
}
|
|
2158
2494
|
function ensureConfigDir(configPath) {
|
|
2159
|
-
const dir =
|
|
2160
|
-
const isNew = !
|
|
2495
|
+
const dir = path15.dirname(configPath);
|
|
2496
|
+
const isNew = !fs14.existsSync(dir);
|
|
2161
2497
|
if (isNew) {
|
|
2162
|
-
|
|
2498
|
+
fs14.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2163
2499
|
}
|
|
2164
2500
|
if (process.platform === "darwin") {
|
|
2165
|
-
const nosyncPath =
|
|
2166
|
-
if (isNew || !
|
|
2501
|
+
const nosyncPath = path15.join(dir, ".nosync");
|
|
2502
|
+
if (isNew || !fs14.existsSync(nosyncPath)) {
|
|
2167
2503
|
try {
|
|
2168
|
-
|
|
2504
|
+
fs14.writeFileSync(nosyncPath, "", { mode: 384 });
|
|
2169
2505
|
(0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
|
|
2170
2506
|
stdio: "ignore",
|
|
2171
2507
|
timeout: 5e3
|
|
@@ -2175,13 +2511,13 @@ var require_auth = __commonJS({
|
|
|
2175
2511
|
}
|
|
2176
2512
|
}
|
|
2177
2513
|
}
|
|
2178
|
-
async function
|
|
2514
|
+
async function loadConfig5(configPath) {
|
|
2179
2515
|
const fullPath = getConfigPath(configPath);
|
|
2180
|
-
if (!
|
|
2516
|
+
if (!fs14.existsSync(fullPath)) {
|
|
2181
2517
|
return null;
|
|
2182
2518
|
}
|
|
2183
2519
|
try {
|
|
2184
|
-
const content =
|
|
2520
|
+
const content = fs14.readFileSync(fullPath, "utf8");
|
|
2185
2521
|
const config = JSON.parse(content);
|
|
2186
2522
|
return config;
|
|
2187
2523
|
} catch (error) {
|
|
@@ -2194,7 +2530,7 @@ var require_auth = __commonJS({
|
|
|
2194
2530
|
ensureConfigDir(fullPath);
|
|
2195
2531
|
try {
|
|
2196
2532
|
const content = JSON.stringify(config, null, 2);
|
|
2197
|
-
|
|
2533
|
+
fs14.writeFileSync(fullPath, content, { mode: 384 });
|
|
2198
2534
|
} catch (error) {
|
|
2199
2535
|
throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
|
|
2200
2536
|
}
|
|
@@ -2234,6 +2570,11 @@ var require_errors = __commonJS({
|
|
|
2234
2570
|
"BRANCH_ALREADY_EXISTS": "Branch already exists",
|
|
2235
2571
|
"PUSH_REJECTED": "Push rejected by remote",
|
|
2236
2572
|
"COMMAND_TIMEOUT": "Command timed out",
|
|
2573
|
+
// EP944: Worktree error messages
|
|
2574
|
+
"WORKTREE_EXISTS": "Worktree already exists at this path",
|
|
2575
|
+
"WORKTREE_NOT_FOUND": "Worktree not found at this path",
|
|
2576
|
+
"WORKTREE_LOCKED": "Worktree is locked",
|
|
2577
|
+
"BRANCH_IN_USE": "Branch is already checked out in another worktree",
|
|
2237
2578
|
"UNKNOWN_ERROR": "Unknown error occurred"
|
|
2238
2579
|
};
|
|
2239
2580
|
let message = messages[code] || `Error: ${code}`;
|
|
@@ -2305,18 +2646,18 @@ __export(port_check_exports, {
|
|
|
2305
2646
|
isPortInUse: () => isPortInUse
|
|
2306
2647
|
});
|
|
2307
2648
|
async function isPortInUse(port) {
|
|
2308
|
-
return new Promise((
|
|
2649
|
+
return new Promise((resolve3) => {
|
|
2309
2650
|
const server = net2.createServer();
|
|
2310
2651
|
server.once("error", (err) => {
|
|
2311
2652
|
if (err.code === "EADDRINUSE") {
|
|
2312
|
-
|
|
2653
|
+
resolve3(true);
|
|
2313
2654
|
} else {
|
|
2314
|
-
|
|
2655
|
+
resolve3(false);
|
|
2315
2656
|
}
|
|
2316
2657
|
});
|
|
2317
2658
|
server.once("listening", () => {
|
|
2318
2659
|
server.close();
|
|
2319
|
-
|
|
2660
|
+
resolve3(false);
|
|
2320
2661
|
});
|
|
2321
2662
|
server.listen(port);
|
|
2322
2663
|
});
|
|
@@ -2340,7 +2681,7 @@ var require_package = __commonJS({
|
|
|
2340
2681
|
"package.json"(exports2, module2) {
|
|
2341
2682
|
module2.exports = {
|
|
2342
2683
|
name: "episoda",
|
|
2343
|
-
version: "0.2.
|
|
2684
|
+
version: "0.2.20",
|
|
2344
2685
|
description: "CLI tool for Episoda local development workflow orchestration",
|
|
2345
2686
|
main: "dist/index.js",
|
|
2346
2687
|
types: "dist/index.d.ts",
|
|
@@ -2535,13 +2876,19 @@ function writeProjects(data) {
|
|
|
2535
2876
|
throw new Error(`Failed to write projects.json: ${error}`);
|
|
2536
2877
|
}
|
|
2537
2878
|
}
|
|
2538
|
-
function addProject(projectId, projectPath) {
|
|
2879
|
+
function addProject(projectId, projectPath, options) {
|
|
2539
2880
|
const data = readProjects();
|
|
2540
2881
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2541
2882
|
const existingByPath = data.projects.find((p) => p.path === projectPath);
|
|
2542
2883
|
if (existingByPath) {
|
|
2543
2884
|
existingByPath.id = projectId;
|
|
2544
2885
|
existingByPath.last_active = now;
|
|
2886
|
+
if (options?.worktreeMode !== void 0) {
|
|
2887
|
+
existingByPath.worktreeMode = options.worktreeMode;
|
|
2888
|
+
}
|
|
2889
|
+
if (options?.bareRepoPath) {
|
|
2890
|
+
existingByPath.bareRepoPath = options.bareRepoPath;
|
|
2891
|
+
}
|
|
2545
2892
|
writeProjects(data);
|
|
2546
2893
|
return existingByPath;
|
|
2547
2894
|
}
|
|
@@ -2557,7 +2904,10 @@ function addProject(projectId, projectPath) {
|
|
|
2557
2904
|
path: projectPath,
|
|
2558
2905
|
name: projectName,
|
|
2559
2906
|
added_at: now,
|
|
2560
|
-
last_active: now
|
|
2907
|
+
last_active: now,
|
|
2908
|
+
// EP944: Worktree mode fields
|
|
2909
|
+
worktreeMode: options?.worktreeMode,
|
|
2910
|
+
bareRepoPath: options?.bareRepoPath
|
|
2561
2911
|
};
|
|
2562
2912
|
data.projects.push(newProject);
|
|
2563
2913
|
writeProjects(data);
|
|
@@ -2630,10 +2980,10 @@ var IPCServer = class {
|
|
|
2630
2980
|
this.server = net.createServer((socket) => {
|
|
2631
2981
|
this.handleConnection(socket);
|
|
2632
2982
|
});
|
|
2633
|
-
return new Promise((
|
|
2983
|
+
return new Promise((resolve3, reject) => {
|
|
2634
2984
|
this.server.listen(socketPath, () => {
|
|
2635
2985
|
fs3.chmodSync(socketPath, 384);
|
|
2636
|
-
|
|
2986
|
+
resolve3();
|
|
2637
2987
|
});
|
|
2638
2988
|
this.server.on("error", reject);
|
|
2639
2989
|
});
|
|
@@ -2644,12 +2994,12 @@ var IPCServer = class {
|
|
|
2644
2994
|
async stop() {
|
|
2645
2995
|
if (!this.server) return;
|
|
2646
2996
|
const socketPath = getSocketPath();
|
|
2647
|
-
return new Promise((
|
|
2997
|
+
return new Promise((resolve3) => {
|
|
2648
2998
|
this.server.close(() => {
|
|
2649
2999
|
if (fs3.existsSync(socketPath)) {
|
|
2650
3000
|
fs3.unlinkSync(socketPath);
|
|
2651
3001
|
}
|
|
2652
|
-
|
|
3002
|
+
resolve3();
|
|
2653
3003
|
});
|
|
2654
3004
|
});
|
|
2655
3005
|
}
|
|
@@ -2711,7 +3061,7 @@ var IPCServer = class {
|
|
|
2711
3061
|
};
|
|
2712
3062
|
|
|
2713
3063
|
// src/daemon/daemon-process.ts
|
|
2714
|
-
var
|
|
3064
|
+
var import_core10 = __toESM(require_dist());
|
|
2715
3065
|
|
|
2716
3066
|
// src/utils/update-checker.ts
|
|
2717
3067
|
var import_child_process2 = require("child_process");
|
|
@@ -3300,7 +3650,7 @@ async function handleExec(command, projectPath) {
|
|
|
3300
3650
|
env = {}
|
|
3301
3651
|
} = command;
|
|
3302
3652
|
const effectiveTimeout = Math.min(Math.max(timeout, 1e3), MAX_TIMEOUT);
|
|
3303
|
-
return new Promise((
|
|
3653
|
+
return new Promise((resolve3) => {
|
|
3304
3654
|
let stdout = "";
|
|
3305
3655
|
let stderr = "";
|
|
3306
3656
|
let timedOut = false;
|
|
@@ -3308,7 +3658,7 @@ async function handleExec(command, projectPath) {
|
|
|
3308
3658
|
const done = (result) => {
|
|
3309
3659
|
if (resolved) return;
|
|
3310
3660
|
resolved = true;
|
|
3311
|
-
|
|
3661
|
+
resolve3(result);
|
|
3312
3662
|
};
|
|
3313
3663
|
try {
|
|
3314
3664
|
const proc = (0, import_child_process3.spawn)(cmd, {
|
|
@@ -3371,8 +3721,103 @@ async function handleExec(command, projectPath) {
|
|
|
3371
3721
|
});
|
|
3372
3722
|
}
|
|
3373
3723
|
|
|
3374
|
-
// src/
|
|
3724
|
+
// src/daemon/handlers/stale-commit-cleanup.ts
|
|
3375
3725
|
var import_child_process4 = require("child_process");
|
|
3726
|
+
var import_util = require("util");
|
|
3727
|
+
var import_core5 = __toESM(require_dist());
|
|
3728
|
+
var execAsync = (0, import_util.promisify)(import_child_process4.exec);
|
|
3729
|
+
async function cleanupStaleCommits(projectPath) {
|
|
3730
|
+
try {
|
|
3731
|
+
const machineId = await getMachineId();
|
|
3732
|
+
const config = await (0, import_core5.loadConfig)();
|
|
3733
|
+
if (!config?.access_token) {
|
|
3734
|
+
return {
|
|
3735
|
+
success: false,
|
|
3736
|
+
deleted_count: 0,
|
|
3737
|
+
kept_count: 0,
|
|
3738
|
+
message: "No access token available"
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
try {
|
|
3742
|
+
await execAsync("git fetch origin", { cwd: projectPath, timeout: 3e4 });
|
|
3743
|
+
} catch (fetchError) {
|
|
3744
|
+
console.warn("[EP950] Could not fetch origin:", fetchError);
|
|
3745
|
+
}
|
|
3746
|
+
let currentBranch = "";
|
|
3747
|
+
try {
|
|
3748
|
+
const { stdout } = await execAsync("git branch --show-current", { cwd: projectPath, timeout: 5e3 });
|
|
3749
|
+
currentBranch = stdout.trim();
|
|
3750
|
+
} catch {
|
|
3751
|
+
return {
|
|
3752
|
+
success: false,
|
|
3753
|
+
deleted_count: 0,
|
|
3754
|
+
kept_count: 0,
|
|
3755
|
+
message: "Could not determine current branch"
|
|
3756
|
+
};
|
|
3757
|
+
}
|
|
3758
|
+
if (currentBranch !== "main" && currentBranch !== "master") {
|
|
3759
|
+
return {
|
|
3760
|
+
success: true,
|
|
3761
|
+
deleted_count: 0,
|
|
3762
|
+
kept_count: 0,
|
|
3763
|
+
message: `Not on main branch (on ${currentBranch}), skipping cleanup`
|
|
3764
|
+
};
|
|
3765
|
+
}
|
|
3766
|
+
let validShas = [];
|
|
3767
|
+
try {
|
|
3768
|
+
const { stdout } = await execAsync(
|
|
3769
|
+
"git log origin/main..HEAD --format=%H",
|
|
3770
|
+
{ cwd: projectPath, timeout: 1e4 }
|
|
3771
|
+
);
|
|
3772
|
+
validShas = stdout.trim().split("\n").filter(Boolean);
|
|
3773
|
+
} catch {
|
|
3774
|
+
validShas = [];
|
|
3775
|
+
}
|
|
3776
|
+
const apiUrl = config.api_url || "https://episoda.dev";
|
|
3777
|
+
const response = await fetch(`${apiUrl}/api/commits/local/batch`, {
|
|
3778
|
+
method: "POST",
|
|
3779
|
+
headers: {
|
|
3780
|
+
"Authorization": `Bearer ${config.access_token}`,
|
|
3781
|
+
"Content-Type": "application/json"
|
|
3782
|
+
},
|
|
3783
|
+
body: JSON.stringify({
|
|
3784
|
+
machine_id: machineId,
|
|
3785
|
+
valid_shas: validShas,
|
|
3786
|
+
branch: "main"
|
|
3787
|
+
})
|
|
3788
|
+
});
|
|
3789
|
+
if (!response.ok) {
|
|
3790
|
+
const errorData = await response.json().catch(() => ({}));
|
|
3791
|
+
return {
|
|
3792
|
+
success: false,
|
|
3793
|
+
deleted_count: 0,
|
|
3794
|
+
kept_count: 0,
|
|
3795
|
+
message: `API error: ${errorData.error?.message || response.statusText}`
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
const result = await response.json();
|
|
3799
|
+
if (result.deleted_count && result.deleted_count > 0) {
|
|
3800
|
+
console.log(`[EP950] Cleaned up ${result.deleted_count} stale commit record(s)`);
|
|
3801
|
+
}
|
|
3802
|
+
return {
|
|
3803
|
+
success: true,
|
|
3804
|
+
deleted_count: result.deleted_count || 0,
|
|
3805
|
+
kept_count: result.kept_count || 0,
|
|
3806
|
+
message: result.message || "Cleanup completed"
|
|
3807
|
+
};
|
|
3808
|
+
} catch (error) {
|
|
3809
|
+
console.error("[EP950] Error during stale commit cleanup:", error);
|
|
3810
|
+
return {
|
|
3811
|
+
success: false,
|
|
3812
|
+
deleted_count: 0,
|
|
3813
|
+
kept_count: 0,
|
|
3814
|
+
message: error.message || "Cleanup failed"
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
// src/tunnel/cloudflared-manager.ts
|
|
3820
|
+
var import_child_process5 = require("child_process");
|
|
3376
3821
|
var fs5 = __toESM(require("fs"));
|
|
3377
3822
|
var path6 = __toESM(require("path"));
|
|
3378
3823
|
var os = __toESM(require("os"));
|
|
@@ -3403,7 +3848,7 @@ function isCloudflaredInPath() {
|
|
|
3403
3848
|
try {
|
|
3404
3849
|
const command = os.platform() === "win32" ? "where" : "which";
|
|
3405
3850
|
const binaryName = os.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
3406
|
-
const result = (0,
|
|
3851
|
+
const result = (0, import_child_process5.spawnSync)(command, [binaryName], { encoding: "utf-8" });
|
|
3407
3852
|
if (result.status === 0 && result.stdout.trim()) {
|
|
3408
3853
|
return result.stdout.trim().split("\n")[0].trim();
|
|
3409
3854
|
}
|
|
@@ -3422,7 +3867,7 @@ function isCloudflaredInstalled() {
|
|
|
3422
3867
|
}
|
|
3423
3868
|
function verifyCloudflared(binaryPath) {
|
|
3424
3869
|
try {
|
|
3425
|
-
const result = (0,
|
|
3870
|
+
const result = (0, import_child_process5.spawnSync)(binaryPath, ["version"], { encoding: "utf-8", timeout: 5e3 });
|
|
3426
3871
|
return result.status === 0 && result.stdout.includes("cloudflared");
|
|
3427
3872
|
} catch {
|
|
3428
3873
|
return false;
|
|
@@ -3438,7 +3883,7 @@ function getDownloadUrl() {
|
|
|
3438
3883
|
return platformUrls[arch3] || null;
|
|
3439
3884
|
}
|
|
3440
3885
|
async function downloadFile(url, destPath) {
|
|
3441
|
-
return new Promise((
|
|
3886
|
+
return new Promise((resolve3, reject) => {
|
|
3442
3887
|
const followRedirect = (currentUrl, redirectCount = 0) => {
|
|
3443
3888
|
if (redirectCount > 5) {
|
|
3444
3889
|
reject(new Error("Too many redirects"));
|
|
@@ -3468,7 +3913,7 @@ async function downloadFile(url, destPath) {
|
|
|
3468
3913
|
response.pipe(file);
|
|
3469
3914
|
file.on("finish", () => {
|
|
3470
3915
|
file.close();
|
|
3471
|
-
|
|
3916
|
+
resolve3();
|
|
3472
3917
|
});
|
|
3473
3918
|
file.on("error", (err) => {
|
|
3474
3919
|
fs5.unlinkSync(destPath);
|
|
@@ -3527,7 +3972,7 @@ async function ensureCloudflared() {
|
|
|
3527
3972
|
}
|
|
3528
3973
|
|
|
3529
3974
|
// src/tunnel/tunnel-manager.ts
|
|
3530
|
-
var
|
|
3975
|
+
var import_child_process6 = require("child_process");
|
|
3531
3976
|
var import_events = require("events");
|
|
3532
3977
|
var fs6 = __toESM(require("fs"));
|
|
3533
3978
|
var path7 = __toESM(require("path"));
|
|
@@ -3644,7 +4089,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3644
4089
|
*/
|
|
3645
4090
|
findCloudflaredProcesses() {
|
|
3646
4091
|
try {
|
|
3647
|
-
const output = (0,
|
|
4092
|
+
const output = (0, import_child_process6.execSync)("pgrep -f cloudflared", { encoding: "utf8" });
|
|
3648
4093
|
return output.trim().split("\n").map((pid) => parseInt(pid, 10)).filter((pid) => !isNaN(pid));
|
|
3649
4094
|
} catch {
|
|
3650
4095
|
return [];
|
|
@@ -3656,7 +4101,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3656
4101
|
*/
|
|
3657
4102
|
getProcessPort(pid) {
|
|
3658
4103
|
try {
|
|
3659
|
-
const output = (0,
|
|
4104
|
+
const output = (0, import_child_process6.execSync)(`ps -p ${pid} -o args=`, { encoding: "utf8" }).trim();
|
|
3660
4105
|
const portMatch = output.match(/--url\s+https?:\/\/localhost:(\d+)/);
|
|
3661
4106
|
if (portMatch) {
|
|
3662
4107
|
return parseInt(portMatch[1], 10);
|
|
@@ -3684,10 +4129,10 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3684
4129
|
const isTracked = Array.from(this.tunnelStates.values()).some((s) => s.info.pid === pid);
|
|
3685
4130
|
console.log(`[Tunnel] EP904: Found cloudflared PID ${pid} on port ${port} (tracked: ${isTracked})`);
|
|
3686
4131
|
this.killByPid(pid, "SIGTERM");
|
|
3687
|
-
await new Promise((
|
|
4132
|
+
await new Promise((resolve3) => setTimeout(resolve3, 500));
|
|
3688
4133
|
if (this.isProcessRunning(pid)) {
|
|
3689
4134
|
this.killByPid(pid, "SIGKILL");
|
|
3690
|
-
await new Promise((
|
|
4135
|
+
await new Promise((resolve3) => setTimeout(resolve3, 200));
|
|
3691
4136
|
}
|
|
3692
4137
|
killed.push(pid);
|
|
3693
4138
|
}
|
|
@@ -3713,7 +4158,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3713
4158
|
if (!this.tunnelStates.has(moduleUid)) {
|
|
3714
4159
|
console.log(`[Tunnel] EP877: Found orphaned process PID ${pid} for ${moduleUid}, killing...`);
|
|
3715
4160
|
this.killByPid(pid, "SIGTERM");
|
|
3716
|
-
await new Promise((
|
|
4161
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
3717
4162
|
if (this.isProcessRunning(pid)) {
|
|
3718
4163
|
this.killByPid(pid, "SIGKILL");
|
|
3719
4164
|
}
|
|
@@ -3728,7 +4173,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3728
4173
|
if (!trackedPids.includes(pid) && !cleaned.includes(pid)) {
|
|
3729
4174
|
console.log(`[Tunnel] EP877: Found untracked cloudflared process PID ${pid}, killing...`);
|
|
3730
4175
|
this.killByPid(pid, "SIGTERM");
|
|
3731
|
-
await new Promise((
|
|
4176
|
+
await new Promise((resolve3) => setTimeout(resolve3, 500));
|
|
3732
4177
|
if (this.isProcessRunning(pid)) {
|
|
3733
4178
|
this.killByPid(pid, "SIGKILL");
|
|
3734
4179
|
}
|
|
@@ -3815,7 +4260,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3815
4260
|
return { success: false, error: `Failed to get cloudflared: ${errorMessage}` };
|
|
3816
4261
|
}
|
|
3817
4262
|
}
|
|
3818
|
-
return new Promise((
|
|
4263
|
+
return new Promise((resolve3) => {
|
|
3819
4264
|
const tunnelInfo = {
|
|
3820
4265
|
moduleUid,
|
|
3821
4266
|
url: "",
|
|
@@ -3825,7 +4270,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3825
4270
|
process: null
|
|
3826
4271
|
// Will be set below
|
|
3827
4272
|
};
|
|
3828
|
-
const process2 = (0,
|
|
4273
|
+
const process2 = (0, import_child_process6.spawn)(this.cloudflaredPath, [
|
|
3829
4274
|
"tunnel",
|
|
3830
4275
|
"--url",
|
|
3831
4276
|
`http://localhost:${port}`
|
|
@@ -3863,7 +4308,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3863
4308
|
moduleUid,
|
|
3864
4309
|
url: tunnelInfo.url
|
|
3865
4310
|
});
|
|
3866
|
-
|
|
4311
|
+
resolve3({ success: true, url: tunnelInfo.url });
|
|
3867
4312
|
}
|
|
3868
4313
|
};
|
|
3869
4314
|
process2.stdout?.on("data", (data) => {
|
|
@@ -3889,7 +4334,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3889
4334
|
onStatusChange?.("error", errorMsg);
|
|
3890
4335
|
this.emitEvent({ type: "error", moduleUid, error: errorMsg });
|
|
3891
4336
|
}
|
|
3892
|
-
|
|
4337
|
+
resolve3({ success: false, error: errorMsg });
|
|
3893
4338
|
} else if (wasConnected) {
|
|
3894
4339
|
if (currentState && !currentState.intentionallyStopped) {
|
|
3895
4340
|
console.log(`[Tunnel] ${moduleUid} crashed unexpectedly, attempting reconnect...`);
|
|
@@ -3914,7 +4359,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3914
4359
|
this.emitEvent({ type: "error", moduleUid, error: error.message });
|
|
3915
4360
|
}
|
|
3916
4361
|
if (!urlFound) {
|
|
3917
|
-
|
|
4362
|
+
resolve3({ success: false, error: error.message });
|
|
3918
4363
|
}
|
|
3919
4364
|
});
|
|
3920
4365
|
setTimeout(() => {
|
|
@@ -3931,7 +4376,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3931
4376
|
onStatusChange?.("error", errorMsg);
|
|
3932
4377
|
this.emitEvent({ type: "error", moduleUid, error: errorMsg });
|
|
3933
4378
|
}
|
|
3934
|
-
|
|
4379
|
+
resolve3({ success: false, error: errorMsg });
|
|
3935
4380
|
}
|
|
3936
4381
|
}, 3e4);
|
|
3937
4382
|
});
|
|
@@ -3974,7 +4419,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3974
4419
|
if (orphanPid && this.isProcessRunning(orphanPid)) {
|
|
3975
4420
|
console.log(`[Tunnel] EP877: Killing orphaned process ${orphanPid} for ${moduleUid} before starting new tunnel`);
|
|
3976
4421
|
this.killByPid(orphanPid, "SIGTERM");
|
|
3977
|
-
await new Promise((
|
|
4422
|
+
await new Promise((resolve3) => setTimeout(resolve3, 500));
|
|
3978
4423
|
if (this.isProcessRunning(orphanPid)) {
|
|
3979
4424
|
this.killByPid(orphanPid, "SIGKILL");
|
|
3980
4425
|
}
|
|
@@ -3983,7 +4428,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3983
4428
|
const killedOnPort = await this.killCloudflaredOnPort(port);
|
|
3984
4429
|
if (killedOnPort.length > 0) {
|
|
3985
4430
|
console.log(`[Tunnel] EP904: Pre-start port cleanup killed ${killedOnPort.length} process(es) on port ${port}`);
|
|
3986
|
-
await new Promise((
|
|
4431
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
3987
4432
|
}
|
|
3988
4433
|
const cleanup = await this.cleanupOrphanedProcesses();
|
|
3989
4434
|
if (cleanup.cleaned > 0) {
|
|
@@ -4003,7 +4448,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4003
4448
|
if (orphanPid && this.isProcessRunning(orphanPid)) {
|
|
4004
4449
|
console.log(`[Tunnel] EP877: Stopping orphaned process ${orphanPid} for ${moduleUid} via PID file`);
|
|
4005
4450
|
this.killByPid(orphanPid, "SIGTERM");
|
|
4006
|
-
await new Promise((
|
|
4451
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
4007
4452
|
if (this.isProcessRunning(orphanPid)) {
|
|
4008
4453
|
this.killByPid(orphanPid, "SIGKILL");
|
|
4009
4454
|
}
|
|
@@ -4019,16 +4464,16 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4019
4464
|
const tunnel = state.info;
|
|
4020
4465
|
if (tunnel.process && !tunnel.process.killed) {
|
|
4021
4466
|
tunnel.process.kill("SIGTERM");
|
|
4022
|
-
await new Promise((
|
|
4467
|
+
await new Promise((resolve3) => {
|
|
4023
4468
|
const timeout = setTimeout(() => {
|
|
4024
4469
|
if (tunnel.process && !tunnel.process.killed) {
|
|
4025
4470
|
tunnel.process.kill("SIGKILL");
|
|
4026
4471
|
}
|
|
4027
|
-
|
|
4472
|
+
resolve3();
|
|
4028
4473
|
}, 3e3);
|
|
4029
4474
|
tunnel.process.once("exit", () => {
|
|
4030
4475
|
clearTimeout(timeout);
|
|
4031
|
-
|
|
4476
|
+
resolve3();
|
|
4032
4477
|
});
|
|
4033
4478
|
});
|
|
4034
4479
|
}
|
|
@@ -4067,6 +4512,12 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4067
4512
|
hasTunnel(moduleUid) {
|
|
4068
4513
|
return this.tunnelStates.has(moduleUid);
|
|
4069
4514
|
}
|
|
4515
|
+
/**
|
|
4516
|
+
* EP956: Get all module UIDs with active tunnels
|
|
4517
|
+
*/
|
|
4518
|
+
getActiveModuleUids() {
|
|
4519
|
+
return Array.from(this.tunnelStates.keys());
|
|
4520
|
+
}
|
|
4070
4521
|
/**
|
|
4071
4522
|
* Get the URL for an active tunnel
|
|
4072
4523
|
*/
|
|
@@ -4083,12 +4534,12 @@ function getTunnelManager() {
|
|
|
4083
4534
|
}
|
|
4084
4535
|
|
|
4085
4536
|
// src/tunnel/tunnel-api.ts
|
|
4086
|
-
var
|
|
4537
|
+
var import_core6 = __toESM(require_dist());
|
|
4087
4538
|
async function clearTunnelUrl(moduleUid) {
|
|
4088
4539
|
if (!moduleUid || moduleUid === "LOCAL") {
|
|
4089
4540
|
return;
|
|
4090
4541
|
}
|
|
4091
|
-
const config = await (0,
|
|
4542
|
+
const config = await (0, import_core6.loadConfig)();
|
|
4092
4543
|
if (!config?.access_token) {
|
|
4093
4544
|
return;
|
|
4094
4545
|
}
|
|
@@ -4105,14 +4556,14 @@ async function clearTunnelUrl(moduleUid) {
|
|
|
4105
4556
|
}
|
|
4106
4557
|
|
|
4107
4558
|
// src/agent/claude-binary.ts
|
|
4108
|
-
var
|
|
4559
|
+
var import_child_process7 = require("child_process");
|
|
4109
4560
|
var path8 = __toESM(require("path"));
|
|
4110
4561
|
var fs7 = __toESM(require("fs"));
|
|
4111
4562
|
var cachedBinaryPath = null;
|
|
4112
4563
|
function isValidClaudeBinary(binaryPath) {
|
|
4113
4564
|
try {
|
|
4114
4565
|
fs7.accessSync(binaryPath, fs7.constants.X_OK);
|
|
4115
|
-
const version = (0,
|
|
4566
|
+
const version = (0, import_child_process7.execSync)(`"${binaryPath}" --version`, {
|
|
4116
4567
|
encoding: "utf-8",
|
|
4117
4568
|
timeout: 5e3,
|
|
4118
4569
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4131,7 +4582,7 @@ async function ensureClaudeBinary() {
|
|
|
4131
4582
|
return cachedBinaryPath;
|
|
4132
4583
|
}
|
|
4133
4584
|
try {
|
|
4134
|
-
const pathResult = (0,
|
|
4585
|
+
const pathResult = (0, import_child_process7.execSync)("which claude", {
|
|
4135
4586
|
encoding: "utf-8",
|
|
4136
4587
|
timeout: 5e3,
|
|
4137
4588
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4157,7 +4608,7 @@ async function ensureClaudeBinary() {
|
|
|
4157
4608
|
}
|
|
4158
4609
|
}
|
|
4159
4610
|
try {
|
|
4160
|
-
const npxResult = (0,
|
|
4611
|
+
const npxResult = (0, import_child_process7.execSync)("npx --yes @anthropic-ai/claude-code --version", {
|
|
4161
4612
|
encoding: "utf-8",
|
|
4162
4613
|
timeout: 3e4,
|
|
4163
4614
|
// npx might need to download
|
|
@@ -4176,7 +4627,7 @@ async function ensureClaudeBinary() {
|
|
|
4176
4627
|
}
|
|
4177
4628
|
|
|
4178
4629
|
// src/agent/agent-manager.ts
|
|
4179
|
-
var
|
|
4630
|
+
var import_child_process8 = require("child_process");
|
|
4180
4631
|
var path9 = __toESM(require("path"));
|
|
4181
4632
|
var fs8 = __toESM(require("fs"));
|
|
4182
4633
|
var os3 = __toESM(require("os"));
|
|
@@ -4320,7 +4771,7 @@ var AgentManager = class {
|
|
|
4320
4771
|
}, null, 2);
|
|
4321
4772
|
fs8.writeFileSync(credentialsPath, credentialsContent, { mode: 384 });
|
|
4322
4773
|
console.log("[AgentManager] EP936: Wrote OAuth credentials to ~/.claude/.credentials.json");
|
|
4323
|
-
const childProcess = (0,
|
|
4774
|
+
const childProcess = (0, import_child_process8.spawn)(spawnCmd, spawnArgs, {
|
|
4324
4775
|
cwd: session.projectPath,
|
|
4325
4776
|
env: {
|
|
4326
4777
|
...process.env,
|
|
@@ -4462,17 +4913,17 @@ var AgentManager = class {
|
|
|
4462
4913
|
if (process2 && !process2.killed) {
|
|
4463
4914
|
console.log(`[AgentManager] Stopping session ${sessionId}`);
|
|
4464
4915
|
process2.kill("SIGINT");
|
|
4465
|
-
await new Promise((
|
|
4916
|
+
await new Promise((resolve3) => {
|
|
4466
4917
|
const timeout = setTimeout(() => {
|
|
4467
4918
|
if (!process2.killed) {
|
|
4468
4919
|
console.log(`[AgentManager] Force killing session ${sessionId}`);
|
|
4469
4920
|
process2.kill("SIGTERM");
|
|
4470
4921
|
}
|
|
4471
|
-
|
|
4922
|
+
resolve3();
|
|
4472
4923
|
}, 5e3);
|
|
4473
4924
|
process2.once("exit", () => {
|
|
4474
4925
|
clearTimeout(timeout);
|
|
4475
|
-
|
|
4926
|
+
resolve3();
|
|
4476
4927
|
});
|
|
4477
4928
|
});
|
|
4478
4929
|
}
|
|
@@ -4564,9 +5015,9 @@ var AgentManager = class {
|
|
|
4564
5015
|
};
|
|
4565
5016
|
|
|
4566
5017
|
// src/utils/dev-server.ts
|
|
4567
|
-
var
|
|
5018
|
+
var import_child_process9 = require("child_process");
|
|
4568
5019
|
init_port_check();
|
|
4569
|
-
var
|
|
5020
|
+
var import_core7 = __toESM(require_dist());
|
|
4570
5021
|
var import_http = __toESM(require("http"));
|
|
4571
5022
|
var fs9 = __toESM(require("fs"));
|
|
4572
5023
|
var path10 = __toESM(require("path"));
|
|
@@ -4577,7 +5028,7 @@ var MAX_LOG_SIZE_BYTES = 5 * 1024 * 1024;
|
|
|
4577
5028
|
var NODE_MEMORY_LIMIT_MB = 2048;
|
|
4578
5029
|
var activeServers = /* @__PURE__ */ new Map();
|
|
4579
5030
|
function getLogsDir() {
|
|
4580
|
-
const logsDir = path10.join((0,
|
|
5031
|
+
const logsDir = path10.join((0, import_core7.getConfigDir)(), "logs");
|
|
4581
5032
|
if (!fs9.existsSync(logsDir)) {
|
|
4582
5033
|
fs9.mkdirSync(logsDir, { recursive: true });
|
|
4583
5034
|
}
|
|
@@ -4614,7 +5065,7 @@ function writeToLog(logPath, line, isError = false) {
|
|
|
4614
5065
|
}
|
|
4615
5066
|
}
|
|
4616
5067
|
async function isDevServerHealthy(port, timeoutMs = 5e3) {
|
|
4617
|
-
return new Promise((
|
|
5068
|
+
return new Promise((resolve3) => {
|
|
4618
5069
|
const req = import_http.default.request(
|
|
4619
5070
|
{
|
|
4620
5071
|
hostname: "localhost",
|
|
@@ -4624,22 +5075,22 @@ async function isDevServerHealthy(port, timeoutMs = 5e3) {
|
|
|
4624
5075
|
timeout: timeoutMs
|
|
4625
5076
|
},
|
|
4626
5077
|
(res) => {
|
|
4627
|
-
|
|
5078
|
+
resolve3(true);
|
|
4628
5079
|
}
|
|
4629
5080
|
);
|
|
4630
5081
|
req.on("error", () => {
|
|
4631
|
-
|
|
5082
|
+
resolve3(false);
|
|
4632
5083
|
});
|
|
4633
5084
|
req.on("timeout", () => {
|
|
4634
5085
|
req.destroy();
|
|
4635
|
-
|
|
5086
|
+
resolve3(false);
|
|
4636
5087
|
});
|
|
4637
5088
|
req.end();
|
|
4638
5089
|
});
|
|
4639
5090
|
}
|
|
4640
5091
|
async function killProcessOnPort(port) {
|
|
4641
5092
|
try {
|
|
4642
|
-
const result = (0,
|
|
5093
|
+
const result = (0, import_child_process9.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
|
|
4643
5094
|
if (!result) {
|
|
4644
5095
|
console.log(`[DevServer] EP929: No process found on port ${port}`);
|
|
4645
5096
|
return true;
|
|
@@ -4648,21 +5099,21 @@ async function killProcessOnPort(port) {
|
|
|
4648
5099
|
console.log(`[DevServer] EP929: Found ${pids.length} process(es) on port ${port}: ${pids.join(", ")}`);
|
|
4649
5100
|
for (const pid of pids) {
|
|
4650
5101
|
try {
|
|
4651
|
-
(0,
|
|
5102
|
+
(0, import_child_process9.execSync)(`kill -15 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
|
|
4652
5103
|
console.log(`[DevServer] EP929: Sent SIGTERM to PID ${pid}`);
|
|
4653
5104
|
} catch {
|
|
4654
5105
|
}
|
|
4655
5106
|
}
|
|
4656
|
-
await new Promise((
|
|
5107
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
4657
5108
|
for (const pid of pids) {
|
|
4658
5109
|
try {
|
|
4659
|
-
(0,
|
|
4660
|
-
(0,
|
|
5110
|
+
(0, import_child_process9.execSync)(`kill -0 ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
5111
|
+
(0, import_child_process9.execSync)(`kill -9 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
|
|
4661
5112
|
console.log(`[DevServer] EP929: Force killed PID ${pid}`);
|
|
4662
5113
|
} catch {
|
|
4663
5114
|
}
|
|
4664
5115
|
}
|
|
4665
|
-
await new Promise((
|
|
5116
|
+
await new Promise((resolve3) => setTimeout(resolve3, 500));
|
|
4666
5117
|
const stillInUse = await isPortInUse(port);
|
|
4667
5118
|
if (stillInUse) {
|
|
4668
5119
|
console.error(`[DevServer] EP929: Port ${port} still in use after kill attempts`);
|
|
@@ -4682,7 +5133,7 @@ async function waitForPort(port, timeoutMs = 3e4) {
|
|
|
4682
5133
|
if (await isPortInUse(port)) {
|
|
4683
5134
|
return true;
|
|
4684
5135
|
}
|
|
4685
|
-
await new Promise((
|
|
5136
|
+
await new Promise((resolve3) => setTimeout(resolve3, checkInterval));
|
|
4686
5137
|
}
|
|
4687
5138
|
return false;
|
|
4688
5139
|
}
|
|
@@ -4695,7 +5146,7 @@ function spawnDevServerProcess(projectPath, port, moduleUid, logPath) {
|
|
|
4695
5146
|
const nodeOptions = process.env.NODE_OPTIONS || "";
|
|
4696
5147
|
const memoryFlag = `--max-old-space-size=${NODE_MEMORY_LIMIT_MB}`;
|
|
4697
5148
|
const enhancedNodeOptions = nodeOptions.includes("max-old-space-size") ? nodeOptions : `${nodeOptions} ${memoryFlag}`.trim();
|
|
4698
|
-
const devProcess = (0,
|
|
5149
|
+
const devProcess = (0, import_child_process9.spawn)("npm", ["run", "dev"], {
|
|
4699
5150
|
cwd: projectPath,
|
|
4700
5151
|
env: {
|
|
4701
5152
|
...process.env,
|
|
@@ -4743,7 +5194,7 @@ async function handleProcessExit(moduleUid, code, signal) {
|
|
|
4743
5194
|
const delay = calculateRestartDelay(serverInfo.restartCount);
|
|
4744
5195
|
console.log(`[DevServer] EP932: Restarting ${moduleUid} in ${delay}ms (attempt ${serverInfo.restartCount + 1}/${MAX_RESTART_ATTEMPTS})`);
|
|
4745
5196
|
writeToLog(serverInfo.logFile || "", `Scheduling restart in ${delay}ms (attempt ${serverInfo.restartCount + 1})`, false);
|
|
4746
|
-
await new Promise((
|
|
5197
|
+
await new Promise((resolve3) => setTimeout(resolve3, delay));
|
|
4747
5198
|
if (!activeServers.has(moduleUid)) {
|
|
4748
5199
|
console.log(`[DevServer] EP932: Server ${moduleUid} was removed during restart delay, aborting restart`);
|
|
4749
5200
|
return;
|
|
@@ -4840,7 +5291,7 @@ async function stopDevServer(moduleUid) {
|
|
|
4840
5291
|
writeToLog(serverInfo.logFile, `Stopping server (manual stop)`, false);
|
|
4841
5292
|
}
|
|
4842
5293
|
serverInfo.process.kill("SIGTERM");
|
|
4843
|
-
await new Promise((
|
|
5294
|
+
await new Promise((resolve3) => setTimeout(resolve3, 2e3));
|
|
4844
5295
|
if (!serverInfo.process.killed) {
|
|
4845
5296
|
serverInfo.process.kill("SIGKILL");
|
|
4846
5297
|
}
|
|
@@ -4858,7 +5309,7 @@ async function restartDevServer(moduleUid) {
|
|
|
4858
5309
|
writeToLog(logFile, `Manual restart requested`, false);
|
|
4859
5310
|
}
|
|
4860
5311
|
await stopDevServer(moduleUid);
|
|
4861
|
-
await new Promise((
|
|
5312
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
4862
5313
|
if (await isPortInUse(port)) {
|
|
4863
5314
|
await killProcessOnPort(port);
|
|
4864
5315
|
}
|
|
@@ -4956,23 +5407,546 @@ function getPortFromPackageJson(projectPath) {
|
|
|
4956
5407
|
return null;
|
|
4957
5408
|
}
|
|
4958
5409
|
|
|
4959
|
-
// src/daemon/
|
|
5410
|
+
// src/daemon/worktree-manager.ts
|
|
4960
5411
|
var fs11 = __toESM(require("fs"));
|
|
4961
|
-
var os4 = __toESM(require("os"));
|
|
4962
5412
|
var path12 = __toESM(require("path"));
|
|
4963
|
-
var
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
if (expiresAt > now + bufferMs) {
|
|
4968
|
-
return config;
|
|
5413
|
+
var import_core8 = __toESM(require_dist());
|
|
5414
|
+
function validateModuleUid(moduleUid) {
|
|
5415
|
+
if (!moduleUid || typeof moduleUid !== "string" || !moduleUid.trim()) {
|
|
5416
|
+
return false;
|
|
4969
5417
|
}
|
|
4970
|
-
if (
|
|
4971
|
-
|
|
4972
|
-
return config;
|
|
5418
|
+
if (moduleUid.includes("/") || moduleUid.includes("\\") || moduleUid.includes("..")) {
|
|
5419
|
+
return false;
|
|
4973
5420
|
}
|
|
4974
|
-
|
|
4975
|
-
|
|
5421
|
+
if (moduleUid.includes("\0")) {
|
|
5422
|
+
return false;
|
|
5423
|
+
}
|
|
5424
|
+
if (moduleUid.startsWith(".")) {
|
|
5425
|
+
return false;
|
|
5426
|
+
}
|
|
5427
|
+
return true;
|
|
5428
|
+
}
|
|
5429
|
+
var WorktreeManager = class _WorktreeManager {
|
|
5430
|
+
constructor(projectRoot) {
|
|
5431
|
+
// ============================================================
|
|
5432
|
+
// Private methods
|
|
5433
|
+
// ============================================================
|
|
5434
|
+
this.lockPath = "";
|
|
5435
|
+
this.projectRoot = projectRoot;
|
|
5436
|
+
this.bareRepoPath = path12.join(projectRoot, ".bare");
|
|
5437
|
+
this.configPath = path12.join(projectRoot, ".episoda", "config.json");
|
|
5438
|
+
this.gitExecutor = new import_core8.GitExecutor();
|
|
5439
|
+
}
|
|
5440
|
+
/**
|
|
5441
|
+
* Initialize worktree manager from existing project root
|
|
5442
|
+
* @returns true if valid worktree project, false otherwise
|
|
5443
|
+
*/
|
|
5444
|
+
async initialize() {
|
|
5445
|
+
if (!fs11.existsSync(this.bareRepoPath)) {
|
|
5446
|
+
return false;
|
|
5447
|
+
}
|
|
5448
|
+
if (!fs11.existsSync(this.configPath)) {
|
|
5449
|
+
return false;
|
|
5450
|
+
}
|
|
5451
|
+
try {
|
|
5452
|
+
const config = this.readConfig();
|
|
5453
|
+
return config?.worktreeMode === true;
|
|
5454
|
+
} catch {
|
|
5455
|
+
return false;
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
/**
|
|
5459
|
+
* Create a new worktree project from scratch
|
|
5460
|
+
*/
|
|
5461
|
+
static async createProject(projectRoot, repoUrl, projectId, workspaceSlug, projectSlug) {
|
|
5462
|
+
const manager = new _WorktreeManager(projectRoot);
|
|
5463
|
+
const episodaDir = path12.join(projectRoot, ".episoda");
|
|
5464
|
+
fs11.mkdirSync(episodaDir, { recursive: true });
|
|
5465
|
+
const cloneResult = await manager.gitExecutor.execute({
|
|
5466
|
+
action: "clone_bare",
|
|
5467
|
+
url: repoUrl,
|
|
5468
|
+
path: manager.bareRepoPath
|
|
5469
|
+
});
|
|
5470
|
+
if (!cloneResult.success) {
|
|
5471
|
+
throw new Error(`Failed to clone repository: ${cloneResult.output}`);
|
|
5472
|
+
}
|
|
5473
|
+
const config = {
|
|
5474
|
+
projectId,
|
|
5475
|
+
workspaceSlug,
|
|
5476
|
+
projectSlug,
|
|
5477
|
+
bareRepoPath: manager.bareRepoPath,
|
|
5478
|
+
worktreeMode: true,
|
|
5479
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5480
|
+
worktrees: []
|
|
5481
|
+
};
|
|
5482
|
+
manager.writeConfig(config);
|
|
5483
|
+
return manager;
|
|
5484
|
+
}
|
|
5485
|
+
/**
|
|
5486
|
+
* Create a worktree for a module
|
|
5487
|
+
* The entire operation is locked to prevent race conditions
|
|
5488
|
+
*/
|
|
5489
|
+
async createWorktree(moduleUid, branchName, createBranch = false) {
|
|
5490
|
+
if (!validateModuleUid(moduleUid)) {
|
|
5491
|
+
return {
|
|
5492
|
+
success: false,
|
|
5493
|
+
error: `Invalid module UID: "${moduleUid}" - contains disallowed characters`
|
|
5494
|
+
};
|
|
5495
|
+
}
|
|
5496
|
+
const worktreePath = path12.join(this.projectRoot, moduleUid);
|
|
5497
|
+
const lockAcquired = await this.acquireLock();
|
|
5498
|
+
if (!lockAcquired) {
|
|
5499
|
+
return {
|
|
5500
|
+
success: false,
|
|
5501
|
+
error: "Could not acquire lock for worktree creation"
|
|
5502
|
+
};
|
|
5503
|
+
}
|
|
5504
|
+
try {
|
|
5505
|
+
const existing = this.getWorktreeByModuleUid(moduleUid);
|
|
5506
|
+
if (existing) {
|
|
5507
|
+
return {
|
|
5508
|
+
success: true,
|
|
5509
|
+
worktreePath: existing.worktreePath,
|
|
5510
|
+
worktreeInfo: existing
|
|
5511
|
+
};
|
|
5512
|
+
}
|
|
5513
|
+
const result = await this.gitExecutor.execute({
|
|
5514
|
+
action: "worktree_add",
|
|
5515
|
+
path: worktreePath,
|
|
5516
|
+
branch: branchName,
|
|
5517
|
+
create: createBranch
|
|
5518
|
+
}, { cwd: this.bareRepoPath });
|
|
5519
|
+
if (!result.success) {
|
|
5520
|
+
return {
|
|
5521
|
+
success: false,
|
|
5522
|
+
error: result.output || "Failed to create worktree"
|
|
5523
|
+
};
|
|
5524
|
+
}
|
|
5525
|
+
const worktreeInfo = {
|
|
5526
|
+
moduleUid,
|
|
5527
|
+
branchName,
|
|
5528
|
+
worktreePath,
|
|
5529
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5530
|
+
lastAccessed: (/* @__PURE__ */ new Date()).toISOString()
|
|
5531
|
+
};
|
|
5532
|
+
const config = this.readConfig();
|
|
5533
|
+
if (config) {
|
|
5534
|
+
config.worktrees.push(worktreeInfo);
|
|
5535
|
+
this.writeConfig(config);
|
|
5536
|
+
}
|
|
5537
|
+
return {
|
|
5538
|
+
success: true,
|
|
5539
|
+
worktreePath,
|
|
5540
|
+
worktreeInfo
|
|
5541
|
+
};
|
|
5542
|
+
} finally {
|
|
5543
|
+
this.releaseLock();
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
/**
|
|
5547
|
+
* Remove a worktree for a module
|
|
5548
|
+
* P1-2: Wrapped entire operation in lock to prevent race with createWorktree
|
|
5549
|
+
*/
|
|
5550
|
+
async removeWorktree(moduleUid, force = false) {
|
|
5551
|
+
if (!validateModuleUid(moduleUid)) {
|
|
5552
|
+
return {
|
|
5553
|
+
success: false,
|
|
5554
|
+
error: `Invalid module UID: "${moduleUid}" - contains disallowed characters`
|
|
5555
|
+
};
|
|
5556
|
+
}
|
|
5557
|
+
const lockAcquired = await this.acquireLock();
|
|
5558
|
+
if (!lockAcquired) {
|
|
5559
|
+
return {
|
|
5560
|
+
success: false,
|
|
5561
|
+
error: "Could not acquire lock for worktree removal"
|
|
5562
|
+
};
|
|
5563
|
+
}
|
|
5564
|
+
try {
|
|
5565
|
+
const existing = this.getWorktreeByModuleUid(moduleUid);
|
|
5566
|
+
if (!existing) {
|
|
5567
|
+
return {
|
|
5568
|
+
success: false,
|
|
5569
|
+
error: `No worktree found for module ${moduleUid}`
|
|
5570
|
+
};
|
|
5571
|
+
}
|
|
5572
|
+
const result = await this.gitExecutor.execute({
|
|
5573
|
+
action: "worktree_remove",
|
|
5574
|
+
path: existing.worktreePath,
|
|
5575
|
+
force
|
|
5576
|
+
}, { cwd: this.bareRepoPath });
|
|
5577
|
+
if (!result.success) {
|
|
5578
|
+
return {
|
|
5579
|
+
success: false,
|
|
5580
|
+
error: result.output || "Failed to remove worktree"
|
|
5581
|
+
};
|
|
5582
|
+
}
|
|
5583
|
+
const config = this.readConfig();
|
|
5584
|
+
if (config) {
|
|
5585
|
+
config.worktrees = config.worktrees.filter((w) => w.moduleUid !== moduleUid);
|
|
5586
|
+
this.writeConfig(config);
|
|
5587
|
+
}
|
|
5588
|
+
return {
|
|
5589
|
+
success: true,
|
|
5590
|
+
worktreePath: existing.worktreePath
|
|
5591
|
+
};
|
|
5592
|
+
} finally {
|
|
5593
|
+
this.releaseLock();
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
/**
|
|
5597
|
+
* Get worktree path for a module
|
|
5598
|
+
*/
|
|
5599
|
+
getWorktreePath(moduleUid) {
|
|
5600
|
+
if (!validateModuleUid(moduleUid)) {
|
|
5601
|
+
return null;
|
|
5602
|
+
}
|
|
5603
|
+
const worktree = this.getWorktreeByModuleUid(moduleUid);
|
|
5604
|
+
return worktree?.worktreePath || null;
|
|
5605
|
+
}
|
|
5606
|
+
/**
|
|
5607
|
+
* Get worktree info by module UID
|
|
5608
|
+
*/
|
|
5609
|
+
getWorktreeByModuleUid(moduleUid) {
|
|
5610
|
+
const config = this.readConfig();
|
|
5611
|
+
return config?.worktrees.find((w) => w.moduleUid === moduleUid) || null;
|
|
5612
|
+
}
|
|
5613
|
+
/**
|
|
5614
|
+
* Get worktree info by branch name
|
|
5615
|
+
*/
|
|
5616
|
+
getWorktreeByBranch(branchName) {
|
|
5617
|
+
const config = this.readConfig();
|
|
5618
|
+
return config?.worktrees.find((w) => w.branchName === branchName) || null;
|
|
5619
|
+
}
|
|
5620
|
+
/**
|
|
5621
|
+
* List all active worktrees
|
|
5622
|
+
*/
|
|
5623
|
+
listWorktrees() {
|
|
5624
|
+
const config = this.readConfig();
|
|
5625
|
+
return config?.worktrees || [];
|
|
5626
|
+
}
|
|
5627
|
+
/**
|
|
5628
|
+
* EP957: Audit worktrees against known active module UIDs
|
|
5629
|
+
*
|
|
5630
|
+
* Compares local worktrees against a list of module UIDs that should be active.
|
|
5631
|
+
* Used by daemon startup to detect orphaned worktrees from crashed sessions.
|
|
5632
|
+
*
|
|
5633
|
+
* @param activeModuleUids - UIDs of modules currently in doing/review state
|
|
5634
|
+
* @returns Object with orphaned and valid worktree lists
|
|
5635
|
+
*/
|
|
5636
|
+
auditWorktrees(activeModuleUids) {
|
|
5637
|
+
const allWorktrees = this.listWorktrees();
|
|
5638
|
+
const activeSet = new Set(activeModuleUids);
|
|
5639
|
+
const orphaned = allWorktrees.filter((w) => !activeSet.has(w.moduleUid));
|
|
5640
|
+
const valid = allWorktrees.filter((w) => activeSet.has(w.moduleUid));
|
|
5641
|
+
return { orphaned, valid };
|
|
5642
|
+
}
|
|
5643
|
+
/**
|
|
5644
|
+
* Update last accessed timestamp for a worktree
|
|
5645
|
+
*/
|
|
5646
|
+
async touchWorktree(moduleUid) {
|
|
5647
|
+
await this.updateConfigSafe((config) => {
|
|
5648
|
+
const worktree = config.worktrees.find((w) => w.moduleUid === moduleUid);
|
|
5649
|
+
if (worktree) {
|
|
5650
|
+
worktree.lastAccessed = (/* @__PURE__ */ new Date()).toISOString();
|
|
5651
|
+
}
|
|
5652
|
+
return config;
|
|
5653
|
+
});
|
|
5654
|
+
}
|
|
5655
|
+
/**
|
|
5656
|
+
* Prune stale worktrees (directories that no longer exist)
|
|
5657
|
+
*/
|
|
5658
|
+
async pruneStaleWorktrees() {
|
|
5659
|
+
await this.gitExecutor.execute({
|
|
5660
|
+
action: "worktree_prune"
|
|
5661
|
+
}, { cwd: this.bareRepoPath });
|
|
5662
|
+
let prunedCount = 0;
|
|
5663
|
+
await this.updateConfigSafe((config) => {
|
|
5664
|
+
const initialCount = config.worktrees.length;
|
|
5665
|
+
config.worktrees = config.worktrees.filter((w) => fs11.existsSync(w.worktreePath));
|
|
5666
|
+
prunedCount = initialCount - config.worktrees.length;
|
|
5667
|
+
return config;
|
|
5668
|
+
});
|
|
5669
|
+
return prunedCount;
|
|
5670
|
+
}
|
|
5671
|
+
/**
|
|
5672
|
+
* Validate all worktrees and sync with git
|
|
5673
|
+
*/
|
|
5674
|
+
async validateWorktrees() {
|
|
5675
|
+
const config = this.readConfig();
|
|
5676
|
+
const valid = [];
|
|
5677
|
+
const stale = [];
|
|
5678
|
+
const orphaned = [];
|
|
5679
|
+
const listResult = await this.gitExecutor.execute({
|
|
5680
|
+
action: "worktree_list"
|
|
5681
|
+
}, { cwd: this.bareRepoPath });
|
|
5682
|
+
const actualWorktrees = new Set(
|
|
5683
|
+
listResult.details?.worktrees?.map((w) => w.path) || []
|
|
5684
|
+
);
|
|
5685
|
+
for (const worktree of config?.worktrees || []) {
|
|
5686
|
+
if (actualWorktrees.has(worktree.worktreePath)) {
|
|
5687
|
+
valid.push(worktree);
|
|
5688
|
+
actualWorktrees.delete(worktree.worktreePath);
|
|
5689
|
+
} else {
|
|
5690
|
+
stale.push(worktree);
|
|
5691
|
+
}
|
|
5692
|
+
}
|
|
5693
|
+
for (const wpath of actualWorktrees) {
|
|
5694
|
+
if (wpath !== this.bareRepoPath) {
|
|
5695
|
+
orphaned.push(wpath);
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
return { valid, stale, orphaned };
|
|
5699
|
+
}
|
|
5700
|
+
/**
|
|
5701
|
+
* Get project configuration
|
|
5702
|
+
*/
|
|
5703
|
+
getConfig() {
|
|
5704
|
+
return this.readConfig();
|
|
5705
|
+
}
|
|
5706
|
+
/**
|
|
5707
|
+
* Get the bare repo path
|
|
5708
|
+
*/
|
|
5709
|
+
getBareRepoPath() {
|
|
5710
|
+
return this.bareRepoPath;
|
|
5711
|
+
}
|
|
5712
|
+
/**
|
|
5713
|
+
* Get the project root path
|
|
5714
|
+
*/
|
|
5715
|
+
getProjectRoot() {
|
|
5716
|
+
return this.projectRoot;
|
|
5717
|
+
}
|
|
5718
|
+
getLockPath() {
|
|
5719
|
+
if (!this.lockPath) {
|
|
5720
|
+
this.lockPath = this.configPath + ".lock";
|
|
5721
|
+
}
|
|
5722
|
+
return this.lockPath;
|
|
5723
|
+
}
|
|
5724
|
+
/**
|
|
5725
|
+
* Check if a process is still running
|
|
5726
|
+
*/
|
|
5727
|
+
isProcessRunning(pid) {
|
|
5728
|
+
try {
|
|
5729
|
+
process.kill(pid, 0);
|
|
5730
|
+
return true;
|
|
5731
|
+
} catch {
|
|
5732
|
+
return false;
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
/**
|
|
5736
|
+
* Acquire a file lock with timeout
|
|
5737
|
+
* Uses atomic file creation to ensure only one process holds the lock
|
|
5738
|
+
* P1-1: Added PID verification before removing stale locks to prevent race conditions
|
|
5739
|
+
*/
|
|
5740
|
+
async acquireLock(timeoutMs = 5e3) {
|
|
5741
|
+
const lockPath = this.getLockPath();
|
|
5742
|
+
const startTime = Date.now();
|
|
5743
|
+
const retryInterval = 50;
|
|
5744
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
5745
|
+
try {
|
|
5746
|
+
fs11.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
5747
|
+
return true;
|
|
5748
|
+
} catch (err) {
|
|
5749
|
+
if (err.code === "EEXIST") {
|
|
5750
|
+
try {
|
|
5751
|
+
const stats = fs11.statSync(lockPath);
|
|
5752
|
+
const lockAge = Date.now() - stats.mtimeMs;
|
|
5753
|
+
if (lockAge > 3e4) {
|
|
5754
|
+
try {
|
|
5755
|
+
const lockContent = fs11.readFileSync(lockPath, "utf-8").trim();
|
|
5756
|
+
const lockPid = parseInt(lockContent, 10);
|
|
5757
|
+
if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
|
|
5758
|
+
await new Promise((resolve3) => setTimeout(resolve3, retryInterval));
|
|
5759
|
+
continue;
|
|
5760
|
+
}
|
|
5761
|
+
} catch {
|
|
5762
|
+
}
|
|
5763
|
+
try {
|
|
5764
|
+
fs11.unlinkSync(lockPath);
|
|
5765
|
+
} catch {
|
|
5766
|
+
}
|
|
5767
|
+
continue;
|
|
5768
|
+
}
|
|
5769
|
+
} catch {
|
|
5770
|
+
continue;
|
|
5771
|
+
}
|
|
5772
|
+
await new Promise((resolve3) => setTimeout(resolve3, retryInterval));
|
|
5773
|
+
continue;
|
|
5774
|
+
}
|
|
5775
|
+
throw err;
|
|
5776
|
+
}
|
|
5777
|
+
}
|
|
5778
|
+
return false;
|
|
5779
|
+
}
|
|
5780
|
+
/**
|
|
5781
|
+
* Release the file lock
|
|
5782
|
+
*/
|
|
5783
|
+
releaseLock() {
|
|
5784
|
+
try {
|
|
5785
|
+
fs11.unlinkSync(this.getLockPath());
|
|
5786
|
+
} catch {
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
readConfig() {
|
|
5790
|
+
try {
|
|
5791
|
+
if (!fs11.existsSync(this.configPath)) {
|
|
5792
|
+
return null;
|
|
5793
|
+
}
|
|
5794
|
+
const content = fs11.readFileSync(this.configPath, "utf-8");
|
|
5795
|
+
return JSON.parse(content);
|
|
5796
|
+
} catch (error) {
|
|
5797
|
+
console.error("[WorktreeManager] Failed to read config:", error);
|
|
5798
|
+
return null;
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5801
|
+
writeConfig(config) {
|
|
5802
|
+
try {
|
|
5803
|
+
const dir = path12.dirname(this.configPath);
|
|
5804
|
+
if (!fs11.existsSync(dir)) {
|
|
5805
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
5806
|
+
}
|
|
5807
|
+
fs11.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
5808
|
+
} catch (error) {
|
|
5809
|
+
console.error("[WorktreeManager] Failed to write config:", error);
|
|
5810
|
+
throw error;
|
|
5811
|
+
}
|
|
5812
|
+
}
|
|
5813
|
+
/**
|
|
5814
|
+
* Read-modify-write with file locking for safe concurrent access
|
|
5815
|
+
*/
|
|
5816
|
+
async updateConfigSafe(updater) {
|
|
5817
|
+
const lockAcquired = await this.acquireLock();
|
|
5818
|
+
if (!lockAcquired) {
|
|
5819
|
+
console.error("[WorktreeManager] Failed to acquire lock for config update");
|
|
5820
|
+
return false;
|
|
5821
|
+
}
|
|
5822
|
+
try {
|
|
5823
|
+
const config = this.readConfig();
|
|
5824
|
+
if (!config) {
|
|
5825
|
+
return false;
|
|
5826
|
+
}
|
|
5827
|
+
const updated = updater(config);
|
|
5828
|
+
this.writeConfig(updated);
|
|
5829
|
+
return true;
|
|
5830
|
+
} finally {
|
|
5831
|
+
this.releaseLock();
|
|
5832
|
+
}
|
|
5833
|
+
}
|
|
5834
|
+
};
|
|
5835
|
+
function getEpisodaRoot() {
|
|
5836
|
+
return process.env.EPISODA_ROOT || path12.join(require("os").homedir(), "episoda");
|
|
5837
|
+
}
|
|
5838
|
+
async function isWorktreeProject(projectRoot) {
|
|
5839
|
+
const manager = new WorktreeManager(projectRoot);
|
|
5840
|
+
return manager.initialize();
|
|
5841
|
+
}
|
|
5842
|
+
async function findProjectRoot(startPath) {
|
|
5843
|
+
let current = path12.resolve(startPath);
|
|
5844
|
+
const episodaRoot = getEpisodaRoot();
|
|
5845
|
+
if (!current.startsWith(episodaRoot)) {
|
|
5846
|
+
return null;
|
|
5847
|
+
}
|
|
5848
|
+
for (let i = 0; i < 10; i++) {
|
|
5849
|
+
const bareDir = path12.join(current, ".bare");
|
|
5850
|
+
const episodaDir = path12.join(current, ".episoda");
|
|
5851
|
+
if (fs11.existsSync(bareDir) && fs11.existsSync(episodaDir)) {
|
|
5852
|
+
if (await isWorktreeProject(current)) {
|
|
5853
|
+
return current;
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
const parent = path12.dirname(current);
|
|
5857
|
+
if (parent === current) {
|
|
5858
|
+
break;
|
|
5859
|
+
}
|
|
5860
|
+
current = parent;
|
|
5861
|
+
}
|
|
5862
|
+
return null;
|
|
5863
|
+
}
|
|
5864
|
+
|
|
5865
|
+
// src/utils/worktree.ts
|
|
5866
|
+
var path13 = __toESM(require("path"));
|
|
5867
|
+
var fs12 = __toESM(require("fs"));
|
|
5868
|
+
var os4 = __toESM(require("os"));
|
|
5869
|
+
var import_core9 = __toESM(require_dist());
|
|
5870
|
+
function getEpisodaRoot2() {
|
|
5871
|
+
return process.env.EPISODA_ROOT || path13.join(os4.homedir(), "episoda");
|
|
5872
|
+
}
|
|
5873
|
+
function getWorktreeInfo(moduleUid, workspaceSlug, projectSlug) {
|
|
5874
|
+
const root = getEpisodaRoot2();
|
|
5875
|
+
const worktreePath = path13.join(root, workspaceSlug, projectSlug, moduleUid);
|
|
5876
|
+
return {
|
|
5877
|
+
path: worktreePath,
|
|
5878
|
+
exists: fs12.existsSync(worktreePath),
|
|
5879
|
+
moduleUid
|
|
5880
|
+
};
|
|
5881
|
+
}
|
|
5882
|
+
async function getWorktreeInfoForModule(moduleUid) {
|
|
5883
|
+
const config = await (0, import_core9.loadConfig)();
|
|
5884
|
+
if (!config?.workspace_slug || !config?.project_slug) {
|
|
5885
|
+
console.warn("[Worktree] Missing workspace_slug or project_slug in config");
|
|
5886
|
+
return null;
|
|
5887
|
+
}
|
|
5888
|
+
return getWorktreeInfo(moduleUid, config.workspace_slug, config.project_slug);
|
|
5889
|
+
}
|
|
5890
|
+
|
|
5891
|
+
// src/utils/port-allocator.ts
|
|
5892
|
+
var PORT_RANGE_START = 3100;
|
|
5893
|
+
var PORT_RANGE_END = 3199;
|
|
5894
|
+
var PORT_WARNING_THRESHOLD = 80;
|
|
5895
|
+
var portAssignments = /* @__PURE__ */ new Map();
|
|
5896
|
+
function allocatePort(moduleUid) {
|
|
5897
|
+
const existing = portAssignments.get(moduleUid);
|
|
5898
|
+
if (existing) {
|
|
5899
|
+
return existing;
|
|
5900
|
+
}
|
|
5901
|
+
const usedPorts = new Set(portAssignments.values());
|
|
5902
|
+
if (usedPorts.size >= PORT_WARNING_THRESHOLD) {
|
|
5903
|
+
console.warn(
|
|
5904
|
+
`[PortAllocator] Warning: ${usedPorts.size}/${PORT_RANGE_END - PORT_RANGE_START + 1} ports allocated`
|
|
5905
|
+
);
|
|
5906
|
+
}
|
|
5907
|
+
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
|
|
5908
|
+
if (!usedPorts.has(port)) {
|
|
5909
|
+
portAssignments.set(moduleUid, port);
|
|
5910
|
+
console.log(`[PortAllocator] Assigned port ${port} to ${moduleUid}`);
|
|
5911
|
+
return port;
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
throw new Error(
|
|
5915
|
+
`No available ports in range ${PORT_RANGE_START}-${PORT_RANGE_END}. ${portAssignments.size} modules are using all available ports.`
|
|
5916
|
+
);
|
|
5917
|
+
}
|
|
5918
|
+
function releasePort(moduleUid) {
|
|
5919
|
+
const port = portAssignments.get(moduleUid);
|
|
5920
|
+
if (port) {
|
|
5921
|
+
portAssignments.delete(moduleUid);
|
|
5922
|
+
console.log(`[PortAllocator] Released port ${port} from ${moduleUid}`);
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
function clearAllPorts() {
|
|
5926
|
+
const count = portAssignments.size;
|
|
5927
|
+
portAssignments.clear();
|
|
5928
|
+
if (count > 0) {
|
|
5929
|
+
console.log(`[PortAllocator] Cleared ${count} port assignments`);
|
|
5930
|
+
}
|
|
5931
|
+
}
|
|
5932
|
+
|
|
5933
|
+
// src/daemon/daemon-process.ts
|
|
5934
|
+
var fs13 = __toESM(require("fs"));
|
|
5935
|
+
var os5 = __toESM(require("os"));
|
|
5936
|
+
var path14 = __toESM(require("path"));
|
|
5937
|
+
var packageJson = require_package();
|
|
5938
|
+
async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
|
|
5939
|
+
const now = Date.now();
|
|
5940
|
+
const expiresAt = config.expires_at || 0;
|
|
5941
|
+
if (expiresAt > now + bufferMs) {
|
|
5942
|
+
return config;
|
|
5943
|
+
}
|
|
5944
|
+
if (!config.refresh_token) {
|
|
5945
|
+
console.warn("[Daemon] EP904: Token expired but no refresh_token available");
|
|
5946
|
+
return config;
|
|
5947
|
+
}
|
|
5948
|
+
console.log("[Daemon] EP904: Access token expired or expiring soon, refreshing...");
|
|
5949
|
+
try {
|
|
4976
5950
|
const apiUrl = config.api_url || "https://episoda.dev";
|
|
4977
5951
|
const response = await fetch(`${apiUrl}/api/oauth/token`, {
|
|
4978
5952
|
method: "POST",
|
|
@@ -4994,7 +5968,7 @@ async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
|
|
|
4994
5968
|
refresh_token: tokenResponse.refresh_token || config.refresh_token,
|
|
4995
5969
|
expires_at: now + tokenResponse.expires_in * 1e3
|
|
4996
5970
|
};
|
|
4997
|
-
await (0,
|
|
5971
|
+
await (0, import_core10.saveConfig)(updatedConfig);
|
|
4998
5972
|
console.log("[Daemon] EP904: Access token refreshed successfully");
|
|
4999
5973
|
return updatedConfig;
|
|
5000
5974
|
} catch (error) {
|
|
@@ -5003,7 +5977,7 @@ async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
|
|
|
5003
5977
|
}
|
|
5004
5978
|
}
|
|
5005
5979
|
async function fetchWithAuth(url, options = {}, retryOnUnauthorized = true) {
|
|
5006
|
-
let config = await (0,
|
|
5980
|
+
let config = await (0, import_core10.loadConfig)();
|
|
5007
5981
|
if (!config?.access_token) {
|
|
5008
5982
|
throw new Error("No access token configured");
|
|
5009
5983
|
}
|
|
@@ -5093,7 +6067,7 @@ var Daemon = class _Daemon {
|
|
|
5093
6067
|
console.log("[Daemon] Starting Episoda daemon...");
|
|
5094
6068
|
this.machineId = await getMachineId();
|
|
5095
6069
|
console.log(`[Daemon] Machine ID: ${this.machineId}`);
|
|
5096
|
-
const config = await (0,
|
|
6070
|
+
const config = await (0, import_core10.loadConfig)();
|
|
5097
6071
|
if (config?.device_id) {
|
|
5098
6072
|
this.deviceId = config.device_id;
|
|
5099
6073
|
console.log(`[Daemon] Loaded cached Device ID (UUID): ${this.deviceId}`);
|
|
@@ -5103,6 +6077,7 @@ var Daemon = class _Daemon {
|
|
|
5103
6077
|
this.registerIPCHandlers();
|
|
5104
6078
|
await this.restoreConnections();
|
|
5105
6079
|
await this.cleanupOrphanedTunnels();
|
|
6080
|
+
await this.auditWorktreesOnStartup();
|
|
5106
6081
|
this.startHealthCheckPolling();
|
|
5107
6082
|
this.setupShutdownHandlers();
|
|
5108
6083
|
console.log("[Daemon] Daemon started successfully");
|
|
@@ -5148,9 +6123,9 @@ var Daemon = class _Daemon {
|
|
|
5148
6123
|
machineId: this.machineId,
|
|
5149
6124
|
deviceId: this.deviceId,
|
|
5150
6125
|
// EP726: UUID for unified device identification
|
|
5151
|
-
hostname:
|
|
5152
|
-
platform:
|
|
5153
|
-
arch:
|
|
6126
|
+
hostname: os5.hostname(),
|
|
6127
|
+
platform: os5.platform(),
|
|
6128
|
+
arch: os5.arch(),
|
|
5154
6129
|
projects
|
|
5155
6130
|
};
|
|
5156
6131
|
});
|
|
@@ -5176,7 +6151,7 @@ var Daemon = class _Daemon {
|
|
|
5176
6151
|
if (attempt < MAX_RETRIES) {
|
|
5177
6152
|
const delay = INITIAL_DELAY * Math.pow(2, attempt - 1);
|
|
5178
6153
|
console.log(`[Daemon] Retrying in ${delay / 1e3}s...`);
|
|
5179
|
-
await new Promise((
|
|
6154
|
+
await new Promise((resolve3) => setTimeout(resolve3, delay));
|
|
5180
6155
|
await this.disconnectProject(projectPath);
|
|
5181
6156
|
}
|
|
5182
6157
|
}
|
|
@@ -5229,7 +6204,7 @@ var Daemon = class _Daemon {
|
|
|
5229
6204
|
};
|
|
5230
6205
|
});
|
|
5231
6206
|
this.ipcServer.on("verify-server-connection", async () => {
|
|
5232
|
-
const config = await (0,
|
|
6207
|
+
const config = await (0, import_core10.loadConfig)();
|
|
5233
6208
|
if (!config?.access_token || !config?.api_url) {
|
|
5234
6209
|
return {
|
|
5235
6210
|
verified: false,
|
|
@@ -5365,8 +6340,8 @@ var Daemon = class _Daemon {
|
|
|
5365
6340
|
}
|
|
5366
6341
|
}
|
|
5367
6342
|
let releaseLock;
|
|
5368
|
-
const lockPromise = new Promise((
|
|
5369
|
-
releaseLock =
|
|
6343
|
+
const lockPromise = new Promise((resolve3) => {
|
|
6344
|
+
releaseLock = resolve3;
|
|
5370
6345
|
});
|
|
5371
6346
|
this.tunnelOperationLocks.set(moduleUid, lockPromise);
|
|
5372
6347
|
try {
|
|
@@ -5392,7 +6367,7 @@ var Daemon = class _Daemon {
|
|
|
5392
6367
|
const maxWait = 35e3;
|
|
5393
6368
|
const startTime = Date.now();
|
|
5394
6369
|
while (this.pendingConnections.has(projectPath) && Date.now() - startTime < maxWait) {
|
|
5395
|
-
await new Promise((
|
|
6370
|
+
await new Promise((resolve3) => setTimeout(resolve3, 500));
|
|
5396
6371
|
}
|
|
5397
6372
|
if (this.liveConnections.has(projectPath)) {
|
|
5398
6373
|
console.log(`[Daemon] Pending connection succeeded for ${projectPath}`);
|
|
@@ -5403,7 +6378,7 @@ var Daemon = class _Daemon {
|
|
|
5403
6378
|
console.warn(`[Daemon] Stale connection detected for ${projectPath}, forcing reconnection`);
|
|
5404
6379
|
await this.disconnectProject(projectPath);
|
|
5405
6380
|
}
|
|
5406
|
-
const config = await (0,
|
|
6381
|
+
const config = await (0, import_core10.loadConfig)();
|
|
5407
6382
|
if (!config || !config.access_token) {
|
|
5408
6383
|
throw new Error("No access token found. Please run: episoda auth");
|
|
5409
6384
|
}
|
|
@@ -5417,8 +6392,8 @@ var Daemon = class _Daemon {
|
|
|
5417
6392
|
const wsPort = process.env.EPISODA_WS_PORT || "3001";
|
|
5418
6393
|
const wsUrl = `${wsProtocol}//${serverUrlObj.hostname}:${wsPort}`;
|
|
5419
6394
|
console.log(`[Daemon] Connecting to ${wsUrl} for project ${projectId}...`);
|
|
5420
|
-
const client = new
|
|
5421
|
-
const gitExecutor = new
|
|
6395
|
+
const client = new import_core10.EpisodaClient();
|
|
6396
|
+
const gitExecutor = new import_core10.GitExecutor();
|
|
5422
6397
|
const connection = {
|
|
5423
6398
|
projectId,
|
|
5424
6399
|
projectPath,
|
|
@@ -5432,8 +6407,13 @@ var Daemon = class _Daemon {
|
|
|
5432
6407
|
console.log(`[Daemon] Received command for ${projectId}:`, message.command);
|
|
5433
6408
|
client.updateActivity();
|
|
5434
6409
|
try {
|
|
5435
|
-
const
|
|
5436
|
-
|
|
6410
|
+
const gitCmd = message.command;
|
|
6411
|
+
const cwd = gitCmd.worktreePath || projectPath;
|
|
6412
|
+
if (gitCmd.worktreePath) {
|
|
6413
|
+
console.log(`[Daemon] EP944: Routing command to worktree: ${gitCmd.worktreePath}`);
|
|
6414
|
+
}
|
|
6415
|
+
const result = await gitExecutor.execute(gitCmd, {
|
|
6416
|
+
cwd
|
|
5437
6417
|
});
|
|
5438
6418
|
await client.send({
|
|
5439
6419
|
type: "result",
|
|
@@ -5441,6 +6421,15 @@ var Daemon = class _Daemon {
|
|
|
5441
6421
|
result
|
|
5442
6422
|
});
|
|
5443
6423
|
console.log(`[Daemon] Command completed for ${projectId}:`, result.success ? "success" : "failed");
|
|
6424
|
+
if (result.success && gitCmd.action === "push" && gitCmd.branch === "main") {
|
|
6425
|
+
cleanupStaleCommits(projectPath).then((cleanupResult) => {
|
|
6426
|
+
if (cleanupResult.deleted_count > 0) {
|
|
6427
|
+
console.log(`[Daemon] EP950: Cleaned up ${cleanupResult.deleted_count} stale commit(s) after push to main`);
|
|
6428
|
+
}
|
|
6429
|
+
}).catch((err) => {
|
|
6430
|
+
console.warn("[Daemon] EP950: Cleanup after push failed:", err.message);
|
|
6431
|
+
});
|
|
6432
|
+
}
|
|
5444
6433
|
} catch (error) {
|
|
5445
6434
|
await client.send({
|
|
5446
6435
|
type: "result",
|
|
@@ -5527,7 +6516,7 @@ var Daemon = class _Daemon {
|
|
|
5527
6516
|
const port = cmd.port || detectDevPort(projectPath);
|
|
5528
6517
|
const previewUrl = `https://${cmd.moduleUid.toLowerCase()}-${cmd.projectUid.toLowerCase()}.episoda.site`;
|
|
5529
6518
|
const reportTunnelStatus = async (data) => {
|
|
5530
|
-
const config2 = await (0,
|
|
6519
|
+
const config2 = await (0, import_core10.loadConfig)();
|
|
5531
6520
|
if (config2?.access_token) {
|
|
5532
6521
|
try {
|
|
5533
6522
|
const apiUrl = config2.api_url || "https://episoda.dev";
|
|
@@ -5551,7 +6540,7 @@ var Daemon = class _Daemon {
|
|
|
5551
6540
|
};
|
|
5552
6541
|
(async () => {
|
|
5553
6542
|
const MAX_RETRIES = 3;
|
|
5554
|
-
const RETRY_DELAY_MS =
|
|
6543
|
+
const RETRY_DELAY_MS = 3e3;
|
|
5555
6544
|
await reportTunnelStatus({
|
|
5556
6545
|
tunnel_started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5557
6546
|
tunnel_error: null
|
|
@@ -5599,7 +6588,7 @@ var Daemon = class _Daemon {
|
|
|
5599
6588
|
console.warn(`[Daemon] Tunnel start attempt ${attempt} failed: ${lastError}`);
|
|
5600
6589
|
if (attempt < MAX_RETRIES) {
|
|
5601
6590
|
console.log(`[Daemon] Retrying in ${RETRY_DELAY_MS}ms...`);
|
|
5602
|
-
await new Promise((
|
|
6591
|
+
await new Promise((resolve3) => setTimeout(resolve3, RETRY_DELAY_MS));
|
|
5603
6592
|
}
|
|
5604
6593
|
}
|
|
5605
6594
|
const errorMsg = `Tunnel failed after ${MAX_RETRIES} attempts: ${lastError}`;
|
|
@@ -5619,7 +6608,7 @@ var Daemon = class _Daemon {
|
|
|
5619
6608
|
} else if (cmd.action === "stop") {
|
|
5620
6609
|
await tunnelManager.stopTunnel(cmd.moduleUid);
|
|
5621
6610
|
await stopDevServer(cmd.moduleUid);
|
|
5622
|
-
const config2 = await (0,
|
|
6611
|
+
const config2 = await (0, import_core10.loadConfig)();
|
|
5623
6612
|
if (config2?.access_token) {
|
|
5624
6613
|
try {
|
|
5625
6614
|
const apiUrl = config2.api_url || "https://episoda.dev";
|
|
@@ -5813,15 +6802,26 @@ var Daemon = class _Daemon {
|
|
|
5813
6802
|
this.autoStartTunnelsForProject(projectPath, projectId).catch((error) => {
|
|
5814
6803
|
console.error(`[Daemon] EP819: Failed to auto-start tunnels:`, error);
|
|
5815
6804
|
});
|
|
6805
|
+
cleanupStaleCommits(projectPath).then((cleanupResult) => {
|
|
6806
|
+
if (cleanupResult.deleted_count > 0) {
|
|
6807
|
+
console.log(`[Daemon] EP950: Cleaned up ${cleanupResult.deleted_count} stale commit(s) on connect`);
|
|
6808
|
+
}
|
|
6809
|
+
}).catch((err) => {
|
|
6810
|
+
console.warn("[Daemon] EP950: Cleanup on connect failed:", err.message);
|
|
6811
|
+
});
|
|
5816
6812
|
});
|
|
5817
6813
|
client.on("module_state_changed", async (message) => {
|
|
5818
6814
|
if (message.type === "module_state_changed") {
|
|
5819
|
-
const { moduleUid, state, previousState, branchName, devMode } = message;
|
|
6815
|
+
const { moduleUid, state, previousState, branchName, devMode, checkoutMachineId } = message;
|
|
5820
6816
|
console.log(`[Daemon] EP843: Module ${moduleUid} state changed: ${previousState} \u2192 ${state}`);
|
|
5821
6817
|
if (devMode !== "local") {
|
|
5822
6818
|
console.log(`[Daemon] EP843: Skipping tunnel action for ${moduleUid} (mode: ${devMode || "unknown"})`);
|
|
5823
6819
|
return;
|
|
5824
6820
|
}
|
|
6821
|
+
if (checkoutMachineId && checkoutMachineId !== this.deviceId) {
|
|
6822
|
+
console.log(`[Daemon] EP956: Skipping ${moduleUid} (checked out on different machine: ${checkoutMachineId})`);
|
|
6823
|
+
return;
|
|
6824
|
+
}
|
|
5825
6825
|
const tunnelManager = getTunnelManager();
|
|
5826
6826
|
await tunnelManager.initialize();
|
|
5827
6827
|
await this.withTunnelLock(moduleUid, async () => {
|
|
@@ -5835,51 +6835,65 @@ var Daemon = class _Daemon {
|
|
|
5835
6835
|
console.log(`[Daemon] EP843: Tunnel already running for ${moduleUid}, skipping start`);
|
|
5836
6836
|
return;
|
|
5837
6837
|
}
|
|
5838
|
-
console.log(`[Daemon]
|
|
6838
|
+
console.log(`[Daemon] EP956: Starting tunnel for ${moduleUid} (${previousState} \u2192 ${state})`);
|
|
5839
6839
|
try {
|
|
5840
|
-
const
|
|
5841
|
-
|
|
6840
|
+
const worktree = await getWorktreeInfoForModule(moduleUid);
|
|
6841
|
+
if (!worktree) {
|
|
6842
|
+
console.error(`[Daemon] EP956: Cannot resolve worktree path for ${moduleUid} (missing config slugs)`);
|
|
6843
|
+
return;
|
|
6844
|
+
}
|
|
6845
|
+
if (!worktree.exists) {
|
|
6846
|
+
console.log(`[Daemon] EP956: No worktree for ${moduleUid} at ${worktree.path}, skipping tunnel`);
|
|
6847
|
+
return;
|
|
6848
|
+
}
|
|
6849
|
+
const port = allocatePort(moduleUid);
|
|
6850
|
+
console.log(`[Daemon] EP956: Using worktree ${worktree.path} on port ${port}`);
|
|
6851
|
+
const devServerResult = await ensureDevServer(worktree.path, port, moduleUid);
|
|
5842
6852
|
if (!devServerResult.success) {
|
|
5843
|
-
console.error(`[Daemon]
|
|
6853
|
+
console.error(`[Daemon] EP956: Dev server failed for ${moduleUid}: ${devServerResult.error}`);
|
|
6854
|
+
releasePort(moduleUid);
|
|
5844
6855
|
return;
|
|
5845
6856
|
}
|
|
5846
|
-
const config2 = await (0,
|
|
6857
|
+
const config2 = await (0, import_core10.loadConfig)();
|
|
5847
6858
|
const apiUrl = config2?.api_url || "https://episoda.dev";
|
|
5848
6859
|
const startResult = await tunnelManager.startTunnel({
|
|
5849
6860
|
moduleUid,
|
|
5850
6861
|
port,
|
|
5851
6862
|
onUrl: async (url) => {
|
|
5852
|
-
console.log(`[Daemon]
|
|
6863
|
+
console.log(`[Daemon] EP956: Tunnel URL for ${moduleUid}: ${url}`);
|
|
5853
6864
|
try {
|
|
5854
6865
|
await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}/tunnel`, {
|
|
5855
6866
|
method: "POST",
|
|
5856
6867
|
body: JSON.stringify({ tunnel_url: url })
|
|
5857
6868
|
});
|
|
5858
6869
|
} catch (err) {
|
|
5859
|
-
console.warn(`[Daemon]
|
|
6870
|
+
console.warn(`[Daemon] EP956: Failed to report tunnel URL:`, err instanceof Error ? err.message : err);
|
|
5860
6871
|
}
|
|
5861
6872
|
},
|
|
5862
6873
|
onStatusChange: (status, error) => {
|
|
5863
6874
|
if (status === "error") {
|
|
5864
|
-
console.error(`[Daemon]
|
|
6875
|
+
console.error(`[Daemon] EP956: Tunnel error for ${moduleUid}: ${error}`);
|
|
5865
6876
|
}
|
|
5866
6877
|
}
|
|
5867
6878
|
});
|
|
5868
6879
|
if (startResult.success) {
|
|
5869
|
-
console.log(`[Daemon]
|
|
6880
|
+
console.log(`[Daemon] EP956: Tunnel started for ${moduleUid}`);
|
|
5870
6881
|
} else {
|
|
5871
|
-
console.error(`[Daemon]
|
|
6882
|
+
console.error(`[Daemon] EP956: Tunnel failed for ${moduleUid}: ${startResult.error}`);
|
|
6883
|
+
releasePort(moduleUid);
|
|
5872
6884
|
}
|
|
5873
6885
|
} catch (error) {
|
|
5874
|
-
console.error(`[Daemon]
|
|
6886
|
+
console.error(`[Daemon] EP956: Error starting tunnel for ${moduleUid}:`, error);
|
|
6887
|
+
releasePort(moduleUid);
|
|
5875
6888
|
}
|
|
5876
6889
|
}
|
|
5877
6890
|
if (state === "done" && wasInActiveZone) {
|
|
5878
|
-
console.log(`[Daemon]
|
|
6891
|
+
console.log(`[Daemon] EP956: Stopping tunnel for ${moduleUid} (${previousState} \u2192 done)`);
|
|
5879
6892
|
try {
|
|
5880
6893
|
await tunnelManager.stopTunnel(moduleUid);
|
|
5881
|
-
|
|
5882
|
-
|
|
6894
|
+
releasePort(moduleUid);
|
|
6895
|
+
console.log(`[Daemon] EP956: Tunnel stopped and port released for ${moduleUid}`);
|
|
6896
|
+
const config2 = await (0, import_core10.loadConfig)();
|
|
5883
6897
|
const apiUrl = config2?.api_url || "https://episoda.dev";
|
|
5884
6898
|
try {
|
|
5885
6899
|
await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}/tunnel`, {
|
|
@@ -5887,10 +6901,14 @@ var Daemon = class _Daemon {
|
|
|
5887
6901
|
body: JSON.stringify({ tunnel_url: null })
|
|
5888
6902
|
});
|
|
5889
6903
|
} catch (err) {
|
|
5890
|
-
console.warn(`[Daemon]
|
|
6904
|
+
console.warn(`[Daemon] EP956: Failed to clear tunnel URL:`, err instanceof Error ? err.message : err);
|
|
5891
6905
|
}
|
|
6906
|
+
this.cleanupModuleWorktree(moduleUid).catch((err) => {
|
|
6907
|
+
console.warn(`[Daemon] EP956: Async cleanup failed for ${moduleUid}:`, err instanceof Error ? err.message : err);
|
|
6908
|
+
});
|
|
5892
6909
|
} catch (error) {
|
|
5893
|
-
console.error(`[Daemon]
|
|
6910
|
+
console.error(`[Daemon] EP956: Error stopping tunnel for ${moduleUid}:`, error);
|
|
6911
|
+
releasePort(moduleUid);
|
|
5894
6912
|
}
|
|
5895
6913
|
}
|
|
5896
6914
|
});
|
|
@@ -5912,21 +6930,21 @@ var Daemon = class _Daemon {
|
|
|
5912
6930
|
let daemonPid;
|
|
5913
6931
|
try {
|
|
5914
6932
|
const pidPath = getPidFilePath();
|
|
5915
|
-
if (
|
|
5916
|
-
const pidStr =
|
|
6933
|
+
if (fs13.existsSync(pidPath)) {
|
|
6934
|
+
const pidStr = fs13.readFileSync(pidPath, "utf-8").trim();
|
|
5917
6935
|
daemonPid = parseInt(pidStr, 10);
|
|
5918
6936
|
}
|
|
5919
6937
|
} catch (pidError) {
|
|
5920
6938
|
console.warn(`[Daemon] Could not read daemon PID:`, pidError instanceof Error ? pidError.message : pidError);
|
|
5921
6939
|
}
|
|
5922
|
-
const authSuccessPromise = new Promise((
|
|
6940
|
+
const authSuccessPromise = new Promise((resolve3, reject) => {
|
|
5923
6941
|
const AUTH_TIMEOUT = 3e4;
|
|
5924
6942
|
const timeout = setTimeout(() => {
|
|
5925
6943
|
reject(new Error("Authentication timeout after 30s - server may be under heavy load. Try again in a few seconds."));
|
|
5926
6944
|
}, AUTH_TIMEOUT);
|
|
5927
6945
|
const authHandler = () => {
|
|
5928
6946
|
clearTimeout(timeout);
|
|
5929
|
-
|
|
6947
|
+
resolve3();
|
|
5930
6948
|
};
|
|
5931
6949
|
client.once("auth_success", authHandler);
|
|
5932
6950
|
const errorHandler = (message) => {
|
|
@@ -5937,9 +6955,9 @@ var Daemon = class _Daemon {
|
|
|
5937
6955
|
client.once("auth_error", errorHandler);
|
|
5938
6956
|
});
|
|
5939
6957
|
await client.connect(wsUrl, config.access_token, this.machineId, {
|
|
5940
|
-
hostname:
|
|
5941
|
-
osPlatform:
|
|
5942
|
-
osArch:
|
|
6958
|
+
hostname: os5.hostname(),
|
|
6959
|
+
osPlatform: os5.platform(),
|
|
6960
|
+
osArch: os5.arch(),
|
|
5943
6961
|
daemonPid
|
|
5944
6962
|
});
|
|
5945
6963
|
console.log(`[Daemon] Successfully connected to project ${projectId}`);
|
|
@@ -6034,27 +7052,27 @@ var Daemon = class _Daemon {
|
|
|
6034
7052
|
*/
|
|
6035
7053
|
async installGitHooks(projectPath) {
|
|
6036
7054
|
const hooks = ["post-checkout", "pre-commit", "post-commit"];
|
|
6037
|
-
const hooksDir =
|
|
6038
|
-
if (!
|
|
7055
|
+
const hooksDir = path14.join(projectPath, ".git", "hooks");
|
|
7056
|
+
if (!fs13.existsSync(hooksDir)) {
|
|
6039
7057
|
console.warn(`[Daemon] Hooks directory not found: ${hooksDir}`);
|
|
6040
7058
|
return;
|
|
6041
7059
|
}
|
|
6042
7060
|
for (const hookName of hooks) {
|
|
6043
7061
|
try {
|
|
6044
|
-
const hookPath =
|
|
6045
|
-
const bundledHookPath =
|
|
6046
|
-
if (!
|
|
7062
|
+
const hookPath = path14.join(hooksDir, hookName);
|
|
7063
|
+
const bundledHookPath = path14.join(__dirname, "..", "hooks", hookName);
|
|
7064
|
+
if (!fs13.existsSync(bundledHookPath)) {
|
|
6047
7065
|
console.warn(`[Daemon] Bundled hook not found: ${bundledHookPath}`);
|
|
6048
7066
|
continue;
|
|
6049
7067
|
}
|
|
6050
|
-
const hookContent =
|
|
6051
|
-
if (
|
|
6052
|
-
const existingContent =
|
|
7068
|
+
const hookContent = fs13.readFileSync(bundledHookPath, "utf-8");
|
|
7069
|
+
if (fs13.existsSync(hookPath)) {
|
|
7070
|
+
const existingContent = fs13.readFileSync(hookPath, "utf-8");
|
|
6053
7071
|
if (existingContent === hookContent) {
|
|
6054
7072
|
continue;
|
|
6055
7073
|
}
|
|
6056
7074
|
}
|
|
6057
|
-
|
|
7075
|
+
fs13.writeFileSync(hookPath, hookContent, { mode: 493 });
|
|
6058
7076
|
console.log(`[Daemon] Installed git hook: ${hookName}`);
|
|
6059
7077
|
} catch (error) {
|
|
6060
7078
|
console.warn(`[Daemon] Failed to install ${hookName} hook:`, error instanceof Error ? error.message : error);
|
|
@@ -6069,7 +7087,7 @@ var Daemon = class _Daemon {
|
|
|
6069
7087
|
*/
|
|
6070
7088
|
async cacheDeviceId(deviceId) {
|
|
6071
7089
|
try {
|
|
6072
|
-
const config = await (0,
|
|
7090
|
+
const config = await (0, import_core10.loadConfig)();
|
|
6073
7091
|
if (!config) {
|
|
6074
7092
|
console.warn("[Daemon] Cannot cache device ID - no config found");
|
|
6075
7093
|
return;
|
|
@@ -6082,12 +7100,39 @@ var Daemon = class _Daemon {
|
|
|
6082
7100
|
device_id: deviceId,
|
|
6083
7101
|
machine_id: this.machineId
|
|
6084
7102
|
};
|
|
6085
|
-
await (0,
|
|
7103
|
+
await (0, import_core10.saveConfig)(updatedConfig);
|
|
6086
7104
|
console.log(`[Daemon] Cached device ID to config: ${deviceId}`);
|
|
6087
7105
|
} catch (error) {
|
|
6088
7106
|
console.warn("[Daemon] Failed to cache device ID:", error instanceof Error ? error.message : error);
|
|
6089
7107
|
}
|
|
6090
7108
|
}
|
|
7109
|
+
/**
|
|
7110
|
+
* EP956: Cleanup module worktree when module moves to done
|
|
7111
|
+
*
|
|
7112
|
+
* Runs asynchronously to reduce burden on the state change handler.
|
|
7113
|
+
* Steps:
|
|
7114
|
+
* 1. Stop dev server for the module
|
|
7115
|
+
* 2. Log worktree path for potential removal (actual removal TBD)
|
|
7116
|
+
*
|
|
7117
|
+
* Note: Worktree removal requires git worktree remove command, which
|
|
7118
|
+
* needs careful handling to avoid data loss. For now, just stop the
|
|
7119
|
+
* dev server and log the path.
|
|
7120
|
+
*/
|
|
7121
|
+
async cleanupModuleWorktree(moduleUid) {
|
|
7122
|
+
console.log(`[Daemon] EP956: Starting async cleanup for ${moduleUid}`);
|
|
7123
|
+
try {
|
|
7124
|
+
await stopDevServer(moduleUid);
|
|
7125
|
+
console.log(`[Daemon] EP956: Dev server stopped for ${moduleUid}`);
|
|
7126
|
+
const worktree = await getWorktreeInfoForModule(moduleUid);
|
|
7127
|
+
if (worktree?.exists) {
|
|
7128
|
+
console.log(`[Daemon] EP956: Worktree for ${moduleUid} at ${worktree.path} is ready for cleanup`);
|
|
7129
|
+
}
|
|
7130
|
+
console.log(`[Daemon] EP956: Async cleanup complete for ${moduleUid}`);
|
|
7131
|
+
} catch (error) {
|
|
7132
|
+
console.error(`[Daemon] EP956: Cleanup error for ${moduleUid}:`, error instanceof Error ? error.message : error);
|
|
7133
|
+
throw error;
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
6091
7136
|
/**
|
|
6092
7137
|
* EP819: Auto-start tunnels for active local modules on daemon connect/reconnect
|
|
6093
7138
|
*
|
|
@@ -6097,7 +7142,7 @@ var Daemon = class _Daemon {
|
|
|
6097
7142
|
async autoStartTunnelsForProject(projectPath, projectUid) {
|
|
6098
7143
|
console.log(`[Daemon] EP819: Checking for active local modules to auto-start tunnels...`);
|
|
6099
7144
|
try {
|
|
6100
|
-
const config = await (0,
|
|
7145
|
+
const config = await (0, import_core10.loadConfig)();
|
|
6101
7146
|
if (!config?.access_token) {
|
|
6102
7147
|
console.warn(`[Daemon] EP819: No access token, skipping tunnel auto-start`);
|
|
6103
7148
|
return;
|
|
@@ -6114,6 +7159,33 @@ var Daemon = class _Daemon {
|
|
|
6114
7159
|
const modules = data.modules || [];
|
|
6115
7160
|
const tunnelManager = getTunnelManager();
|
|
6116
7161
|
await tunnelManager.initialize();
|
|
7162
|
+
const activeTunnelUids = tunnelManager.getActiveModuleUids();
|
|
7163
|
+
const validModuleUids = new Set(
|
|
7164
|
+
modules.filter(
|
|
7165
|
+
(m) => m.dev_mode === "local" && (!m.checkout_machine_id || m.checkout_machine_id === this.deviceId)
|
|
7166
|
+
).map((m) => m.uid)
|
|
7167
|
+
);
|
|
7168
|
+
const orphanedTunnels = activeTunnelUids.filter((uid) => !validModuleUids.has(uid));
|
|
7169
|
+
if (orphanedTunnels.length > 0) {
|
|
7170
|
+
console.log(`[Daemon] EP956: Found ${orphanedTunnels.length} orphaned tunnels to stop: ${orphanedTunnels.join(", ")}`);
|
|
7171
|
+
for (const orphanUid of orphanedTunnels) {
|
|
7172
|
+
try {
|
|
7173
|
+
await tunnelManager.stopTunnel(orphanUid);
|
|
7174
|
+
releasePort(orphanUid);
|
|
7175
|
+
console.log(`[Daemon] EP956: Stopped orphaned tunnel and released port for ${orphanUid}`);
|
|
7176
|
+
try {
|
|
7177
|
+
await fetchWithAuth(`${apiUrl}/api/modules/${orphanUid}/tunnel`, {
|
|
7178
|
+
method: "POST",
|
|
7179
|
+
body: JSON.stringify({ tunnel_url: null })
|
|
7180
|
+
});
|
|
7181
|
+
} catch (err) {
|
|
7182
|
+
console.warn(`[Daemon] EP956: Failed to clear tunnel URL for ${orphanUid}:`, err instanceof Error ? err.message : err);
|
|
7183
|
+
}
|
|
7184
|
+
} catch (err) {
|
|
7185
|
+
console.error(`[Daemon] EP956: Failed to stop orphaned tunnel ${orphanUid}:`, err instanceof Error ? err.message : err);
|
|
7186
|
+
}
|
|
7187
|
+
}
|
|
7188
|
+
}
|
|
6117
7189
|
const localModulesNeedingTunnel = modules.filter(
|
|
6118
7190
|
(m) => m.dev_mode === "local" && (!m.checkout_machine_id || m.checkout_machine_id === this.deviceId) && !tunnelManager.hasTunnel(m.uid)
|
|
6119
7191
|
);
|
|
@@ -6121,11 +7193,20 @@ var Daemon = class _Daemon {
|
|
|
6121
7193
|
console.log(`[Daemon] EP819: No local modules need tunnel auto-start`);
|
|
6122
7194
|
return;
|
|
6123
7195
|
}
|
|
6124
|
-
console.log(`[Daemon]
|
|
7196
|
+
console.log(`[Daemon] EP956: Found ${localModulesNeedingTunnel.length} local modules needing tunnels`);
|
|
6125
7197
|
for (const module2 of localModulesNeedingTunnel) {
|
|
6126
7198
|
const moduleUid = module2.uid;
|
|
6127
|
-
const
|
|
6128
|
-
|
|
7199
|
+
const worktree = await getWorktreeInfoForModule(moduleUid);
|
|
7200
|
+
if (!worktree) {
|
|
7201
|
+
console.warn(`[Daemon] EP956: Cannot resolve worktree for ${moduleUid} (missing config slugs)`);
|
|
7202
|
+
continue;
|
|
7203
|
+
}
|
|
7204
|
+
if (!worktree.exists) {
|
|
7205
|
+
console.log(`[Daemon] EP956: No worktree for ${moduleUid} at ${worktree.path}, skipping`);
|
|
7206
|
+
continue;
|
|
7207
|
+
}
|
|
7208
|
+
const port = allocatePort(moduleUid);
|
|
7209
|
+
console.log(`[Daemon] EP956: Auto-starting tunnel for ${moduleUid} at ${worktree.path} on port ${port}`);
|
|
6129
7210
|
const reportTunnelStatus = async (statusData) => {
|
|
6130
7211
|
try {
|
|
6131
7212
|
const statusResponse = await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}/tunnel`, {
|
|
@@ -6144,25 +7225,26 @@ var Daemon = class _Daemon {
|
|
|
6144
7225
|
(async () => {
|
|
6145
7226
|
await this.withTunnelLock(moduleUid, async () => {
|
|
6146
7227
|
if (tunnelManager.hasTunnel(moduleUid)) {
|
|
6147
|
-
console.log(`[Daemon]
|
|
7228
|
+
console.log(`[Daemon] EP956: Tunnel already running for ${moduleUid}, skipping auto-start`);
|
|
6148
7229
|
return;
|
|
6149
7230
|
}
|
|
6150
7231
|
const MAX_RETRIES = 3;
|
|
6151
|
-
const RETRY_DELAY_MS =
|
|
7232
|
+
const RETRY_DELAY_MS = 3e3;
|
|
6152
7233
|
await reportTunnelStatus({
|
|
6153
7234
|
tunnel_started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6154
7235
|
tunnel_error: null
|
|
6155
7236
|
});
|
|
6156
7237
|
try {
|
|
6157
|
-
console.log(`[Daemon]
|
|
6158
|
-
const devServerResult = await ensureDevServer(
|
|
7238
|
+
console.log(`[Daemon] EP956: Ensuring dev server is running for ${moduleUid} at ${worktree.path}...`);
|
|
7239
|
+
const devServerResult = await ensureDevServer(worktree.path, port, moduleUid);
|
|
6159
7240
|
if (!devServerResult.success) {
|
|
6160
7241
|
const errorMsg2 = `Dev server failed to start: ${devServerResult.error}`;
|
|
6161
|
-
console.error(`[Daemon]
|
|
7242
|
+
console.error(`[Daemon] EP956: ${errorMsg2}`);
|
|
6162
7243
|
await reportTunnelStatus({ tunnel_error: errorMsg2 });
|
|
7244
|
+
releasePort(moduleUid);
|
|
6163
7245
|
return;
|
|
6164
7246
|
}
|
|
6165
|
-
console.log(`[Daemon]
|
|
7247
|
+
console.log(`[Daemon] EP956: Dev server ready on port ${port}`);
|
|
6166
7248
|
let lastError;
|
|
6167
7249
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
6168
7250
|
console.log(`[Daemon] EP819: Starting tunnel for ${moduleUid} (attempt ${attempt}/${MAX_RETRIES})...`);
|
|
@@ -6193,16 +7275,18 @@ var Daemon = class _Daemon {
|
|
|
6193
7275
|
console.warn(`[Daemon] EP819: Tunnel start attempt ${attempt} failed: ${lastError}`);
|
|
6194
7276
|
if (attempt < MAX_RETRIES) {
|
|
6195
7277
|
console.log(`[Daemon] EP819: Retrying in ${RETRY_DELAY_MS}ms...`);
|
|
6196
|
-
await new Promise((
|
|
7278
|
+
await new Promise((resolve3) => setTimeout(resolve3, RETRY_DELAY_MS));
|
|
6197
7279
|
}
|
|
6198
7280
|
}
|
|
6199
7281
|
const errorMsg = `Tunnel failed after ${MAX_RETRIES} attempts: ${lastError}`;
|
|
6200
|
-
console.error(`[Daemon]
|
|
7282
|
+
console.error(`[Daemon] EP956: ${errorMsg}`);
|
|
6201
7283
|
await reportTunnelStatus({ tunnel_error: errorMsg });
|
|
7284
|
+
releasePort(moduleUid);
|
|
6202
7285
|
} catch (error) {
|
|
6203
7286
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
6204
|
-
console.error(`[Daemon]
|
|
7287
|
+
console.error(`[Daemon] EP956: Async tunnel startup error:`, error);
|
|
6205
7288
|
await reportTunnelStatus({ tunnel_error: errorMsg });
|
|
7289
|
+
releasePort(moduleUid);
|
|
6206
7290
|
}
|
|
6207
7291
|
});
|
|
6208
7292
|
})();
|
|
@@ -6249,7 +7333,7 @@ var Daemon = class _Daemon {
|
|
|
6249
7333
|
}
|
|
6250
7334
|
this.healthCheckInProgress = true;
|
|
6251
7335
|
try {
|
|
6252
|
-
const config = await (0,
|
|
7336
|
+
const config = await (0, import_core10.loadConfig)();
|
|
6253
7337
|
if (config?.access_token) {
|
|
6254
7338
|
await this.performHealthChecks(config);
|
|
6255
7339
|
}
|
|
@@ -6306,6 +7390,91 @@ var Daemon = class _Daemon {
|
|
|
6306
7390
|
console.error("[Daemon] EP904: Failed to clean up orphaned tunnels:", error);
|
|
6307
7391
|
}
|
|
6308
7392
|
}
|
|
7393
|
+
/**
|
|
7394
|
+
* EP957: Audit worktrees on daemon startup to detect orphaned worktrees
|
|
7395
|
+
*
|
|
7396
|
+
* Compares local worktrees against active modules (doing/review state).
|
|
7397
|
+
* Logs orphaned worktrees for visibility but does NOT auto-cleanup - that's
|
|
7398
|
+
* the user's decision via `episoda release` or manual cleanup.
|
|
7399
|
+
*
|
|
7400
|
+
* This is a non-blocking diagnostic - failures are logged but don't affect startup.
|
|
7401
|
+
*/
|
|
7402
|
+
async auditWorktreesOnStartup() {
|
|
7403
|
+
try {
|
|
7404
|
+
const projects = getAllProjects();
|
|
7405
|
+
for (const project of projects) {
|
|
7406
|
+
await this.auditProjectWorktrees(project.path);
|
|
7407
|
+
}
|
|
7408
|
+
} catch (error) {
|
|
7409
|
+
console.warn("[Daemon] EP957: Worktree audit failed (non-blocking):", error);
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7412
|
+
/**
|
|
7413
|
+
* EP957: Audit worktrees for a single project
|
|
7414
|
+
*/
|
|
7415
|
+
async auditProjectWorktrees(projectPath) {
|
|
7416
|
+
try {
|
|
7417
|
+
const projectRoot = await findProjectRoot(projectPath);
|
|
7418
|
+
if (!projectRoot) {
|
|
7419
|
+
return;
|
|
7420
|
+
}
|
|
7421
|
+
const manager = new WorktreeManager(projectRoot);
|
|
7422
|
+
if (!await manager.initialize()) {
|
|
7423
|
+
return;
|
|
7424
|
+
}
|
|
7425
|
+
const config = manager.getConfig();
|
|
7426
|
+
if (!config) {
|
|
7427
|
+
return;
|
|
7428
|
+
}
|
|
7429
|
+
const worktrees = manager.listWorktrees();
|
|
7430
|
+
if (worktrees.length === 0) {
|
|
7431
|
+
return;
|
|
7432
|
+
}
|
|
7433
|
+
const activeModuleUids = await this.fetchActiveModuleUids(config.projectId);
|
|
7434
|
+
if (activeModuleUids === null) {
|
|
7435
|
+
return;
|
|
7436
|
+
}
|
|
7437
|
+
const { orphaned } = manager.auditWorktrees(activeModuleUids);
|
|
7438
|
+
if (orphaned.length > 0) {
|
|
7439
|
+
console.log(`[Daemon] EP957: Found ${orphaned.length} orphaned worktree(s) in ${config.workspaceSlug}/${config.projectSlug}:`);
|
|
7440
|
+
for (const w of orphaned) {
|
|
7441
|
+
console.log(` - ${w.moduleUid} (branch: ${w.branchName})`);
|
|
7442
|
+
}
|
|
7443
|
+
console.log('[Daemon] EP957: Run "episoda release <module>" to clean up');
|
|
7444
|
+
}
|
|
7445
|
+
} catch (error) {
|
|
7446
|
+
console.warn(`[Daemon] EP957: Failed to audit ${projectPath}:`, error);
|
|
7447
|
+
}
|
|
7448
|
+
}
|
|
7449
|
+
/**
|
|
7450
|
+
* EP957: Fetch UIDs of active modules (doing/review state) for a project
|
|
7451
|
+
* Returns null if fetch fails (non-blocking)
|
|
7452
|
+
*/
|
|
7453
|
+
async fetchActiveModuleUids(projectId) {
|
|
7454
|
+
try {
|
|
7455
|
+
const config = await (0, import_core10.loadConfig)();
|
|
7456
|
+
if (!config?.access_token || !config?.api_url) {
|
|
7457
|
+
return null;
|
|
7458
|
+
}
|
|
7459
|
+
const url = `${config.api_url}/api/modules?project_id=${projectId}&state=doing,review`;
|
|
7460
|
+
const response = await fetch(url, {
|
|
7461
|
+
headers: {
|
|
7462
|
+
"Authorization": `Bearer ${config.access_token}`,
|
|
7463
|
+
"Content-Type": "application/json"
|
|
7464
|
+
}
|
|
7465
|
+
});
|
|
7466
|
+
if (!response.ok) {
|
|
7467
|
+
return null;
|
|
7468
|
+
}
|
|
7469
|
+
const data = await response.json();
|
|
7470
|
+
if (!data.success || !data.modules) {
|
|
7471
|
+
return null;
|
|
7472
|
+
}
|
|
7473
|
+
return data.modules.map((m) => m.uid).filter((uid) => !!uid);
|
|
7474
|
+
} catch {
|
|
7475
|
+
return null;
|
|
7476
|
+
}
|
|
7477
|
+
}
|
|
6309
7478
|
// EP843: syncTunnelsWithActiveModules() removed - replaced by push-based state sync
|
|
6310
7479
|
// See module_state_changed handler for the new implementation
|
|
6311
7480
|
/**
|
|
@@ -6389,7 +7558,7 @@ var Daemon = class _Daemon {
|
|
|
6389
7558
|
const tunnelManager = getTunnelManager();
|
|
6390
7559
|
try {
|
|
6391
7560
|
await tunnelManager.stopTunnel(moduleUid);
|
|
6392
|
-
const config = await (0,
|
|
7561
|
+
const config = await (0, import_core10.loadConfig)();
|
|
6393
7562
|
if (!config?.access_token) {
|
|
6394
7563
|
console.error(`[Daemon] EP833: No access token for tunnel restart`);
|
|
6395
7564
|
return;
|
|
@@ -6493,11 +7662,11 @@ var Daemon = class _Daemon {
|
|
|
6493
7662
|
* Used to clean up orphaned cloudflared processes
|
|
6494
7663
|
*/
|
|
6495
7664
|
async killProcessesByPattern(pattern) {
|
|
6496
|
-
const { exec } = await import("child_process");
|
|
6497
|
-
const { promisify } = await import("util");
|
|
6498
|
-
const
|
|
7665
|
+
const { exec: exec2 } = await import("child_process");
|
|
7666
|
+
const { promisify: promisify2 } = await import("util");
|
|
7667
|
+
const execAsync2 = promisify2(exec2);
|
|
6499
7668
|
try {
|
|
6500
|
-
const { stdout } = await
|
|
7669
|
+
const { stdout } = await execAsync2(`pgrep -f "${pattern}"`);
|
|
6501
7670
|
const pids = stdout.trim().split("\n").filter(Boolean);
|
|
6502
7671
|
if (pids.length === 0) {
|
|
6503
7672
|
return 0;
|
|
@@ -6505,7 +7674,7 @@ var Daemon = class _Daemon {
|
|
|
6505
7674
|
let killed = 0;
|
|
6506
7675
|
for (const pid of pids) {
|
|
6507
7676
|
try {
|
|
6508
|
-
await
|
|
7677
|
+
await execAsync2(`kill ${pid}`);
|
|
6509
7678
|
killed++;
|
|
6510
7679
|
} catch {
|
|
6511
7680
|
}
|
|
@@ -6523,11 +7692,11 @@ var Daemon = class _Daemon {
|
|
|
6523
7692
|
* Used to clean up dev servers when stopping tunnels
|
|
6524
7693
|
*/
|
|
6525
7694
|
async killProcessOnPort(port) {
|
|
6526
|
-
const { exec } = await import("child_process");
|
|
6527
|
-
const { promisify } = await import("util");
|
|
6528
|
-
const
|
|
7695
|
+
const { exec: exec2 } = await import("child_process");
|
|
7696
|
+
const { promisify: promisify2 } = await import("util");
|
|
7697
|
+
const execAsync2 = promisify2(exec2);
|
|
6529
7698
|
try {
|
|
6530
|
-
const { stdout } = await
|
|
7699
|
+
const { stdout } = await execAsync2(`lsof -ti :${port}`);
|
|
6531
7700
|
const pids = stdout.trim().split("\n").filter(Boolean);
|
|
6532
7701
|
if (pids.length === 0) {
|
|
6533
7702
|
return false;
|
|
@@ -6535,7 +7704,7 @@ var Daemon = class _Daemon {
|
|
|
6535
7704
|
let killed = false;
|
|
6536
7705
|
for (const pid of pids) {
|
|
6537
7706
|
try {
|
|
6538
|
-
await
|
|
7707
|
+
await execAsync2(`kill ${pid}`);
|
|
6539
7708
|
killed = true;
|
|
6540
7709
|
} catch {
|
|
6541
7710
|
}
|
|
@@ -6553,11 +7722,11 @@ var Daemon = class _Daemon {
|
|
|
6553
7722
|
* Returns process info for cloudflared processes not tracked by TunnelManager
|
|
6554
7723
|
*/
|
|
6555
7724
|
async findOrphanedCloudflaredProcesses() {
|
|
6556
|
-
const { exec } = await import("child_process");
|
|
6557
|
-
const { promisify } = await import("util");
|
|
6558
|
-
const
|
|
7725
|
+
const { exec: exec2 } = await import("child_process");
|
|
7726
|
+
const { promisify: promisify2 } = await import("util");
|
|
7727
|
+
const execAsync2 = promisify2(exec2);
|
|
6559
7728
|
try {
|
|
6560
|
-
const { stdout } = await
|
|
7729
|
+
const { stdout } = await execAsync2("ps aux | grep cloudflared | grep -v grep");
|
|
6561
7730
|
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
6562
7731
|
const tunnelManager = getTunnelManager();
|
|
6563
7732
|
const trackedModules = new Set(tunnelManager.getAllTunnels().map((t) => t.moduleUid));
|
|
@@ -6617,6 +7786,7 @@ var Daemon = class _Daemon {
|
|
|
6617
7786
|
} catch (error) {
|
|
6618
7787
|
console.error("[Daemon] Failed to stop tunnels:", error);
|
|
6619
7788
|
}
|
|
7789
|
+
clearAllPorts();
|
|
6620
7790
|
try {
|
|
6621
7791
|
const agentManager = getAgentManager();
|
|
6622
7792
|
await agentManager.stopAllSessions();
|
|
@@ -6635,8 +7805,8 @@ var Daemon = class _Daemon {
|
|
|
6635
7805
|
await this.shutdown();
|
|
6636
7806
|
try {
|
|
6637
7807
|
const pidPath = getPidFilePath();
|
|
6638
|
-
if (
|
|
6639
|
-
|
|
7808
|
+
if (fs13.existsSync(pidPath)) {
|
|
7809
|
+
fs13.unlinkSync(pidPath);
|
|
6640
7810
|
console.log("[Daemon] PID file cleaned up");
|
|
6641
7811
|
}
|
|
6642
7812
|
} catch (error) {
|