episoda 0.2.19 → 0.2.20

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.
@@ -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 execAsync = (0, util_1.promisify)(child_process_1.exec);
305
- var GitExecutor2 = class {
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 execAsync("git branch --list", { cwd, timeout: options?.timeout || 1e4 });
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 execAsync(`git ls-remote --heads origin ${command.branch}`, { cwd, timeout: options?.timeout || 1e4 });
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 execAsync(`git cherry origin/${baseBranch} ${command.branch}`, { cwd, timeout: options?.timeout || 1e4 });
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 execAsync(`git rev-list --count origin/${baseBranch}..${command.branch}`, { cwd, timeout: options?.timeout || 1e4 });
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 execAsync("git branch -a", { cwd, timeout: options?.timeout || 1e4 });
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 execAsync("git branch --show-current", { cwd, timeout: options?.timeout || 1e4 });
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 execAsync("git status --porcelain", { cwd, timeout: options?.timeout || 1e4 });
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 execAsync('git log origin/main..HEAD --format="%H|%s|%an"', { cwd, timeout: options?.timeout || 1e4 });
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 execAsync(`git log ${baseBranch}.."${command.branch}" --pretty=format:"%H|%an|%ae|%aI|%s" -n ${limit} --`, { cwd, timeout: options?.timeout || 1e4 });
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 execAsync(`git log "${command.branch}" --pretty=format:"%H|%an|%ae|%aI|%s" -n ${limit} --`, { cwd, timeout: options?.timeout || 1e4 });
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 execAsync(`git log "origin/${command.branch}" --pretty=format:"%H" -n ${limit} --`, { cwd, timeout: options?.timeout || 1e4 });
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 execAsync("git rev-parse --abbrev-ref HEAD", { cwd });
1072
+ const { stdout: currentBranchOut } = await execAsync2("git rev-parse --abbrev-ref HEAD", { cwd });
1060
1073
  const currentBranch = currentBranchOut.trim();
1061
- const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd });
1074
+ const { stdout: statusOutput } = await execAsync2("git status --porcelain", { cwd });
1062
1075
  if (statusOutput.trim()) {
1063
1076
  try {
1064
- await execAsync("git add -A", { cwd });
1065
- const { stdout: stashHash } = await execAsync('git stash create -m "episoda-move-to-module"', { cwd });
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 execAsync(`git stash store -m "episoda-move-to-module" ${stashHash.trim()}`, { cwd });
1068
- await execAsync("git reset --hard HEAD", { cwd });
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 execAsync("git checkout main", { cwd });
1088
+ await execAsync2("git checkout main", { cwd });
1076
1089
  }
1077
1090
  let branchExists = false;
1078
1091
  try {
1079
- await execAsync(`git rev-parse --verify ${targetBranch}`, { cwd });
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 execAsync(`git checkout -b ${targetBranch}`, { cwd });
1098
+ await execAsync2(`git checkout -b ${targetBranch}`, { cwd });
1086
1099
  } else {
1087
- await execAsync(`git checkout ${targetBranch}`, { cwd });
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 execAsync(`git merge ${currentBranch} ${mergeStrategy} --no-edit`, { cwd });
1104
+ await execAsync2(`git merge ${currentBranch} ${mergeStrategy} --no-edit`, { cwd });
1092
1105
  } catch (mergeError) {
1093
- const { stdout: conflictStatus } = await execAsync("git status --porcelain", { cwd });
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 execAsync("git diff --name-only --diff-filter=U", { cwd });
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 execAsync(`git checkout --${conflictResolution} "${file}"`, { cwd });
1100
- await execAsync(`git add "${file}"`, { cwd });
1112
+ await execAsync2(`git checkout --${conflictResolution} "${file}"`, { cwd });
1113
+ await execAsync2(`git add "${file}"`, { cwd });
1101
1114
  }
1102
- await execAsync("git commit --no-edit", { cwd });
1115
+ await execAsync2("git commit --no-edit", { cwd });
1103
1116
  } else {
1104
- await execAsync("git merge --abort", { cwd });
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 execAsync(`git log --format=%H ${targetBranch} | grep ${sha}`, { cwd }).catch(() => ({ stdout: "" }));
1136
+ const { stdout: logOutput } = await execAsync2(`git log --format=%H ${targetBranch} | grep ${sha}`, { cwd }).catch(() => ({ stdout: "" }));
1124
1137
  if (!logOutput.trim()) {
1125
- await execAsync(`git cherry-pick ${sha}`, { cwd });
1138
+ await execAsync2(`git cherry-pick ${sha}`, { cwd });
1126
1139
  cherryPickedCommits.push(sha);
1127
1140
  }
1128
1141
  } catch (err) {
1129
- await execAsync("git cherry-pick --abort", { cwd }).catch(() => {
1142
+ await execAsync2("git cherry-pick --abort", { cwd }).catch(() => {
1130
1143
  });
1131
1144
  }
1132
1145
  }
1133
- await execAsync("git checkout main", { cwd });
1134
- await execAsync("git reset --hard origin/main", { cwd });
1135
- await execAsync(`git checkout ${targetBranch}`, { cwd });
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 execAsync("git stash pop", { cwd });
1152
+ await execAsync2("git stash pop", { cwd });
1140
1153
  } catch (stashError) {
1141
- const { stdout: conflictStatus } = await execAsync("git status --porcelain", { cwd });
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 execAsync("git diff --name-only --diff-filter=U", { cwd });
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 execAsync(`git checkout --${conflictResolution} "${file}"`, { cwd });
1148
- await execAsync(`git add "${file}"`, { cwd });
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 execAsync("git stash list", { cwd });
1179
+ const { stdout: stashList } = await execAsync2("git stash list", { cwd });
1167
1180
  if (stashList.includes("episoda-move-to-module")) {
1168
- await execAsync("git stash pop", { cwd });
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 execAsync("git rev-parse --abbrev-ref HEAD", { cwd });
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 execAsync("git status --porcelain", { cwd });
1209
+ const { stdout: statusOutput } = await execAsync2("git status --porcelain", { cwd });
1197
1210
  if (statusOutput.trim()) {
1198
1211
  try {
1199
- await execAsync("git stash --include-untracked", { cwd });
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 execAsync("git fetch origin", { cwd });
1205
- await execAsync(`git reset --hard origin/${branch}`, { cwd });
1217
+ await execAsync2("git fetch origin", { cwd });
1218
+ await execAsync2(`git reset --hard origin/${branch}`, { cwd });
1206
1219
  try {
1207
- await execAsync("git clean -fd", { cwd });
1220
+ await execAsync2("git clean -fd", { cwd });
1208
1221
  } catch (cleanError) {
1209
1222
  }
1210
1223
  try {
1211
- await execAsync("git stash drop", { cwd });
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 execAsync("git fetch origin", { cwd, timeout: options?.timeout || 3e4 });
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 execAsync(`git rev-list --count ${command.branch}..origin/main`, { cwd, timeout: options?.timeout || 1e4 });
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 execAsync(`git rev-list --count origin/main..${command.branch}`, { cwd, timeout: options?.timeout || 1e4 });
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 execAsync("git branch --show-current", { cwd, timeout: 5e3 });
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 execAsync("git fetch origin main", { cwd, timeout: options?.timeout || 3e4 });
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 execAsync("git status --porcelain", { cwd, timeout: 5e3 });
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 execAsync("git checkout main", { cwd, timeout: options?.timeout || 1e4 });
1339
+ await execAsync2("git checkout main", { cwd, timeout: options?.timeout || 1e4 });
1327
1340
  }
1328
1341
  try {
1329
- await execAsync("git pull origin main", { cwd, timeout: options?.timeout || 3e4 });
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 execAsync("git merge --abort", { cwd, timeout: 5e3 }).catch(() => {
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 execAsync(`git checkout "${currentBranch}"`, { cwd, timeout: options?.timeout || 1e4 });
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 execAsync("git status --porcelain", { cwd, timeout: 5e3 });
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 execAsync("git branch --show-current", { cwd, timeout: 5e3 });
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 execAsync(`git checkout "${command.branch}"`, { cwd, timeout: options?.timeout || 1e4 });
1400
+ await execAsync2(`git checkout "${command.branch}"`, { cwd, timeout: options?.timeout || 1e4 });
1388
1401
  }
1389
- await execAsync("git fetch origin main", { cwd, timeout: options?.timeout || 3e4 });
1402
+ await execAsync2("git fetch origin main", { cwd, timeout: options?.timeout || 3e4 });
1390
1403
  try {
1391
- await execAsync("git rebase origin/main", { cwd, timeout: options?.timeout || 6e4 });
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 execAsync("git diff --name-only --diff-filter=U", { cwd, timeout: 5e3 });
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 execAsync("git status --porcelain", { cwd, timeout: 5e3 });
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 execAsync("git rebase --abort", { cwd, timeout: options?.timeout || 1e4 });
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 execAsync("git add -A", { cwd, timeout: 5e3 });
1473
- await execAsync("git rebase --continue", { cwd, timeout: options?.timeout || 6e4 });
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 execAsync("git diff --name-only --diff-filter=U", { cwd, timeout: 5e3 });
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 execAsync("git rev-parse --git-dir", { cwd, timeout: 5e3 });
1539
+ const { stdout: gitDir } = await execAsync2("git rev-parse --git-dir", { cwd, timeout: 5e3 });
1527
1540
  const gitDirPath = gitDir.trim();
1528
- const fs12 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
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 fs12.access(rebaseMergePath);
1545
+ await fs14.access(rebaseMergePath);
1533
1546
  inRebase = true;
1534
1547
  } catch {
1535
1548
  try {
1536
- await fs12.access(rebaseApplyPath);
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 execAsync("git status", { cwd, timeout: 5e3 });
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 execAsync("git diff --name-only --diff-filter=U", { cwd, timeout: 5e3 });
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 execAsync(command, execOptions);
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 execAsync("git --version", { timeout: 5e3 });
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 execAsync("git rev-parse --git-dir", { cwd, timeout: 5e3 });
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 execAsync("git rev-parse --show-toplevel", {
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 = GitExecutor2;
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((resolve2, reject) => {
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
- resolve2();
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((resolve2, reject) => {
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
- resolve2();
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
- if (this.reconnectAttempts >= 1) {
2027
- console.error('[EpisodaClient] Connection lost. Retry failed. Check server status or restart with "episoda dev".');
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("[EpisodaClient] Connection lost, retrying in 1 second...");
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 = loadConfig3;
2477
+ exports2.loadConfig = loadConfig5;
2142
2478
  exports2.saveConfig = saveConfig2;
2143
2479
  exports2.validateToken = validateToken;
2144
- var fs12 = __importStar(require("fs"));
2145
- var path13 = __importStar(require("path"));
2146
- var os5 = __importStar(require("os"));
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 || path13.join(os5.homedir(), ".episoda");
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 path13.join(getConfigDir6(), DEFAULT_CONFIG_FILE);
2492
+ return path15.join(getConfigDir6(), DEFAULT_CONFIG_FILE);
2157
2493
  }
2158
2494
  function ensureConfigDir(configPath) {
2159
- const dir = path13.dirname(configPath);
2160
- const isNew = !fs12.existsSync(dir);
2495
+ const dir = path15.dirname(configPath);
2496
+ const isNew = !fs14.existsSync(dir);
2161
2497
  if (isNew) {
2162
- fs12.mkdirSync(dir, { recursive: true, mode: 448 });
2498
+ fs14.mkdirSync(dir, { recursive: true, mode: 448 });
2163
2499
  }
2164
2500
  if (process.platform === "darwin") {
2165
- const nosyncPath = path13.join(dir, ".nosync");
2166
- if (isNew || !fs12.existsSync(nosyncPath)) {
2501
+ const nosyncPath = path15.join(dir, ".nosync");
2502
+ if (isNew || !fs14.existsSync(nosyncPath)) {
2167
2503
  try {
2168
- fs12.writeFileSync(nosyncPath, "", { mode: 384 });
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 loadConfig3(configPath) {
2514
+ async function loadConfig5(configPath) {
2179
2515
  const fullPath = getConfigPath(configPath);
2180
- if (!fs12.existsSync(fullPath)) {
2516
+ if (!fs14.existsSync(fullPath)) {
2181
2517
  return null;
2182
2518
  }
2183
2519
  try {
2184
- const content = fs12.readFileSync(fullPath, "utf8");
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
- fs12.writeFileSync(fullPath, content, { mode: 384 });
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((resolve2) => {
2649
+ return new Promise((resolve3) => {
2309
2650
  const server = net2.createServer();
2310
2651
  server.once("error", (err) => {
2311
2652
  if (err.code === "EADDRINUSE") {
2312
- resolve2(true);
2653
+ resolve3(true);
2313
2654
  } else {
2314
- resolve2(false);
2655
+ resolve3(false);
2315
2656
  }
2316
2657
  });
2317
2658
  server.once("listening", () => {
2318
2659
  server.close();
2319
- resolve2(false);
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.18",
2684
+ version: "0.2.19",
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((resolve2, reject) => {
2983
+ return new Promise((resolve3, reject) => {
2634
2984
  this.server.listen(socketPath, () => {
2635
2985
  fs3.chmodSync(socketPath, 384);
2636
- resolve2();
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((resolve2) => {
2997
+ return new Promise((resolve3) => {
2648
2998
  this.server.close(() => {
2649
2999
  if (fs3.existsSync(socketPath)) {
2650
3000
  fs3.unlinkSync(socketPath);
2651
3001
  }
2652
- resolve2();
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 import_core7 = __toESM(require_dist());
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((resolve2) => {
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
- resolve2(result);
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/tunnel/cloudflared-manager.ts
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, import_child_process4.spawnSync)(command, [binaryName], { encoding: "utf-8" });
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, import_child_process4.spawnSync)(binaryPath, ["version"], { encoding: "utf-8", timeout: 5e3 });
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((resolve2, reject) => {
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
- resolve2();
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 import_child_process5 = require("child_process");
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, import_child_process5.execSync)("pgrep -f cloudflared", { encoding: "utf8" });
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, import_child_process5.execSync)(`ps -p ${pid} -o args=`, { encoding: "utf8" }).trim();
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((resolve2) => setTimeout(resolve2, 500));
4132
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
3688
4133
  if (this.isProcessRunning(pid)) {
3689
4134
  this.killByPid(pid, "SIGKILL");
3690
- await new Promise((resolve2) => setTimeout(resolve2, 200));
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((resolve2) => setTimeout(resolve2, 1e3));
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((resolve2) => setTimeout(resolve2, 500));
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((resolve2) => {
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, import_child_process5.spawn)(this.cloudflaredPath, [
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
- resolve2({ success: true, url: tunnelInfo.url });
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
- resolve2({ success: false, error: errorMsg });
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
- resolve2({ success: false, error: error.message });
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
- resolve2({ success: false, error: errorMsg });
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((resolve2) => setTimeout(resolve2, 500));
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((resolve2) => setTimeout(resolve2, 300));
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((resolve2) => setTimeout(resolve2, 1e3));
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((resolve2) => {
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
- resolve2();
4472
+ resolve3();
4028
4473
  }, 3e3);
4029
4474
  tunnel.process.once("exit", () => {
4030
4475
  clearTimeout(timeout);
4031
- resolve2();
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 import_core5 = __toESM(require_dist());
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, import_core5.loadConfig)();
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 import_child_process6 = require("child_process");
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, import_child_process6.execSync)(`"${binaryPath}" --version`, {
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, import_child_process6.execSync)("which claude", {
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, import_child_process6.execSync)("npx --yes @anthropic-ai/claude-code --version", {
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 import_child_process7 = require("child_process");
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, import_child_process7.spawn)(spawnCmd, spawnArgs, {
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((resolve2) => {
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
- resolve2();
4922
+ resolve3();
4472
4923
  }, 5e3);
4473
4924
  process2.once("exit", () => {
4474
4925
  clearTimeout(timeout);
4475
- resolve2();
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 import_child_process8 = require("child_process");
5018
+ var import_child_process9 = require("child_process");
4568
5019
  init_port_check();
4569
- var import_core6 = __toESM(require_dist());
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, import_core6.getConfigDir)(), "logs");
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((resolve2) => {
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
- resolve2(true);
5078
+ resolve3(true);
4628
5079
  }
4629
5080
  );
4630
5081
  req.on("error", () => {
4631
- resolve2(false);
5082
+ resolve3(false);
4632
5083
  });
4633
5084
  req.on("timeout", () => {
4634
5085
  req.destroy();
4635
- resolve2(false);
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, import_child_process8.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
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, import_child_process8.execSync)(`kill -15 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
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((resolve2) => setTimeout(resolve2, 1e3));
5107
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
4657
5108
  for (const pid of pids) {
4658
5109
  try {
4659
- (0, import_child_process8.execSync)(`kill -0 ${pid} 2>/dev/null`, { encoding: "utf8" });
4660
- (0, import_child_process8.execSync)(`kill -9 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
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((resolve2) => setTimeout(resolve2, 500));
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((resolve2) => setTimeout(resolve2, checkInterval));
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, import_child_process8.spawn)("npm", ["run", "dev"], {
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((resolve2) => setTimeout(resolve2, delay));
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((resolve2) => setTimeout(resolve2, 2e3));
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((resolve2) => setTimeout(resolve2, 1e3));
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/daemon-process.ts
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 packageJson = require_package();
4964
- async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
4965
- const now = Date.now();
4966
- const expiresAt = config.expires_at || 0;
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 (!config.refresh_token) {
4971
- console.warn("[Daemon] EP904: Token expired but no refresh_token available");
4972
- return config;
5418
+ if (moduleUid.includes("/") || moduleUid.includes("\\") || moduleUid.includes("..")) {
5419
+ return false;
4973
5420
  }
4974
- console.log("[Daemon] EP904: Access token expired or expiring soon, refreshing...");
4975
- try {
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, import_core7.saveConfig)(updatedConfig);
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, import_core7.loadConfig)();
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, import_core7.loadConfig)();
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: os4.hostname(),
5152
- platform: os4.platform(),
5153
- arch: os4.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((resolve2) => setTimeout(resolve2, delay));
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, import_core7.loadConfig)();
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((resolve2) => {
5369
- releaseLock = resolve2;
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((resolve2) => setTimeout(resolve2, 500));
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, import_core7.loadConfig)();
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 import_core7.EpisodaClient();
5421
- const gitExecutor = new import_core7.GitExecutor();
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 result = await gitExecutor.execute(message.command, {
5436
- cwd: projectPath
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, import_core7.loadConfig)();
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 = 2e3;
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((resolve2) => setTimeout(resolve2, RETRY_DELAY_MS));
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, import_core7.loadConfig)();
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] EP843: Starting tunnel for ${moduleUid} (${previousState} \u2192 ${state})`);
6838
+ console.log(`[Daemon] EP956: Starting tunnel for ${moduleUid} (${previousState} \u2192 ${state})`);
5839
6839
  try {
5840
- const port = detectDevPort(projectPath);
5841
- const devServerResult = await ensureDevServer(projectPath, port, moduleUid);
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] EP843: Dev server failed for ${moduleUid}: ${devServerResult.error}`);
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, import_core7.loadConfig)();
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] EP843: Tunnel URL for ${moduleUid}: ${url}`);
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] EP843: Failed to report tunnel URL:`, err instanceof Error ? err.message : err);
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] EP843: Tunnel error for ${moduleUid}: ${error}`);
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] EP843: Tunnel started for ${moduleUid}`);
6880
+ console.log(`[Daemon] EP956: Tunnel started for ${moduleUid}`);
5870
6881
  } else {
5871
- console.error(`[Daemon] EP843: Tunnel failed for ${moduleUid}: ${startResult.error}`);
6882
+ console.error(`[Daemon] EP956: Tunnel failed for ${moduleUid}: ${startResult.error}`);
6883
+ releasePort(moduleUid);
5872
6884
  }
5873
6885
  } catch (error) {
5874
- console.error(`[Daemon] EP843: Error starting tunnel for ${moduleUid}:`, error);
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] EP933: Stopping tunnel for ${moduleUid} (${previousState} \u2192 done)`);
6891
+ console.log(`[Daemon] EP956: Stopping tunnel for ${moduleUid} (${previousState} \u2192 done)`);
5879
6892
  try {
5880
6893
  await tunnelManager.stopTunnel(moduleUid);
5881
- console.log(`[Daemon] EP843: Tunnel stopped for ${moduleUid}`);
5882
- const config2 = await (0, import_core7.loadConfig)();
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] EP843: Failed to clear tunnel URL:`, err instanceof Error ? err.message : err);
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] EP843: Error stopping tunnel for ${moduleUid}:`, error);
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 (fs11.existsSync(pidPath)) {
5916
- const pidStr = fs11.readFileSync(pidPath, "utf-8").trim();
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((resolve2, reject) => {
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
- resolve2();
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: os4.hostname(),
5941
- osPlatform: os4.platform(),
5942
- osArch: os4.arch(),
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 = path12.join(projectPath, ".git", "hooks");
6038
- if (!fs11.existsSync(hooksDir)) {
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 = path12.join(hooksDir, hookName);
6045
- const bundledHookPath = path12.join(__dirname, "..", "hooks", hookName);
6046
- if (!fs11.existsSync(bundledHookPath)) {
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 = fs11.readFileSync(bundledHookPath, "utf-8");
6051
- if (fs11.existsSync(hookPath)) {
6052
- const existingContent = fs11.readFileSync(hookPath, "utf-8");
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
- fs11.writeFileSync(hookPath, hookContent, { mode: 493 });
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, import_core7.loadConfig)();
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, import_core7.saveConfig)(updatedConfig);
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, import_core7.loadConfig)();
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] EP819: Found ${localModulesNeedingTunnel.length} local modules needing tunnels`);
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 port = detectDevPort(projectPath);
6128
- console.log(`[Daemon] EP819: Auto-starting tunnel for ${moduleUid} on port ${port}`);
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] EP819: Tunnel already running for ${moduleUid}, skipping auto-start`);
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 = 2e3;
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] EP819: Ensuring dev server is running for ${moduleUid}...`);
6158
- const devServerResult = await ensureDevServer(projectPath, port, moduleUid);
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] EP819: ${errorMsg2}`);
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] EP819: Dev server ready on port ${port}`);
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((resolve2) => setTimeout(resolve2, RETRY_DELAY_MS));
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] EP819: ${errorMsg}`);
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] EP819: Async tunnel startup error:`, error);
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, import_core7.loadConfig)();
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, import_core7.loadConfig)();
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 execAsync = promisify(exec);
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 execAsync(`pgrep -f "${pattern}"`);
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 execAsync(`kill ${pid}`);
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 execAsync = promisify(exec);
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 execAsync(`lsof -ti :${port}`);
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 execAsync(`kill ${pid}`);
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 execAsync = promisify(exec);
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 execAsync("ps aux | grep cloudflared | grep -v grep");
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 (fs11.existsSync(pidPath)) {
6639
- fs11.unlinkSync(pidPath);
7808
+ if (fs13.existsSync(pidPath)) {
7809
+ fs13.unlinkSync(pidPath);
6640
7810
  console.log("[Daemon] PID file cleaned up");
6641
7811
  }
6642
7812
  } catch (error) {