episoda 0.2.51 → 0.2.53

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.
@@ -1563,15 +1563,15 @@ var require_git_executor = __commonJS({
1563
1563
  try {
1564
1564
  const { stdout: gitDir } = await execAsync2("git rev-parse --git-dir", { cwd, timeout: 5e3 });
1565
1565
  const gitDirPath = gitDir.trim();
1566
- const fs18 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1566
+ const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1567
1567
  const rebaseMergePath = `${gitDirPath}/rebase-merge`;
1568
1568
  const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
1569
1569
  try {
1570
- await fs18.access(rebaseMergePath);
1570
+ await fs17.access(rebaseMergePath);
1571
1571
  inRebase = true;
1572
1572
  } catch {
1573
1573
  try {
1574
- await fs18.access(rebaseApplyPath);
1574
+ await fs17.access(rebaseApplyPath);
1575
1575
  inRebase = true;
1576
1576
  } catch {
1577
1577
  inRebase = false;
@@ -1625,9 +1625,9 @@ var require_git_executor = __commonJS({
1625
1625
  error: validation.error || "UNKNOWN_ERROR"
1626
1626
  };
1627
1627
  }
1628
- const fs18 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1628
+ const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1629
1629
  try {
1630
- await fs18.access(command.path);
1630
+ await fs17.access(command.path);
1631
1631
  return {
1632
1632
  success: false,
1633
1633
  error: "WORKTREE_EXISTS",
@@ -1681,9 +1681,9 @@ var require_git_executor = __commonJS({
1681
1681
  */
1682
1682
  async executeWorktreeRemove(command, cwd, options) {
1683
1683
  try {
1684
- const fs18 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1684
+ const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1685
1685
  try {
1686
- await fs18.access(command.path);
1686
+ await fs17.access(command.path);
1687
1687
  } catch {
1688
1688
  return {
1689
1689
  success: false,
@@ -1718,7 +1718,7 @@ var require_git_executor = __commonJS({
1718
1718
  const result = await this.runGitCommand(args, cwd, options);
1719
1719
  if (result.success) {
1720
1720
  try {
1721
- await fs18.rm(command.path, { recursive: true, force: true });
1721
+ await fs17.rm(command.path, { recursive: true, force: true });
1722
1722
  } catch {
1723
1723
  }
1724
1724
  return {
@@ -1852,10 +1852,10 @@ var require_git_executor = __commonJS({
1852
1852
  */
1853
1853
  async executeCloneBare(command, options) {
1854
1854
  try {
1855
- const fs18 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1856
- const path19 = await Promise.resolve().then(() => __importStar(require("path")));
1855
+ const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1856
+ const path18 = await Promise.resolve().then(() => __importStar(require("path")));
1857
1857
  try {
1858
- await fs18.access(command.path);
1858
+ await fs17.access(command.path);
1859
1859
  return {
1860
1860
  success: false,
1861
1861
  error: "BRANCH_ALREADY_EXISTS",
@@ -1864,9 +1864,9 @@ var require_git_executor = __commonJS({
1864
1864
  };
1865
1865
  } catch {
1866
1866
  }
1867
- const parentDir = path19.dirname(command.path);
1867
+ const parentDir = path18.dirname(command.path);
1868
1868
  try {
1869
- await fs18.mkdir(parentDir, { recursive: true });
1869
+ await fs17.mkdir(parentDir, { recursive: true });
1870
1870
  } catch {
1871
1871
  }
1872
1872
  const { stdout, stderr } = await execAsync2(
@@ -1914,22 +1914,22 @@ var require_git_executor = __commonJS({
1914
1914
  */
1915
1915
  async executeProjectInfo(cwd, options) {
1916
1916
  try {
1917
- const fs18 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1918
- const path19 = await Promise.resolve().then(() => __importStar(require("path")));
1917
+ const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1918
+ const path18 = await Promise.resolve().then(() => __importStar(require("path")));
1919
1919
  let currentPath = cwd;
1920
1920
  let projectPath = cwd;
1921
1921
  let bareRepoPath;
1922
1922
  for (let i = 0; i < 10; i++) {
1923
- const bareDir = path19.join(currentPath, ".bare");
1924
- const episodaDir = path19.join(currentPath, ".episoda");
1923
+ const bareDir = path18.join(currentPath, ".bare");
1924
+ const episodaDir = path18.join(currentPath, ".episoda");
1925
1925
  try {
1926
- await fs18.access(bareDir);
1927
- await fs18.access(episodaDir);
1926
+ await fs17.access(bareDir);
1927
+ await fs17.access(episodaDir);
1928
1928
  projectPath = currentPath;
1929
1929
  bareRepoPath = bareDir;
1930
1930
  break;
1931
1931
  } catch {
1932
- const parentPath = path19.dirname(currentPath);
1932
+ const parentPath = path18.dirname(currentPath);
1933
1933
  if (parentPath === currentPath) {
1934
1934
  break;
1935
1935
  }
@@ -2538,31 +2538,31 @@ var require_auth = __commonJS({
2538
2538
  exports2.loadConfig = loadConfig7;
2539
2539
  exports2.saveConfig = saveConfig2;
2540
2540
  exports2.validateToken = validateToken;
2541
- var fs18 = __importStar(require("fs"));
2542
- var path19 = __importStar(require("path"));
2541
+ var fs17 = __importStar(require("fs"));
2542
+ var path18 = __importStar(require("path"));
2543
2543
  var os7 = __importStar(require("os"));
2544
2544
  var child_process_1 = require("child_process");
2545
2545
  var DEFAULT_CONFIG_FILE = "config.json";
2546
2546
  function getConfigDir8() {
2547
- return process.env.EPISODA_CONFIG_DIR || path19.join(os7.homedir(), ".episoda");
2547
+ return process.env.EPISODA_CONFIG_DIR || path18.join(os7.homedir(), ".episoda");
2548
2548
  }
2549
2549
  function getConfigPath(configPath) {
2550
2550
  if (configPath) {
2551
2551
  return configPath;
2552
2552
  }
2553
- return path19.join(getConfigDir8(), DEFAULT_CONFIG_FILE);
2553
+ return path18.join(getConfigDir8(), DEFAULT_CONFIG_FILE);
2554
2554
  }
2555
2555
  function ensureConfigDir(configPath) {
2556
- const dir = path19.dirname(configPath);
2557
- const isNew = !fs18.existsSync(dir);
2556
+ const dir = path18.dirname(configPath);
2557
+ const isNew = !fs17.existsSync(dir);
2558
2558
  if (isNew) {
2559
- fs18.mkdirSync(dir, { recursive: true, mode: 448 });
2559
+ fs17.mkdirSync(dir, { recursive: true, mode: 448 });
2560
2560
  }
2561
2561
  if (process.platform === "darwin") {
2562
- const nosyncPath = path19.join(dir, ".nosync");
2563
- if (isNew || !fs18.existsSync(nosyncPath)) {
2562
+ const nosyncPath = path18.join(dir, ".nosync");
2563
+ if (isNew || !fs17.existsSync(nosyncPath)) {
2564
2564
  try {
2565
- fs18.writeFileSync(nosyncPath, "", { mode: 384 });
2565
+ fs17.writeFileSync(nosyncPath, "", { mode: 384 });
2566
2566
  (0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
2567
2567
  stdio: "ignore",
2568
2568
  timeout: 5e3
@@ -2574,9 +2574,9 @@ var require_auth = __commonJS({
2574
2574
  }
2575
2575
  async function loadConfig7(configPath) {
2576
2576
  const fullPath = getConfigPath(configPath);
2577
- if (fs18.existsSync(fullPath)) {
2577
+ if (fs17.existsSync(fullPath)) {
2578
2578
  try {
2579
- const content = fs18.readFileSync(fullPath, "utf8");
2579
+ const content = fs17.readFileSync(fullPath, "utf8");
2580
2580
  const config = JSON.parse(content);
2581
2581
  return config;
2582
2582
  } catch (error) {
@@ -2601,7 +2601,7 @@ var require_auth = __commonJS({
2601
2601
  ensureConfigDir(fullPath);
2602
2602
  try {
2603
2603
  const content = JSON.stringify(config, null, 2);
2604
- fs18.writeFileSync(fullPath, content, { mode: 384 });
2604
+ fs17.writeFileSync(fullPath, content, { mode: 384 });
2605
2605
  } catch (error) {
2606
2606
  throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
2607
2607
  }
@@ -2718,7 +2718,7 @@ var require_package = __commonJS({
2718
2718
  "package.json"(exports2, module2) {
2719
2719
  module2.exports = {
2720
2720
  name: "episoda",
2721
- version: "0.2.51",
2721
+ version: "0.2.53",
2722
2722
  description: "CLI tool for Episoda local development workflow orchestration",
2723
2723
  main: "dist/index.js",
2724
2724
  types: "dist/index.d.ts",
@@ -4020,12 +4020,14 @@ var os2 = __toESM(require("os"));
4020
4020
  // src/tunnel/tunnel-api.ts
4021
4021
  var import_core7 = __toESM(require_dist());
4022
4022
  async function provisionNamedTunnel(moduleId, port = 3e3) {
4023
+ console.log(`[TunnelAPI] EP1038: provisionNamedTunnel called for moduleId ${moduleId} with port ${port}`);
4023
4024
  const config = await (0, import_core7.loadConfig)();
4024
4025
  if (!config?.access_token) {
4025
4026
  return { success: false, error: "Not authenticated" };
4026
4027
  }
4027
4028
  try {
4028
4029
  const apiUrl = config.api_url || "https://episoda.dev";
4030
+ console.log(`[TunnelAPI] EP1038: POSTing to ${apiUrl}/api/tunnels with port=${port}`);
4029
4031
  const response = await fetch(`${apiUrl}/api/tunnels`, {
4030
4032
  method: "POST",
4031
4033
  headers: {
@@ -4055,6 +4057,7 @@ async function provisionNamedTunnel(moduleId, port = 3e3) {
4055
4057
  }
4056
4058
  }
4057
4059
  async function provisionNamedTunnelByUid(moduleUid, port = 3e3) {
4060
+ console.log(`[TunnelAPI] EP1038: provisionNamedTunnelByUid called for ${moduleUid} with port ${port}`);
4058
4061
  const config = await (0, import_core7.loadConfig)();
4059
4062
  if (!config?.access_token) {
4060
4063
  return { success: false, error: "Not authenticated" };
@@ -5857,6 +5860,48 @@ function getDevServerRunner() {
5857
5860
  return instance2;
5858
5861
  }
5859
5862
 
5863
+ // src/utils/port-allocator.ts
5864
+ var PORT_RANGE_START = 3100;
5865
+ var PORT_RANGE_END = 3199;
5866
+ var PORT_WARNING_THRESHOLD = 80;
5867
+ var portAssignments = /* @__PURE__ */ new Map();
5868
+ function allocatePort(moduleUid) {
5869
+ const existing = portAssignments.get(moduleUid);
5870
+ if (existing) {
5871
+ return existing;
5872
+ }
5873
+ const usedPorts = new Set(portAssignments.values());
5874
+ if (usedPorts.size >= PORT_WARNING_THRESHOLD) {
5875
+ console.warn(
5876
+ `[PortAllocator] Warning: ${usedPorts.size}/${PORT_RANGE_END - PORT_RANGE_START + 1} ports allocated`
5877
+ );
5878
+ }
5879
+ for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
5880
+ if (!usedPorts.has(port)) {
5881
+ portAssignments.set(moduleUid, port);
5882
+ console.log(`[PortAllocator] Assigned port ${port} to ${moduleUid}`);
5883
+ return port;
5884
+ }
5885
+ }
5886
+ throw new Error(
5887
+ `No available ports in range ${PORT_RANGE_START}-${PORT_RANGE_END}. ${portAssignments.size} modules are using all available ports.`
5888
+ );
5889
+ }
5890
+ function releasePort(moduleUid) {
5891
+ const port = portAssignments.get(moduleUid);
5892
+ if (port) {
5893
+ portAssignments.delete(moduleUid);
5894
+ console.log(`[PortAllocator] Released port ${port} from ${moduleUid}`);
5895
+ }
5896
+ }
5897
+ function clearAllPorts() {
5898
+ const count = portAssignments.size;
5899
+ portAssignments.clear();
5900
+ if (count > 0) {
5901
+ console.log(`[PortAllocator] Cleared ${count} port assignments`);
5902
+ }
5903
+ }
5904
+
5860
5905
  // src/preview/preview-manager.ts
5861
5906
  var DEFAULT_PORT = 3e3;
5862
5907
  var PreviewManager = class extends import_events3.EventEmitter {
@@ -6069,6 +6114,7 @@ var PreviewManager = class extends import_events3.EventEmitter {
6069
6114
  } catch (error) {
6070
6115
  console.warn(`[PreviewManager] Error clearing tunnel URL for ${moduleUid}:`, error);
6071
6116
  }
6117
+ releasePort(moduleUid);
6072
6118
  if (state) {
6073
6119
  state.state = "stopped";
6074
6120
  state.tunnelUrl = void 0;
@@ -6574,80 +6620,9 @@ function getDevServerStatus() {
6574
6620
  }));
6575
6621
  }
6576
6622
 
6577
- // src/utils/port-detect.ts
6623
+ // src/daemon/worktree-manager.ts
6578
6624
  var fs13 = __toESM(require("fs"));
6579
6625
  var path14 = __toESM(require("path"));
6580
- var DEFAULT_PORT2 = 3e3;
6581
- function detectDevPort(projectPath) {
6582
- const envPort = getPortFromEnv(projectPath);
6583
- if (envPort) {
6584
- console.log(`[PortDetect] Found PORT=${envPort} in .env`);
6585
- return envPort;
6586
- }
6587
- const scriptPort = getPortFromPackageJson(projectPath);
6588
- if (scriptPort) {
6589
- console.log(`[PortDetect] Found port ${scriptPort} in package.json dev script`);
6590
- return scriptPort;
6591
- }
6592
- console.log(`[PortDetect] Using default port ${DEFAULT_PORT2}`);
6593
- return DEFAULT_PORT2;
6594
- }
6595
- function getPortFromEnv(projectPath) {
6596
- const envPaths = [
6597
- path14.join(projectPath, ".env"),
6598
- path14.join(projectPath, ".env.local"),
6599
- path14.join(projectPath, ".env.development"),
6600
- path14.join(projectPath, ".env.development.local")
6601
- ];
6602
- for (const envPath of envPaths) {
6603
- try {
6604
- if (!fs13.existsSync(envPath)) continue;
6605
- const content = fs13.readFileSync(envPath, "utf-8");
6606
- const lines = content.split("\n");
6607
- for (const line of lines) {
6608
- const match = line.match(/^\s*PORT\s*=\s*["']?(\d+)["']?\s*(?:#.*)?$/);
6609
- if (match) {
6610
- const port = parseInt(match[1], 10);
6611
- if (port > 0 && port < 65536) {
6612
- return port;
6613
- }
6614
- }
6615
- }
6616
- } catch {
6617
- }
6618
- }
6619
- return null;
6620
- }
6621
- function getPortFromPackageJson(projectPath) {
6622
- const packageJsonPath = path14.join(projectPath, "package.json");
6623
- try {
6624
- if (!fs13.existsSync(packageJsonPath)) return null;
6625
- const content = fs13.readFileSync(packageJsonPath, "utf-8");
6626
- const pkg = JSON.parse(content);
6627
- const devScript = pkg.scripts?.dev;
6628
- if (!devScript) return null;
6629
- const portMatch = devScript.match(/(?:--port[=\s]|-p[=\s])(\d+)/);
6630
- if (portMatch) {
6631
- const port = parseInt(portMatch[1], 10);
6632
- if (port > 0 && port < 65536) {
6633
- return port;
6634
- }
6635
- }
6636
- const envMatch = devScript.match(/PORT[=\s](\d+)/);
6637
- if (envMatch) {
6638
- const port = parseInt(envMatch[1], 10);
6639
- if (port > 0 && port < 65536) {
6640
- return port;
6641
- }
6642
- }
6643
- } catch {
6644
- }
6645
- return null;
6646
- }
6647
-
6648
- // src/daemon/worktree-manager.ts
6649
- var fs14 = __toESM(require("fs"));
6650
- var path15 = __toESM(require("path"));
6651
6626
  var import_core10 = __toESM(require_dist());
6652
6627
  function validateModuleUid(moduleUid) {
6653
6628
  if (!moduleUid || typeof moduleUid !== "string" || !moduleUid.trim()) {
@@ -6671,8 +6646,8 @@ var WorktreeManager = class _WorktreeManager {
6671
6646
  // ============================================================
6672
6647
  this.lockPath = "";
6673
6648
  this.projectRoot = projectRoot;
6674
- this.bareRepoPath = path15.join(projectRoot, ".bare");
6675
- this.configPath = path15.join(projectRoot, ".episoda", "config.json");
6649
+ this.bareRepoPath = path14.join(projectRoot, ".bare");
6650
+ this.configPath = path14.join(projectRoot, ".episoda", "config.json");
6676
6651
  this.gitExecutor = new import_core10.GitExecutor();
6677
6652
  }
6678
6653
  /**
@@ -6681,10 +6656,10 @@ var WorktreeManager = class _WorktreeManager {
6681
6656
  * @returns true if valid project, false otherwise
6682
6657
  */
6683
6658
  async initialize() {
6684
- if (!fs14.existsSync(this.bareRepoPath)) {
6659
+ if (!fs13.existsSync(this.bareRepoPath)) {
6685
6660
  return false;
6686
6661
  }
6687
- if (!fs14.existsSync(this.configPath)) {
6662
+ if (!fs13.existsSync(this.configPath)) {
6688
6663
  return false;
6689
6664
  }
6690
6665
  try {
@@ -6731,8 +6706,8 @@ var WorktreeManager = class _WorktreeManager {
6731
6706
  */
6732
6707
  static async createProject(projectRoot, repoUrl, projectId, workspaceSlug, projectSlug) {
6733
6708
  const manager = new _WorktreeManager(projectRoot);
6734
- const episodaDir = path15.join(projectRoot, ".episoda");
6735
- fs14.mkdirSync(episodaDir, { recursive: true });
6709
+ const episodaDir = path14.join(projectRoot, ".episoda");
6710
+ fs13.mkdirSync(episodaDir, { recursive: true });
6736
6711
  const cloneResult = await manager.gitExecutor.execute({
6737
6712
  action: "clone_bare",
6738
6713
  url: repoUrl,
@@ -6763,7 +6738,7 @@ var WorktreeManager = class _WorktreeManager {
6763
6738
  error: `Invalid module UID: "${moduleUid}" - contains disallowed characters`
6764
6739
  };
6765
6740
  }
6766
- const worktreePath = path15.join(this.projectRoot, moduleUid);
6741
+ const worktreePath = path14.join(this.projectRoot, moduleUid);
6767
6742
  const lockAcquired = await this.acquireLock();
6768
6743
  if (!lockAcquired) {
6769
6744
  return {
@@ -6863,14 +6838,21 @@ var WorktreeManager = class _WorktreeManager {
6863
6838
  error: result.output || "Failed to remove worktree"
6864
6839
  };
6865
6840
  }
6866
- const config = this.readConfig();
6867
- if (config) {
6868
- config.worktrees = config.worktrees.filter((w) => w.moduleUid !== moduleUid);
6869
- this.writeConfig(config);
6841
+ let configUpdated = false;
6842
+ try {
6843
+ const config = this.readConfig();
6844
+ if (config) {
6845
+ config.worktrees = config.worktrees.filter((w) => w.moduleUid !== moduleUid);
6846
+ this.writeConfig(config);
6847
+ configUpdated = true;
6848
+ }
6849
+ } catch (configError) {
6850
+ console.warn(`[WorktreeManager] EP1035: Config update failed after removing ${moduleUid} (non-blocking):`, configError.message);
6870
6851
  }
6871
6852
  return {
6872
6853
  success: true,
6873
- worktreePath: existing.worktreePath
6854
+ worktreePath: existing.worktreePath,
6855
+ configUpdated
6874
6856
  };
6875
6857
  } finally {
6876
6858
  this.releaseLock();
@@ -6945,7 +6927,7 @@ var WorktreeManager = class _WorktreeManager {
6945
6927
  let prunedCount = 0;
6946
6928
  await this.updateConfigSafe((config) => {
6947
6929
  const initialCount = config.worktrees.length;
6948
- config.worktrees = config.worktrees.filter((w) => fs14.existsSync(w.worktreePath));
6930
+ config.worktrees = config.worktrees.filter((w) => fs13.existsSync(w.worktreePath));
6949
6931
  prunedCount = initialCount - config.worktrees.length;
6950
6932
  return config;
6951
6933
  });
@@ -7026,16 +7008,16 @@ var WorktreeManager = class _WorktreeManager {
7026
7008
  const retryInterval = 50;
7027
7009
  while (Date.now() - startTime < timeoutMs) {
7028
7010
  try {
7029
- fs14.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
7011
+ fs13.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
7030
7012
  return true;
7031
7013
  } catch (err) {
7032
7014
  if (err.code === "EEXIST") {
7033
7015
  try {
7034
- const stats = fs14.statSync(lockPath);
7016
+ const stats = fs13.statSync(lockPath);
7035
7017
  const lockAge = Date.now() - stats.mtimeMs;
7036
7018
  if (lockAge > 3e4) {
7037
7019
  try {
7038
- const lockContent = fs14.readFileSync(lockPath, "utf-8").trim();
7020
+ const lockContent = fs13.readFileSync(lockPath, "utf-8").trim();
7039
7021
  const lockPid = parseInt(lockContent, 10);
7040
7022
  if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
7041
7023
  await new Promise((resolve3) => setTimeout(resolve3, retryInterval));
@@ -7044,7 +7026,7 @@ var WorktreeManager = class _WorktreeManager {
7044
7026
  } catch {
7045
7027
  }
7046
7028
  try {
7047
- fs14.unlinkSync(lockPath);
7029
+ fs13.unlinkSync(lockPath);
7048
7030
  } catch {
7049
7031
  }
7050
7032
  continue;
@@ -7065,29 +7047,34 @@ var WorktreeManager = class _WorktreeManager {
7065
7047
  */
7066
7048
  releaseLock() {
7067
7049
  try {
7068
- fs14.unlinkSync(this.getLockPath());
7050
+ fs13.unlinkSync(this.getLockPath());
7069
7051
  } catch {
7070
7052
  }
7071
7053
  }
7072
7054
  readConfig() {
7073
7055
  try {
7074
- if (!fs14.existsSync(this.configPath)) {
7056
+ if (!fs13.existsSync(this.configPath)) {
7075
7057
  return null;
7076
7058
  }
7077
- const content = fs14.readFileSync(this.configPath, "utf-8");
7059
+ const content = fs13.readFileSync(this.configPath, "utf-8");
7078
7060
  return JSON.parse(content);
7079
7061
  } catch (error) {
7080
- console.error("[WorktreeManager] Failed to read config:", error);
7062
+ if (error instanceof SyntaxError) {
7063
+ console.warn(`[WorktreeManager] EP1035: Config file corrupted at ${this.configPath} - JSON parse error:`, error.message);
7064
+ console.warn("[WorktreeManager] EP1035: Config will be recovered on next pruneStaleWorktrees call");
7065
+ } else {
7066
+ console.error("[WorktreeManager] Failed to read config:", error.message);
7067
+ }
7081
7068
  return null;
7082
7069
  }
7083
7070
  }
7084
7071
  writeConfig(config) {
7085
7072
  try {
7086
- const dir = path15.dirname(this.configPath);
7087
- if (!fs14.existsSync(dir)) {
7088
- fs14.mkdirSync(dir, { recursive: true });
7073
+ const dir = path14.dirname(this.configPath);
7074
+ if (!fs13.existsSync(dir)) {
7075
+ fs13.mkdirSync(dir, { recursive: true });
7089
7076
  }
7090
- fs14.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
7077
+ fs13.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
7091
7078
  } catch (error) {
7092
7079
  console.error("[WorktreeManager] Failed to write config:", error);
7093
7080
  throw error;
@@ -7168,14 +7155,14 @@ var WorktreeManager = class _WorktreeManager {
7168
7155
  }
7169
7156
  try {
7170
7157
  for (const file of files) {
7171
- const srcPath = path15.join(mainWorktree.worktreePath, file);
7172
- const destPath = path15.join(worktree.worktreePath, file);
7173
- if (fs14.existsSync(srcPath)) {
7174
- const destDir = path15.dirname(destPath);
7175
- if (!fs14.existsSync(destDir)) {
7176
- fs14.mkdirSync(destDir, { recursive: true });
7177
- }
7178
- fs14.copyFileSync(srcPath, destPath);
7158
+ const srcPath = path14.join(mainWorktree.worktreePath, file);
7159
+ const destPath = path14.join(worktree.worktreePath, file);
7160
+ if (fs13.existsSync(srcPath)) {
7161
+ const destDir = path14.dirname(destPath);
7162
+ if (!fs13.existsSync(destDir)) {
7163
+ fs13.mkdirSync(destDir, { recursive: true });
7164
+ }
7165
+ fs13.copyFileSync(srcPath, destPath);
7179
7166
  console.log(`[WorktreeManager] EP964: Copied ${file} to ${moduleUid} (deprecated)`);
7180
7167
  } else {
7181
7168
  console.log(`[WorktreeManager] EP964: Skipped ${file} (not found in main)`);
@@ -7258,27 +7245,27 @@ var WorktreeManager = class _WorktreeManager {
7258
7245
  }
7259
7246
  };
7260
7247
  function getEpisodaRoot() {
7261
- return process.env.EPISODA_ROOT || path15.join(require("os").homedir(), "episoda");
7248
+ return process.env.EPISODA_ROOT || path14.join(require("os").homedir(), "episoda");
7262
7249
  }
7263
7250
  async function isWorktreeProject(projectRoot) {
7264
7251
  const manager = new WorktreeManager(projectRoot);
7265
7252
  return manager.initialize();
7266
7253
  }
7267
7254
  async function findProjectRoot(startPath) {
7268
- let current = path15.resolve(startPath);
7255
+ let current = path14.resolve(startPath);
7269
7256
  const episodaRoot = getEpisodaRoot();
7270
7257
  if (!current.startsWith(episodaRoot)) {
7271
7258
  return null;
7272
7259
  }
7273
7260
  for (let i = 0; i < 10; i++) {
7274
- const bareDir = path15.join(current, ".bare");
7275
- const episodaDir = path15.join(current, ".episoda");
7276
- if (fs14.existsSync(bareDir) && fs14.existsSync(episodaDir)) {
7261
+ const bareDir = path14.join(current, ".bare");
7262
+ const episodaDir = path14.join(current, ".episoda");
7263
+ if (fs13.existsSync(bareDir) && fs13.existsSync(episodaDir)) {
7277
7264
  if (await isWorktreeProject(current)) {
7278
7265
  return current;
7279
7266
  }
7280
7267
  }
7281
- const parent = path15.dirname(current);
7268
+ const parent = path14.dirname(current);
7282
7269
  if (parent === current) {
7283
7270
  break;
7284
7271
  }
@@ -7288,19 +7275,19 @@ async function findProjectRoot(startPath) {
7288
7275
  }
7289
7276
 
7290
7277
  // src/utils/worktree.ts
7291
- var path16 = __toESM(require("path"));
7292
- var fs15 = __toESM(require("fs"));
7278
+ var path15 = __toESM(require("path"));
7279
+ var fs14 = __toESM(require("fs"));
7293
7280
  var os5 = __toESM(require("os"));
7294
7281
  var import_core11 = __toESM(require_dist());
7295
7282
  function getEpisodaRoot2() {
7296
- return process.env.EPISODA_ROOT || path16.join(os5.homedir(), "episoda");
7283
+ return process.env.EPISODA_ROOT || path15.join(os5.homedir(), "episoda");
7297
7284
  }
7298
7285
  function getWorktreeInfo(moduleUid, workspaceSlug, projectSlug) {
7299
7286
  const root = getEpisodaRoot2();
7300
- const worktreePath = path16.join(root, workspaceSlug, projectSlug, moduleUid);
7287
+ const worktreePath = path15.join(root, workspaceSlug, projectSlug, moduleUid);
7301
7288
  return {
7302
7289
  path: worktreePath,
7303
- exists: fs15.existsSync(worktreePath),
7290
+ exists: fs14.existsSync(worktreePath),
7304
7291
  moduleUid
7305
7292
  };
7306
7293
  }
@@ -7313,72 +7300,62 @@ async function getWorktreeInfoForModule(moduleUid) {
7313
7300
  return getWorktreeInfo(moduleUid, config.workspace_slug, config.project_slug);
7314
7301
  }
7315
7302
 
7316
- // src/utils/port-allocator.ts
7317
- var portAssignments = /* @__PURE__ */ new Map();
7318
- function clearAllPorts() {
7319
- const count = portAssignments.size;
7320
- portAssignments.clear();
7321
- if (count > 0) {
7322
- console.log(`[PortAllocator] Cleared ${count} port assignments`);
7323
- }
7324
- }
7325
-
7326
7303
  // src/framework-detector.ts
7327
- var fs16 = __toESM(require("fs"));
7328
- var path17 = __toESM(require("path"));
7304
+ var fs15 = __toESM(require("fs"));
7305
+ var path16 = __toESM(require("path"));
7329
7306
  function getInstallCommand(cwd) {
7330
- if (fs16.existsSync(path17.join(cwd, "bun.lockb"))) {
7307
+ if (fs15.existsSync(path16.join(cwd, "bun.lockb"))) {
7331
7308
  return {
7332
7309
  command: ["bun", "install"],
7333
7310
  description: "Installing dependencies with bun",
7334
7311
  detectedFrom: "bun.lockb"
7335
7312
  };
7336
7313
  }
7337
- if (fs16.existsSync(path17.join(cwd, "pnpm-lock.yaml"))) {
7314
+ if (fs15.existsSync(path16.join(cwd, "pnpm-lock.yaml"))) {
7338
7315
  return {
7339
7316
  command: ["pnpm", "install"],
7340
7317
  description: "Installing dependencies with pnpm",
7341
7318
  detectedFrom: "pnpm-lock.yaml"
7342
7319
  };
7343
7320
  }
7344
- if (fs16.existsSync(path17.join(cwd, "yarn.lock"))) {
7321
+ if (fs15.existsSync(path16.join(cwd, "yarn.lock"))) {
7345
7322
  return {
7346
7323
  command: ["yarn", "install"],
7347
7324
  description: "Installing dependencies with yarn",
7348
7325
  detectedFrom: "yarn.lock"
7349
7326
  };
7350
7327
  }
7351
- if (fs16.existsSync(path17.join(cwd, "package-lock.json"))) {
7328
+ if (fs15.existsSync(path16.join(cwd, "package-lock.json"))) {
7352
7329
  return {
7353
7330
  command: ["npm", "ci"],
7354
7331
  description: "Installing dependencies with npm ci",
7355
7332
  detectedFrom: "package-lock.json"
7356
7333
  };
7357
7334
  }
7358
- if (fs16.existsSync(path17.join(cwd, "package.json"))) {
7335
+ if (fs15.existsSync(path16.join(cwd, "package.json"))) {
7359
7336
  return {
7360
7337
  command: ["npm", "install"],
7361
7338
  description: "Installing dependencies with npm",
7362
7339
  detectedFrom: "package.json"
7363
7340
  };
7364
7341
  }
7365
- if (fs16.existsSync(path17.join(cwd, "Pipfile.lock")) || fs16.existsSync(path17.join(cwd, "Pipfile"))) {
7342
+ if (fs15.existsSync(path16.join(cwd, "Pipfile.lock")) || fs15.existsSync(path16.join(cwd, "Pipfile"))) {
7366
7343
  return {
7367
7344
  command: ["pipenv", "install"],
7368
7345
  description: "Installing dependencies with pipenv",
7369
- detectedFrom: fs16.existsSync(path17.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
7346
+ detectedFrom: fs15.existsSync(path16.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
7370
7347
  };
7371
7348
  }
7372
- if (fs16.existsSync(path17.join(cwd, "poetry.lock"))) {
7349
+ if (fs15.existsSync(path16.join(cwd, "poetry.lock"))) {
7373
7350
  return {
7374
7351
  command: ["poetry", "install"],
7375
7352
  description: "Installing dependencies with poetry",
7376
7353
  detectedFrom: "poetry.lock"
7377
7354
  };
7378
7355
  }
7379
- if (fs16.existsSync(path17.join(cwd, "pyproject.toml"))) {
7380
- const pyprojectPath = path17.join(cwd, "pyproject.toml");
7381
- const content = fs16.readFileSync(pyprojectPath, "utf-8");
7356
+ if (fs15.existsSync(path16.join(cwd, "pyproject.toml"))) {
7357
+ const pyprojectPath = path16.join(cwd, "pyproject.toml");
7358
+ const content = fs15.readFileSync(pyprojectPath, "utf-8");
7382
7359
  if (content.includes("[tool.poetry]")) {
7383
7360
  return {
7384
7361
  command: ["poetry", "install"],
@@ -7387,41 +7364,41 @@ function getInstallCommand(cwd) {
7387
7364
  };
7388
7365
  }
7389
7366
  }
7390
- if (fs16.existsSync(path17.join(cwd, "requirements.txt"))) {
7367
+ if (fs15.existsSync(path16.join(cwd, "requirements.txt"))) {
7391
7368
  return {
7392
7369
  command: ["pip", "install", "-r", "requirements.txt"],
7393
7370
  description: "Installing dependencies with pip",
7394
7371
  detectedFrom: "requirements.txt"
7395
7372
  };
7396
7373
  }
7397
- if (fs16.existsSync(path17.join(cwd, "Gemfile.lock")) || fs16.existsSync(path17.join(cwd, "Gemfile"))) {
7374
+ if (fs15.existsSync(path16.join(cwd, "Gemfile.lock")) || fs15.existsSync(path16.join(cwd, "Gemfile"))) {
7398
7375
  return {
7399
7376
  command: ["bundle", "install"],
7400
7377
  description: "Installing dependencies with bundler",
7401
- detectedFrom: fs16.existsSync(path17.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
7378
+ detectedFrom: fs15.existsSync(path16.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
7402
7379
  };
7403
7380
  }
7404
- if (fs16.existsSync(path17.join(cwd, "go.sum")) || fs16.existsSync(path17.join(cwd, "go.mod"))) {
7381
+ if (fs15.existsSync(path16.join(cwd, "go.sum")) || fs15.existsSync(path16.join(cwd, "go.mod"))) {
7405
7382
  return {
7406
7383
  command: ["go", "mod", "download"],
7407
7384
  description: "Downloading Go modules",
7408
- detectedFrom: fs16.existsSync(path17.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
7385
+ detectedFrom: fs15.existsSync(path16.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
7409
7386
  };
7410
7387
  }
7411
- if (fs16.existsSync(path17.join(cwd, "Cargo.lock")) || fs16.existsSync(path17.join(cwd, "Cargo.toml"))) {
7388
+ if (fs15.existsSync(path16.join(cwd, "Cargo.lock")) || fs15.existsSync(path16.join(cwd, "Cargo.toml"))) {
7412
7389
  return {
7413
7390
  command: ["cargo", "build"],
7414
7391
  description: "Building Rust project (downloads dependencies)",
7415
- detectedFrom: fs16.existsSync(path17.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
7392
+ detectedFrom: fs15.existsSync(path16.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
7416
7393
  };
7417
7394
  }
7418
7395
  return null;
7419
7396
  }
7420
7397
 
7421
7398
  // src/daemon/daemon-process.ts
7422
- var fs17 = __toESM(require("fs"));
7399
+ var fs16 = __toESM(require("fs"));
7423
7400
  var os6 = __toESM(require("os"));
7424
- var path18 = __toESM(require("path"));
7401
+ var path17 = __toESM(require("path"));
7425
7402
  var packageJson = require_package();
7426
7403
  async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
7427
7404
  const now = Date.now();
@@ -7772,6 +7749,7 @@ var Daemon = class _Daemon {
7772
7749
  }
7773
7750
  await tunnelManager.stopTunnel(moduleUid);
7774
7751
  await stopDevServer(moduleUid);
7752
+ releasePort(moduleUid);
7775
7753
  await clearTunnelUrl(moduleUid);
7776
7754
  this.tunnelHealthFailures.delete(moduleUid);
7777
7755
  console.log(`[Daemon] EP823: Tunnel stopped for ${moduleUid}`);
@@ -7922,7 +7900,7 @@ var Daemon = class _Daemon {
7922
7900
  client.updateActivity();
7923
7901
  try {
7924
7902
  const gitCmd = message.command;
7925
- const bareRepoPath = path18.join(projectPath, ".bare");
7903
+ const bareRepoPath = path17.join(projectPath, ".bare");
7926
7904
  const cwd = gitCmd.worktreePath || bareRepoPath;
7927
7905
  if (gitCmd.worktreePath) {
7928
7906
  console.log(`[Daemon] Routing command to worktree: ${gitCmd.worktreePath}`);
@@ -8076,7 +8054,8 @@ var Daemon = class _Daemon {
8076
8054
  return;
8077
8055
  }
8078
8056
  console.log(`[Daemon] EP1024: Using worktree path ${worktree.path} for ${cmd.moduleUid}`);
8079
- const port = cmd.port || detectDevPort(worktree.path);
8057
+ const port = cmd.port || allocatePort(cmd.moduleUid);
8058
+ console.log(`[Daemon] EP1038: Allocated port ${port} for ${cmd.moduleUid}`);
8080
8059
  const devConfig = await (0, import_core12.loadConfig)();
8081
8060
  const customCommand = devConfig?.project_settings?.worktree_dev_server_script;
8082
8061
  const startResult = await previewManager.startPreview({
@@ -8250,6 +8229,78 @@ var Daemon = class _Daemon {
8250
8229
  }
8251
8230
  }
8252
8231
  });
8232
+ client.on("worktree_cleanup_command", async (message) => {
8233
+ if (message.type === "worktree_cleanup_command" && message.command) {
8234
+ const cmd = message.command;
8235
+ console.log(`[Daemon] EP1035: Received worktree cleanup command for ${cmd.moduleUid}`);
8236
+ client.updateActivity();
8237
+ let result;
8238
+ try {
8239
+ const projectRootPath = await findProjectRoot(cmd.worktreePath);
8240
+ if (!projectRootPath) {
8241
+ console.warn(`[Daemon] EP1035: Cannot find project root for ${cmd.worktreePath}`);
8242
+ result = {
8243
+ success: false,
8244
+ error: `Cannot find project root for worktree path: ${cmd.worktreePath}`
8245
+ };
8246
+ } else {
8247
+ const manager = new WorktreeManager(projectRootPath);
8248
+ const initialized = await manager.initialize();
8249
+ if (!initialized) {
8250
+ console.warn(`[Daemon] EP1035: Failed to initialize WorktreeManager for ${projectRootPath}`);
8251
+ result = {
8252
+ success: false,
8253
+ error: `Failed to initialize WorktreeManager for project: ${projectRootPath}`
8254
+ };
8255
+ } else {
8256
+ const removeResult = await manager.removeWorktree(cmd.moduleUid, cmd.force || false);
8257
+ if (removeResult.success) {
8258
+ if (removeResult.configUpdated === false) {
8259
+ console.warn(`[Daemon] EP1035: Worktree ${cmd.moduleUid} removed but config update failed (will sync on next prune)`);
8260
+ } else {
8261
+ console.log(`[Daemon] EP1035: Successfully cleaned up worktree for ${cmd.moduleUid}`);
8262
+ }
8263
+ result = {
8264
+ success: true,
8265
+ worktreePath: removeResult.worktreePath
8266
+ };
8267
+ } else {
8268
+ if (removeResult.error?.includes("not found") || removeResult.error?.includes("No worktree found")) {
8269
+ console.log(`[Daemon] EP1035: Worktree for ${cmd.moduleUid} already removed, syncing config`);
8270
+ await manager.pruneStaleWorktrees();
8271
+ result = {
8272
+ success: true,
8273
+ worktreePath: cmd.worktreePath
8274
+ };
8275
+ } else {
8276
+ console.error(`[Daemon] EP1035: Failed to remove worktree for ${cmd.moduleUid}: ${removeResult.error}`);
8277
+ result = {
8278
+ success: false,
8279
+ error: removeResult.error
8280
+ };
8281
+ }
8282
+ }
8283
+ }
8284
+ }
8285
+ } catch (error) {
8286
+ console.error(`[Daemon] EP1035: Worktree cleanup error for ${cmd.moduleUid}:`, error);
8287
+ result = {
8288
+ success: false,
8289
+ error: error instanceof Error ? error.message : String(error)
8290
+ };
8291
+ }
8292
+ try {
8293
+ await client.send({
8294
+ type: "worktree_cleanup_result",
8295
+ commandId: message.id,
8296
+ result
8297
+ });
8298
+ } catch (sendError) {
8299
+ console.error(`[Daemon] EP1035: Failed to send cleanup result (WebSocket may be disconnected):`, sendError);
8300
+ }
8301
+ console.log(`[Daemon] EP1035: Worktree cleanup for ${cmd.moduleUid} completed:`, result.success ? "success" : "failed");
8302
+ }
8303
+ });
8253
8304
  client.on("shutdown", async (message) => {
8254
8305
  const shutdownMessage = message;
8255
8306
  const reason = shutdownMessage.reason || "unknown";
@@ -8337,8 +8388,8 @@ var Daemon = class _Daemon {
8337
8388
  let daemonPid;
8338
8389
  try {
8339
8390
  const pidPath = getPidFilePath();
8340
- if (fs17.existsSync(pidPath)) {
8341
- const pidStr = fs17.readFileSync(pidPath, "utf-8").trim();
8391
+ if (fs16.existsSync(pidPath)) {
8392
+ const pidStr = fs16.readFileSync(pidPath, "utf-8").trim();
8342
8393
  daemonPid = parseInt(pidStr, 10);
8343
8394
  }
8344
8395
  } catch (pidError) {
@@ -8459,27 +8510,27 @@ var Daemon = class _Daemon {
8459
8510
  */
8460
8511
  async installGitHooks(projectPath) {
8461
8512
  const hooks = ["post-checkout", "pre-commit", "post-commit"];
8462
- const hooksDir = path18.join(projectPath, ".git", "hooks");
8463
- if (!fs17.existsSync(hooksDir)) {
8513
+ const hooksDir = path17.join(projectPath, ".git", "hooks");
8514
+ if (!fs16.existsSync(hooksDir)) {
8464
8515
  console.warn(`[Daemon] Hooks directory not found: ${hooksDir}`);
8465
8516
  return;
8466
8517
  }
8467
8518
  for (const hookName of hooks) {
8468
8519
  try {
8469
- const hookPath = path18.join(hooksDir, hookName);
8470
- const bundledHookPath = path18.join(__dirname, "..", "hooks", hookName);
8471
- if (!fs17.existsSync(bundledHookPath)) {
8520
+ const hookPath = path17.join(hooksDir, hookName);
8521
+ const bundledHookPath = path17.join(__dirname, "..", "hooks", hookName);
8522
+ if (!fs16.existsSync(bundledHookPath)) {
8472
8523
  console.warn(`[Daemon] Bundled hook not found: ${bundledHookPath}`);
8473
8524
  continue;
8474
8525
  }
8475
- const hookContent = fs17.readFileSync(bundledHookPath, "utf-8");
8476
- if (fs17.existsSync(hookPath)) {
8477
- const existingContent = fs17.readFileSync(hookPath, "utf-8");
8526
+ const hookContent = fs16.readFileSync(bundledHookPath, "utf-8");
8527
+ if (fs16.existsSync(hookPath)) {
8528
+ const existingContent = fs16.readFileSync(hookPath, "utf-8");
8478
8529
  if (existingContent === hookContent) {
8479
8530
  continue;
8480
8531
  }
8481
8532
  }
8482
- fs17.writeFileSync(hookPath, hookContent, { mode: 493 });
8533
+ fs16.writeFileSync(hookPath, hookContent, { mode: 493 });
8483
8534
  console.log(`[Daemon] Installed git hook: ${hookName}`);
8484
8535
  } catch (error) {
8485
8536
  console.warn(`[Daemon] Failed to install ${hookName} hook:`, error instanceof Error ? error.message : error);
@@ -9030,14 +9081,36 @@ var Daemon = class _Daemon {
9030
9081
  }
9031
9082
  const { orphaned } = manager.auditWorktrees(activeModuleUids);
9032
9083
  if (orphaned.length > 0) {
9033
- console.log(`[Daemon] EP957: Found ${orphaned.length} orphaned worktree(s) in ${config.workspaceSlug}/${config.projectSlug}:`);
9084
+ console.log(`[Daemon] EP1035: Found ${orphaned.length} orphaned worktree(s) in ${config.workspaceSlug}/${config.projectSlug}:`);
9034
9085
  for (const w of orphaned) {
9035
9086
  console.log(` - ${w.moduleUid} (branch: ${w.branchName})`);
9036
9087
  }
9037
- console.log('[Daemon] EP957: Run "episoda release <module>" to clean up');
9088
+ console.log("[Daemon] EP1035: Auto-cleaning orphaned worktrees...");
9089
+ for (const w of orphaned) {
9090
+ try {
9091
+ const removeResult = await manager.removeWorktree(w.moduleUid, true);
9092
+ if (removeResult.success) {
9093
+ console.log(`[Daemon] EP1035: Cleaned up orphaned worktree ${w.moduleUid}`);
9094
+ } else {
9095
+ if (removeResult.error?.includes("not found") || removeResult.error?.includes("No worktree found")) {
9096
+ console.log(`[Daemon] EP1035: Worktree ${w.moduleUid} already removed, syncing config`);
9097
+ await manager.pruneStaleWorktrees();
9098
+ } else {
9099
+ console.warn(`[Daemon] EP1035: Failed to clean up ${w.moduleUid}: ${removeResult.error}`);
9100
+ }
9101
+ }
9102
+ } catch (cleanupError) {
9103
+ console.warn(`[Daemon] EP1035: Error cleaning up ${w.moduleUid}:`, cleanupError.message);
9104
+ }
9105
+ }
9106
+ try {
9107
+ await manager.pruneStaleWorktrees();
9108
+ } catch (pruneError) {
9109
+ console.warn("[Daemon] EP1035: Failed to prune stale worktrees:", pruneError.message);
9110
+ }
9038
9111
  }
9039
9112
  } catch (error) {
9040
- console.warn(`[Daemon] EP957: Failed to audit ${projectPath}:`, error);
9113
+ console.warn(`[Daemon] EP1035: Failed to audit ${projectPath}:`, error);
9041
9114
  }
9042
9115
  }
9043
9116
  /**
@@ -9342,8 +9415,8 @@ var Daemon = class _Daemon {
9342
9415
  await this.shutdown();
9343
9416
  try {
9344
9417
  const pidPath = getPidFilePath();
9345
- if (fs17.existsSync(pidPath)) {
9346
- fs17.unlinkSync(pidPath);
9418
+ if (fs16.existsSync(pidPath)) {
9419
+ fs16.unlinkSync(pidPath);
9347
9420
  console.log("[Daemon] PID file cleaned up");
9348
9421
  }
9349
9422
  } catch (error) {