episoda 0.2.51 → 0.2.52

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.52",
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 {
@@ -6945,7 +6920,7 @@ var WorktreeManager = class _WorktreeManager {
6945
6920
  let prunedCount = 0;
6946
6921
  await this.updateConfigSafe((config) => {
6947
6922
  const initialCount = config.worktrees.length;
6948
- config.worktrees = config.worktrees.filter((w) => fs14.existsSync(w.worktreePath));
6923
+ config.worktrees = config.worktrees.filter((w) => fs13.existsSync(w.worktreePath));
6949
6924
  prunedCount = initialCount - config.worktrees.length;
6950
6925
  return config;
6951
6926
  });
@@ -7026,16 +7001,16 @@ var WorktreeManager = class _WorktreeManager {
7026
7001
  const retryInterval = 50;
7027
7002
  while (Date.now() - startTime < timeoutMs) {
7028
7003
  try {
7029
- fs14.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
7004
+ fs13.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
7030
7005
  return true;
7031
7006
  } catch (err) {
7032
7007
  if (err.code === "EEXIST") {
7033
7008
  try {
7034
- const stats = fs14.statSync(lockPath);
7009
+ const stats = fs13.statSync(lockPath);
7035
7010
  const lockAge = Date.now() - stats.mtimeMs;
7036
7011
  if (lockAge > 3e4) {
7037
7012
  try {
7038
- const lockContent = fs14.readFileSync(lockPath, "utf-8").trim();
7013
+ const lockContent = fs13.readFileSync(lockPath, "utf-8").trim();
7039
7014
  const lockPid = parseInt(lockContent, 10);
7040
7015
  if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
7041
7016
  await new Promise((resolve3) => setTimeout(resolve3, retryInterval));
@@ -7044,7 +7019,7 @@ var WorktreeManager = class _WorktreeManager {
7044
7019
  } catch {
7045
7020
  }
7046
7021
  try {
7047
- fs14.unlinkSync(lockPath);
7022
+ fs13.unlinkSync(lockPath);
7048
7023
  } catch {
7049
7024
  }
7050
7025
  continue;
@@ -7065,16 +7040,16 @@ var WorktreeManager = class _WorktreeManager {
7065
7040
  */
7066
7041
  releaseLock() {
7067
7042
  try {
7068
- fs14.unlinkSync(this.getLockPath());
7043
+ fs13.unlinkSync(this.getLockPath());
7069
7044
  } catch {
7070
7045
  }
7071
7046
  }
7072
7047
  readConfig() {
7073
7048
  try {
7074
- if (!fs14.existsSync(this.configPath)) {
7049
+ if (!fs13.existsSync(this.configPath)) {
7075
7050
  return null;
7076
7051
  }
7077
- const content = fs14.readFileSync(this.configPath, "utf-8");
7052
+ const content = fs13.readFileSync(this.configPath, "utf-8");
7078
7053
  return JSON.parse(content);
7079
7054
  } catch (error) {
7080
7055
  console.error("[WorktreeManager] Failed to read config:", error);
@@ -7083,11 +7058,11 @@ var WorktreeManager = class _WorktreeManager {
7083
7058
  }
7084
7059
  writeConfig(config) {
7085
7060
  try {
7086
- const dir = path15.dirname(this.configPath);
7087
- if (!fs14.existsSync(dir)) {
7088
- fs14.mkdirSync(dir, { recursive: true });
7061
+ const dir = path14.dirname(this.configPath);
7062
+ if (!fs13.existsSync(dir)) {
7063
+ fs13.mkdirSync(dir, { recursive: true });
7089
7064
  }
7090
- fs14.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
7065
+ fs13.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
7091
7066
  } catch (error) {
7092
7067
  console.error("[WorktreeManager] Failed to write config:", error);
7093
7068
  throw error;
@@ -7168,14 +7143,14 @@ var WorktreeManager = class _WorktreeManager {
7168
7143
  }
7169
7144
  try {
7170
7145
  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);
7146
+ const srcPath = path14.join(mainWorktree.worktreePath, file);
7147
+ const destPath = path14.join(worktree.worktreePath, file);
7148
+ if (fs13.existsSync(srcPath)) {
7149
+ const destDir = path14.dirname(destPath);
7150
+ if (!fs13.existsSync(destDir)) {
7151
+ fs13.mkdirSync(destDir, { recursive: true });
7152
+ }
7153
+ fs13.copyFileSync(srcPath, destPath);
7179
7154
  console.log(`[WorktreeManager] EP964: Copied ${file} to ${moduleUid} (deprecated)`);
7180
7155
  } else {
7181
7156
  console.log(`[WorktreeManager] EP964: Skipped ${file} (not found in main)`);
@@ -7258,27 +7233,27 @@ var WorktreeManager = class _WorktreeManager {
7258
7233
  }
7259
7234
  };
7260
7235
  function getEpisodaRoot() {
7261
- return process.env.EPISODA_ROOT || path15.join(require("os").homedir(), "episoda");
7236
+ return process.env.EPISODA_ROOT || path14.join(require("os").homedir(), "episoda");
7262
7237
  }
7263
7238
  async function isWorktreeProject(projectRoot) {
7264
7239
  const manager = new WorktreeManager(projectRoot);
7265
7240
  return manager.initialize();
7266
7241
  }
7267
7242
  async function findProjectRoot(startPath) {
7268
- let current = path15.resolve(startPath);
7243
+ let current = path14.resolve(startPath);
7269
7244
  const episodaRoot = getEpisodaRoot();
7270
7245
  if (!current.startsWith(episodaRoot)) {
7271
7246
  return null;
7272
7247
  }
7273
7248
  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)) {
7249
+ const bareDir = path14.join(current, ".bare");
7250
+ const episodaDir = path14.join(current, ".episoda");
7251
+ if (fs13.existsSync(bareDir) && fs13.existsSync(episodaDir)) {
7277
7252
  if (await isWorktreeProject(current)) {
7278
7253
  return current;
7279
7254
  }
7280
7255
  }
7281
- const parent = path15.dirname(current);
7256
+ const parent = path14.dirname(current);
7282
7257
  if (parent === current) {
7283
7258
  break;
7284
7259
  }
@@ -7288,19 +7263,19 @@ async function findProjectRoot(startPath) {
7288
7263
  }
7289
7264
 
7290
7265
  // src/utils/worktree.ts
7291
- var path16 = __toESM(require("path"));
7292
- var fs15 = __toESM(require("fs"));
7266
+ var path15 = __toESM(require("path"));
7267
+ var fs14 = __toESM(require("fs"));
7293
7268
  var os5 = __toESM(require("os"));
7294
7269
  var import_core11 = __toESM(require_dist());
7295
7270
  function getEpisodaRoot2() {
7296
- return process.env.EPISODA_ROOT || path16.join(os5.homedir(), "episoda");
7271
+ return process.env.EPISODA_ROOT || path15.join(os5.homedir(), "episoda");
7297
7272
  }
7298
7273
  function getWorktreeInfo(moduleUid, workspaceSlug, projectSlug) {
7299
7274
  const root = getEpisodaRoot2();
7300
- const worktreePath = path16.join(root, workspaceSlug, projectSlug, moduleUid);
7275
+ const worktreePath = path15.join(root, workspaceSlug, projectSlug, moduleUid);
7301
7276
  return {
7302
7277
  path: worktreePath,
7303
- exists: fs15.existsSync(worktreePath),
7278
+ exists: fs14.existsSync(worktreePath),
7304
7279
  moduleUid
7305
7280
  };
7306
7281
  }
@@ -7313,72 +7288,62 @@ async function getWorktreeInfoForModule(moduleUid) {
7313
7288
  return getWorktreeInfo(moduleUid, config.workspace_slug, config.project_slug);
7314
7289
  }
7315
7290
 
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
7291
  // src/framework-detector.ts
7327
- var fs16 = __toESM(require("fs"));
7328
- var path17 = __toESM(require("path"));
7292
+ var fs15 = __toESM(require("fs"));
7293
+ var path16 = __toESM(require("path"));
7329
7294
  function getInstallCommand(cwd) {
7330
- if (fs16.existsSync(path17.join(cwd, "bun.lockb"))) {
7295
+ if (fs15.existsSync(path16.join(cwd, "bun.lockb"))) {
7331
7296
  return {
7332
7297
  command: ["bun", "install"],
7333
7298
  description: "Installing dependencies with bun",
7334
7299
  detectedFrom: "bun.lockb"
7335
7300
  };
7336
7301
  }
7337
- if (fs16.existsSync(path17.join(cwd, "pnpm-lock.yaml"))) {
7302
+ if (fs15.existsSync(path16.join(cwd, "pnpm-lock.yaml"))) {
7338
7303
  return {
7339
7304
  command: ["pnpm", "install"],
7340
7305
  description: "Installing dependencies with pnpm",
7341
7306
  detectedFrom: "pnpm-lock.yaml"
7342
7307
  };
7343
7308
  }
7344
- if (fs16.existsSync(path17.join(cwd, "yarn.lock"))) {
7309
+ if (fs15.existsSync(path16.join(cwd, "yarn.lock"))) {
7345
7310
  return {
7346
7311
  command: ["yarn", "install"],
7347
7312
  description: "Installing dependencies with yarn",
7348
7313
  detectedFrom: "yarn.lock"
7349
7314
  };
7350
7315
  }
7351
- if (fs16.existsSync(path17.join(cwd, "package-lock.json"))) {
7316
+ if (fs15.existsSync(path16.join(cwd, "package-lock.json"))) {
7352
7317
  return {
7353
7318
  command: ["npm", "ci"],
7354
7319
  description: "Installing dependencies with npm ci",
7355
7320
  detectedFrom: "package-lock.json"
7356
7321
  };
7357
7322
  }
7358
- if (fs16.existsSync(path17.join(cwd, "package.json"))) {
7323
+ if (fs15.existsSync(path16.join(cwd, "package.json"))) {
7359
7324
  return {
7360
7325
  command: ["npm", "install"],
7361
7326
  description: "Installing dependencies with npm",
7362
7327
  detectedFrom: "package.json"
7363
7328
  };
7364
7329
  }
7365
- if (fs16.existsSync(path17.join(cwd, "Pipfile.lock")) || fs16.existsSync(path17.join(cwd, "Pipfile"))) {
7330
+ if (fs15.existsSync(path16.join(cwd, "Pipfile.lock")) || fs15.existsSync(path16.join(cwd, "Pipfile"))) {
7366
7331
  return {
7367
7332
  command: ["pipenv", "install"],
7368
7333
  description: "Installing dependencies with pipenv",
7369
- detectedFrom: fs16.existsSync(path17.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
7334
+ detectedFrom: fs15.existsSync(path16.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
7370
7335
  };
7371
7336
  }
7372
- if (fs16.existsSync(path17.join(cwd, "poetry.lock"))) {
7337
+ if (fs15.existsSync(path16.join(cwd, "poetry.lock"))) {
7373
7338
  return {
7374
7339
  command: ["poetry", "install"],
7375
7340
  description: "Installing dependencies with poetry",
7376
7341
  detectedFrom: "poetry.lock"
7377
7342
  };
7378
7343
  }
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");
7344
+ if (fs15.existsSync(path16.join(cwd, "pyproject.toml"))) {
7345
+ const pyprojectPath = path16.join(cwd, "pyproject.toml");
7346
+ const content = fs15.readFileSync(pyprojectPath, "utf-8");
7382
7347
  if (content.includes("[tool.poetry]")) {
7383
7348
  return {
7384
7349
  command: ["poetry", "install"],
@@ -7387,41 +7352,41 @@ function getInstallCommand(cwd) {
7387
7352
  };
7388
7353
  }
7389
7354
  }
7390
- if (fs16.existsSync(path17.join(cwd, "requirements.txt"))) {
7355
+ if (fs15.existsSync(path16.join(cwd, "requirements.txt"))) {
7391
7356
  return {
7392
7357
  command: ["pip", "install", "-r", "requirements.txt"],
7393
7358
  description: "Installing dependencies with pip",
7394
7359
  detectedFrom: "requirements.txt"
7395
7360
  };
7396
7361
  }
7397
- if (fs16.existsSync(path17.join(cwd, "Gemfile.lock")) || fs16.existsSync(path17.join(cwd, "Gemfile"))) {
7362
+ if (fs15.existsSync(path16.join(cwd, "Gemfile.lock")) || fs15.existsSync(path16.join(cwd, "Gemfile"))) {
7398
7363
  return {
7399
7364
  command: ["bundle", "install"],
7400
7365
  description: "Installing dependencies with bundler",
7401
- detectedFrom: fs16.existsSync(path17.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
7366
+ detectedFrom: fs15.existsSync(path16.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
7402
7367
  };
7403
7368
  }
7404
- if (fs16.existsSync(path17.join(cwd, "go.sum")) || fs16.existsSync(path17.join(cwd, "go.mod"))) {
7369
+ if (fs15.existsSync(path16.join(cwd, "go.sum")) || fs15.existsSync(path16.join(cwd, "go.mod"))) {
7405
7370
  return {
7406
7371
  command: ["go", "mod", "download"],
7407
7372
  description: "Downloading Go modules",
7408
- detectedFrom: fs16.existsSync(path17.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
7373
+ detectedFrom: fs15.existsSync(path16.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
7409
7374
  };
7410
7375
  }
7411
- if (fs16.existsSync(path17.join(cwd, "Cargo.lock")) || fs16.existsSync(path17.join(cwd, "Cargo.toml"))) {
7376
+ if (fs15.existsSync(path16.join(cwd, "Cargo.lock")) || fs15.existsSync(path16.join(cwd, "Cargo.toml"))) {
7412
7377
  return {
7413
7378
  command: ["cargo", "build"],
7414
7379
  description: "Building Rust project (downloads dependencies)",
7415
- detectedFrom: fs16.existsSync(path17.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
7380
+ detectedFrom: fs15.existsSync(path16.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
7416
7381
  };
7417
7382
  }
7418
7383
  return null;
7419
7384
  }
7420
7385
 
7421
7386
  // src/daemon/daemon-process.ts
7422
- var fs17 = __toESM(require("fs"));
7387
+ var fs16 = __toESM(require("fs"));
7423
7388
  var os6 = __toESM(require("os"));
7424
- var path18 = __toESM(require("path"));
7389
+ var path17 = __toESM(require("path"));
7425
7390
  var packageJson = require_package();
7426
7391
  async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
7427
7392
  const now = Date.now();
@@ -7772,6 +7737,7 @@ var Daemon = class _Daemon {
7772
7737
  }
7773
7738
  await tunnelManager.stopTunnel(moduleUid);
7774
7739
  await stopDevServer(moduleUid);
7740
+ releasePort(moduleUid);
7775
7741
  await clearTunnelUrl(moduleUid);
7776
7742
  this.tunnelHealthFailures.delete(moduleUid);
7777
7743
  console.log(`[Daemon] EP823: Tunnel stopped for ${moduleUid}`);
@@ -7922,7 +7888,7 @@ var Daemon = class _Daemon {
7922
7888
  client.updateActivity();
7923
7889
  try {
7924
7890
  const gitCmd = message.command;
7925
- const bareRepoPath = path18.join(projectPath, ".bare");
7891
+ const bareRepoPath = path17.join(projectPath, ".bare");
7926
7892
  const cwd = gitCmd.worktreePath || bareRepoPath;
7927
7893
  if (gitCmd.worktreePath) {
7928
7894
  console.log(`[Daemon] Routing command to worktree: ${gitCmd.worktreePath}`);
@@ -8076,7 +8042,8 @@ var Daemon = class _Daemon {
8076
8042
  return;
8077
8043
  }
8078
8044
  console.log(`[Daemon] EP1024: Using worktree path ${worktree.path} for ${cmd.moduleUid}`);
8079
- const port = cmd.port || detectDevPort(worktree.path);
8045
+ const port = cmd.port || allocatePort(cmd.moduleUid);
8046
+ console.log(`[Daemon] EP1038: Allocated port ${port} for ${cmd.moduleUid}`);
8080
8047
  const devConfig = await (0, import_core12.loadConfig)();
8081
8048
  const customCommand = devConfig?.project_settings?.worktree_dev_server_script;
8082
8049
  const startResult = await previewManager.startPreview({
@@ -8337,8 +8304,8 @@ var Daemon = class _Daemon {
8337
8304
  let daemonPid;
8338
8305
  try {
8339
8306
  const pidPath = getPidFilePath();
8340
- if (fs17.existsSync(pidPath)) {
8341
- const pidStr = fs17.readFileSync(pidPath, "utf-8").trim();
8307
+ if (fs16.existsSync(pidPath)) {
8308
+ const pidStr = fs16.readFileSync(pidPath, "utf-8").trim();
8342
8309
  daemonPid = parseInt(pidStr, 10);
8343
8310
  }
8344
8311
  } catch (pidError) {
@@ -8459,27 +8426,27 @@ var Daemon = class _Daemon {
8459
8426
  */
8460
8427
  async installGitHooks(projectPath) {
8461
8428
  const hooks = ["post-checkout", "pre-commit", "post-commit"];
8462
- const hooksDir = path18.join(projectPath, ".git", "hooks");
8463
- if (!fs17.existsSync(hooksDir)) {
8429
+ const hooksDir = path17.join(projectPath, ".git", "hooks");
8430
+ if (!fs16.existsSync(hooksDir)) {
8464
8431
  console.warn(`[Daemon] Hooks directory not found: ${hooksDir}`);
8465
8432
  return;
8466
8433
  }
8467
8434
  for (const hookName of hooks) {
8468
8435
  try {
8469
- const hookPath = path18.join(hooksDir, hookName);
8470
- const bundledHookPath = path18.join(__dirname, "..", "hooks", hookName);
8471
- if (!fs17.existsSync(bundledHookPath)) {
8436
+ const hookPath = path17.join(hooksDir, hookName);
8437
+ const bundledHookPath = path17.join(__dirname, "..", "hooks", hookName);
8438
+ if (!fs16.existsSync(bundledHookPath)) {
8472
8439
  console.warn(`[Daemon] Bundled hook not found: ${bundledHookPath}`);
8473
8440
  continue;
8474
8441
  }
8475
- const hookContent = fs17.readFileSync(bundledHookPath, "utf-8");
8476
- if (fs17.existsSync(hookPath)) {
8477
- const existingContent = fs17.readFileSync(hookPath, "utf-8");
8442
+ const hookContent = fs16.readFileSync(bundledHookPath, "utf-8");
8443
+ if (fs16.existsSync(hookPath)) {
8444
+ const existingContent = fs16.readFileSync(hookPath, "utf-8");
8478
8445
  if (existingContent === hookContent) {
8479
8446
  continue;
8480
8447
  }
8481
8448
  }
8482
- fs17.writeFileSync(hookPath, hookContent, { mode: 493 });
8449
+ fs16.writeFileSync(hookPath, hookContent, { mode: 493 });
8483
8450
  console.log(`[Daemon] Installed git hook: ${hookName}`);
8484
8451
  } catch (error) {
8485
8452
  console.warn(`[Daemon] Failed to install ${hookName} hook:`, error instanceof Error ? error.message : error);
@@ -9342,8 +9309,8 @@ var Daemon = class _Daemon {
9342
9309
  await this.shutdown();
9343
9310
  try {
9344
9311
  const pidPath = getPidFilePath();
9345
- if (fs17.existsSync(pidPath)) {
9346
- fs17.unlinkSync(pidPath);
9312
+ if (fs16.existsSync(pidPath)) {
9313
+ fs16.unlinkSync(pidPath);
9347
9314
  console.log("[Daemon] PID file cleaned up");
9348
9315
  }
9349
9316
  } catch (error) {