episoda 0.2.52 → 0.2.54

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 fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1566
+ const fs19 = 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 fs17.access(rebaseMergePath);
1570
+ await fs19.access(rebaseMergePath);
1571
1571
  inRebase = true;
1572
1572
  } catch {
1573
1573
  try {
1574
- await fs17.access(rebaseApplyPath);
1574
+ await fs19.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 fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1628
+ const fs19 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1629
1629
  try {
1630
- await fs17.access(command.path);
1630
+ await fs19.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 fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1684
+ const fs19 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1685
1685
  try {
1686
- await fs17.access(command.path);
1686
+ await fs19.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 fs17.rm(command.path, { recursive: true, force: true });
1721
+ await fs19.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 fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1856
- const path18 = await Promise.resolve().then(() => __importStar(require("path")));
1855
+ const fs19 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1856
+ const path20 = await Promise.resolve().then(() => __importStar(require("path")));
1857
1857
  try {
1858
- await fs17.access(command.path);
1858
+ await fs19.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 = path18.dirname(command.path);
1867
+ const parentDir = path20.dirname(command.path);
1868
1868
  try {
1869
- await fs17.mkdir(parentDir, { recursive: true });
1869
+ await fs19.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 fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1918
- const path18 = await Promise.resolve().then(() => __importStar(require("path")));
1917
+ const fs19 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1918
+ const path20 = 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 = path18.join(currentPath, ".bare");
1924
- const episodaDir = path18.join(currentPath, ".episoda");
1923
+ const bareDir = path20.join(currentPath, ".bare");
1924
+ const episodaDir = path20.join(currentPath, ".episoda");
1925
1925
  try {
1926
- await fs17.access(bareDir);
1927
- await fs17.access(episodaDir);
1926
+ await fs19.access(bareDir);
1927
+ await fs19.access(episodaDir);
1928
1928
  projectPath = currentPath;
1929
1929
  bareRepoPath = bareDir;
1930
1930
  break;
1931
1931
  } catch {
1932
- const parentPath = path18.dirname(currentPath);
1932
+ const parentPath = path20.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 fs17 = __importStar(require("fs"));
2542
- var path18 = __importStar(require("path"));
2543
- var os7 = __importStar(require("os"));
2541
+ var fs19 = __importStar(require("fs"));
2542
+ var path20 = __importStar(require("path"));
2543
+ var os9 = __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 || path18.join(os7.homedir(), ".episoda");
2547
+ return process.env.EPISODA_CONFIG_DIR || path20.join(os9.homedir(), ".episoda");
2548
2548
  }
2549
2549
  function getConfigPath(configPath) {
2550
2550
  if (configPath) {
2551
2551
  return configPath;
2552
2552
  }
2553
- return path18.join(getConfigDir8(), DEFAULT_CONFIG_FILE);
2553
+ return path20.join(getConfigDir8(), DEFAULT_CONFIG_FILE);
2554
2554
  }
2555
2555
  function ensureConfigDir(configPath) {
2556
- const dir = path18.dirname(configPath);
2557
- const isNew = !fs17.existsSync(dir);
2556
+ const dir = path20.dirname(configPath);
2557
+ const isNew = !fs19.existsSync(dir);
2558
2558
  if (isNew) {
2559
- fs17.mkdirSync(dir, { recursive: true, mode: 448 });
2559
+ fs19.mkdirSync(dir, { recursive: true, mode: 448 });
2560
2560
  }
2561
2561
  if (process.platform === "darwin") {
2562
- const nosyncPath = path18.join(dir, ".nosync");
2563
- if (isNew || !fs17.existsSync(nosyncPath)) {
2562
+ const nosyncPath = path20.join(dir, ".nosync");
2563
+ if (isNew || !fs19.existsSync(nosyncPath)) {
2564
2564
  try {
2565
- fs17.writeFileSync(nosyncPath, "", { mode: 384 });
2565
+ fs19.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 (fs17.existsSync(fullPath)) {
2577
+ if (fs19.existsSync(fullPath)) {
2578
2578
  try {
2579
- const content = fs17.readFileSync(fullPath, "utf8");
2579
+ const content = fs19.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
- fs17.writeFileSync(fullPath, content, { mode: 384 });
2604
+ fs19.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.52",
2721
+ version: "0.2.54",
2722
2722
  description: "CLI tool for Episoda local development workflow orchestration",
2723
2723
  main: "dist/index.js",
2724
2724
  types: "dist/index.d.ts",
@@ -5272,10 +5272,10 @@ var import_events3 = require("events");
5272
5272
  var import_fs = require("fs");
5273
5273
 
5274
5274
  // src/preview/dev-server-runner.ts
5275
- var import_child_process9 = require("child_process");
5275
+ var import_child_process10 = require("child_process");
5276
5276
  var http = __toESM(require("http"));
5277
- var fs11 = __toESM(require("fs"));
5278
- var path12 = __toESM(require("path"));
5277
+ var fs12 = __toESM(require("fs"));
5278
+ var path13 = __toESM(require("path"));
5279
5279
  var import_events2 = require("events");
5280
5280
  var import_core8 = __toESM(require_dist());
5281
5281
 
@@ -5450,6 +5450,296 @@ No cached values available as fallback.`
5450
5450
  }
5451
5451
  }
5452
5452
 
5453
+ // src/preview/dev-server-registry.ts
5454
+ var fs11 = __toESM(require("fs"));
5455
+ var path12 = __toESM(require("path"));
5456
+ var os5 = __toESM(require("os"));
5457
+ var import_child_process9 = require("child_process");
5458
+ var DEV_SERVER_REGISTRY_DIR = path12.join(os5.homedir(), ".episoda", "dev-servers");
5459
+ var DevServerRegistry = class {
5460
+ constructor() {
5461
+ this.ensureRegistryDir();
5462
+ }
5463
+ /**
5464
+ * Ensure the registry directory exists
5465
+ */
5466
+ ensureRegistryDir() {
5467
+ try {
5468
+ if (!fs11.existsSync(DEV_SERVER_REGISTRY_DIR)) {
5469
+ console.log(`[DevServerRegistry] EP1042: Creating registry directory: ${DEV_SERVER_REGISTRY_DIR}`);
5470
+ fs11.mkdirSync(DEV_SERVER_REGISTRY_DIR, { recursive: true });
5471
+ }
5472
+ } catch (error) {
5473
+ console.error(`[DevServerRegistry] EP1042: Failed to create registry directory:`, error);
5474
+ throw error;
5475
+ }
5476
+ }
5477
+ /**
5478
+ * Get the registry file path for a module
5479
+ */
5480
+ getEntryPath(moduleUid) {
5481
+ return path12.join(DEV_SERVER_REGISTRY_DIR, `${moduleUid}.json`);
5482
+ }
5483
+ /**
5484
+ * Register a dev server
5485
+ *
5486
+ * @param entry - Dev server entry to register
5487
+ */
5488
+ register(entry) {
5489
+ try {
5490
+ this.ensureRegistryDir();
5491
+ const entryPath = this.getEntryPath(entry.moduleUid);
5492
+ fs11.writeFileSync(entryPath, JSON.stringify(entry, null, 2), "utf8");
5493
+ console.log(`[DevServerRegistry] EP1042: Registered ${entry.moduleUid} (PID ${entry.pid}, port ${entry.port})`);
5494
+ } catch (error) {
5495
+ console.error(`[DevServerRegistry] EP1042: Failed to register ${entry.moduleUid}:`, error);
5496
+ }
5497
+ }
5498
+ /**
5499
+ * Unregister a dev server
5500
+ *
5501
+ * @param moduleUid - Module UID to unregister
5502
+ */
5503
+ unregister(moduleUid) {
5504
+ try {
5505
+ const entryPath = this.getEntryPath(moduleUid);
5506
+ if (fs11.existsSync(entryPath)) {
5507
+ fs11.unlinkSync(entryPath);
5508
+ console.log(`[DevServerRegistry] EP1042: Unregistered ${moduleUid}`);
5509
+ }
5510
+ } catch (error) {
5511
+ console.error(`[DevServerRegistry] EP1042: Failed to unregister ${moduleUid}:`, error);
5512
+ }
5513
+ }
5514
+ /**
5515
+ * Get a dev server entry by module UID
5516
+ *
5517
+ * @param moduleUid - Module UID to look up
5518
+ * @returns Entry if found and valid, null otherwise
5519
+ */
5520
+ getByModule(moduleUid) {
5521
+ try {
5522
+ const entryPath = this.getEntryPath(moduleUid);
5523
+ if (!fs11.existsSync(entryPath)) {
5524
+ return null;
5525
+ }
5526
+ const content = fs11.readFileSync(entryPath, "utf8");
5527
+ const entry = JSON.parse(content);
5528
+ if (!entry.pid || !entry.port || !entry.worktreePath) {
5529
+ console.warn(`[DevServerRegistry] EP1042: Invalid entry for ${moduleUid}, removing`);
5530
+ this.unregister(moduleUid);
5531
+ return null;
5532
+ }
5533
+ if (!this.isProcessRunning(entry.pid)) {
5534
+ console.log(`[DevServerRegistry] EP1042: PID ${entry.pid} for ${moduleUid} is not running, removing stale entry`);
5535
+ this.unregister(moduleUid);
5536
+ return null;
5537
+ }
5538
+ return entry;
5539
+ } catch (error) {
5540
+ console.warn(`[DevServerRegistry] EP1042: Failed to read entry for ${moduleUid}:`, error);
5541
+ this.unregister(moduleUid);
5542
+ return null;
5543
+ }
5544
+ }
5545
+ /**
5546
+ * Get a dev server entry by port
5547
+ *
5548
+ * @param port - Port to look up
5549
+ * @returns Entry if found, null otherwise
5550
+ */
5551
+ getByPort(port) {
5552
+ const entries = this.getAll();
5553
+ return entries.find((e) => e.port === port) || null;
5554
+ }
5555
+ /**
5556
+ * Get all registered dev server entries
5557
+ *
5558
+ * @returns Array of valid entries
5559
+ */
5560
+ getAll() {
5561
+ const entries = [];
5562
+ try {
5563
+ this.ensureRegistryDir();
5564
+ const files = fs11.readdirSync(DEV_SERVER_REGISTRY_DIR).filter((f) => f.endsWith(".json"));
5565
+ for (const file of files) {
5566
+ const moduleUid = file.replace(".json", "");
5567
+ const entry = this.getByModule(moduleUid);
5568
+ if (entry) {
5569
+ entries.push(entry);
5570
+ }
5571
+ }
5572
+ } catch (error) {
5573
+ console.error(`[DevServerRegistry] EP1042: Failed to read registry:`, error);
5574
+ }
5575
+ return entries;
5576
+ }
5577
+ /**
5578
+ * Check if a process is running by PID
5579
+ */
5580
+ isProcessRunning(pid) {
5581
+ try {
5582
+ process.kill(pid, 0);
5583
+ return true;
5584
+ } catch {
5585
+ return false;
5586
+ }
5587
+ }
5588
+ /**
5589
+ * Kill a process by PID
5590
+ *
5591
+ * @param pid - Process ID to kill
5592
+ * @param signal - Signal to send (default: SIGTERM)
5593
+ * @returns True if signal was sent
5594
+ */
5595
+ killProcess(pid, signal = "SIGTERM") {
5596
+ try {
5597
+ process.kill(pid, signal);
5598
+ console.log(`[DevServerRegistry] EP1042: Sent ${signal} to PID ${pid}`);
5599
+ return true;
5600
+ } catch {
5601
+ return false;
5602
+ }
5603
+ }
5604
+ /**
5605
+ * Get the working directory of a process by PID
5606
+ *
5607
+ * @param pid - Process ID
5608
+ * @returns Working directory or null
5609
+ */
5610
+ getProcessCwd(pid) {
5611
+ try {
5612
+ const output = (0, import_child_process9.execSync)(`lsof -p ${pid} -Fn | grep ^n | grep cwd | head -1`, {
5613
+ encoding: "utf8",
5614
+ timeout: 5e3
5615
+ }).trim();
5616
+ if (output.startsWith("n")) {
5617
+ return output.slice(1);
5618
+ }
5619
+ return null;
5620
+ } catch {
5621
+ try {
5622
+ return fs11.readlinkSync(`/proc/${pid}/cwd`);
5623
+ } catch {
5624
+ return null;
5625
+ }
5626
+ }
5627
+ }
5628
+ /**
5629
+ * Find node processes running in a specific directory
5630
+ *
5631
+ * @param worktreePath - Path to search for
5632
+ * @returns Array of PIDs
5633
+ */
5634
+ findProcessesInWorktree(worktreePath) {
5635
+ const pids = [];
5636
+ try {
5637
+ const output = (0, import_child_process9.execSync)(
5638
+ `lsof -c node -c next | grep "${worktreePath}" | awk '{print $2}' | sort -u`,
5639
+ { encoding: "utf8", timeout: 5e3 }
5640
+ ).trim();
5641
+ if (output) {
5642
+ for (const line of output.split("\n")) {
5643
+ const pid = parseInt(line, 10);
5644
+ if (!isNaN(pid) && pid > 0) {
5645
+ pids.push(pid);
5646
+ }
5647
+ }
5648
+ }
5649
+ } catch {
5650
+ }
5651
+ return [...new Set(pids)];
5652
+ }
5653
+ /**
5654
+ * Clean up orphaned dev servers
5655
+ *
5656
+ * An orphan is a dev server whose:
5657
+ * - PID is no longer running
5658
+ * - Module is not in doing/review state
5659
+ * - Worktree no longer exists
5660
+ *
5661
+ * @param activeModuleUids - UIDs of modules in doing/review state
5662
+ * @returns Cleanup stats
5663
+ */
5664
+ async cleanupOrphans(activeModuleUids) {
5665
+ const cleaned = [];
5666
+ try {
5667
+ const entries = this.getAll();
5668
+ for (const entry of entries) {
5669
+ if (!activeModuleUids.includes(entry.moduleUid)) {
5670
+ console.log(`[DevServerRegistry] EP1042: Module ${entry.moduleUid} not active, cleaning up PID ${entry.pid}`);
5671
+ if (this.isProcessRunning(entry.pid)) {
5672
+ this.killProcess(entry.pid, "SIGTERM");
5673
+ await this.wait(1e3);
5674
+ if (this.isProcessRunning(entry.pid)) {
5675
+ this.killProcess(entry.pid, "SIGKILL");
5676
+ }
5677
+ cleaned.push(entry.pid);
5678
+ }
5679
+ this.unregister(entry.moduleUid);
5680
+ }
5681
+ }
5682
+ if (cleaned.length > 0) {
5683
+ console.log(`[DevServerRegistry] EP1042: Cleaned up ${cleaned.length} orphaned dev server(s): ${cleaned.join(", ")}`);
5684
+ }
5685
+ } catch (error) {
5686
+ console.error(`[DevServerRegistry] EP1042: Error during orphan cleanup:`, error);
5687
+ }
5688
+ return { cleaned: cleaned.length, pids: cleaned };
5689
+ }
5690
+ /**
5691
+ * Find processes on a specific port
5692
+ *
5693
+ * @param port - Port to check
5694
+ * @returns Array of PIDs
5695
+ */
5696
+ findProcessesOnPort(port) {
5697
+ try {
5698
+ const output = (0, import_child_process9.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
5699
+ if (!output) {
5700
+ return [];
5701
+ }
5702
+ return output.split("\n").map((pid) => parseInt(pid, 10)).filter((pid) => !isNaN(pid));
5703
+ } catch {
5704
+ return [];
5705
+ }
5706
+ }
5707
+ /**
5708
+ * Kill all processes on a specific port
5709
+ *
5710
+ * @param port - Port to clear
5711
+ * @returns PIDs that were killed
5712
+ */
5713
+ async killProcessesOnPort(port) {
5714
+ const pids = this.findProcessesOnPort(port);
5715
+ const killed = [];
5716
+ for (const pid of pids) {
5717
+ this.killProcess(pid, "SIGTERM");
5718
+ killed.push(pid);
5719
+ }
5720
+ if (killed.length > 0) {
5721
+ await this.wait(1e3);
5722
+ for (const pid of killed) {
5723
+ if (this.isProcessRunning(pid)) {
5724
+ this.killProcess(pid, "SIGKILL");
5725
+ }
5726
+ }
5727
+ console.log(`[DevServerRegistry] EP1042: Killed ${killed.length} process(es) on port ${port}`);
5728
+ }
5729
+ return killed;
5730
+ }
5731
+ wait(ms) {
5732
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
5733
+ }
5734
+ };
5735
+ var registryInstance = null;
5736
+ function getDevServerRegistry() {
5737
+ if (!registryInstance) {
5738
+ registryInstance = new DevServerRegistry();
5739
+ }
5740
+ return registryInstance;
5741
+ }
5742
+
5453
5743
  // src/preview/dev-server-runner.ts
5454
5744
  var DevServerRunner = class extends import_events2.EventEmitter {
5455
5745
  constructor() {
@@ -5458,6 +5748,9 @@ var DevServerRunner = class extends import_events2.EventEmitter {
5458
5748
  }
5459
5749
  /**
5460
5750
  * Start a dev server for a module
5751
+ *
5752
+ * EP1042: Now validates worktree ownership when port is already in use.
5753
+ * If a different worktree's server is on the port, it kills and restarts.
5461
5754
  */
5462
5755
  async start(config) {
5463
5756
  const {
@@ -5467,9 +5760,26 @@ var DevServerRunner = class extends import_events2.EventEmitter {
5467
5760
  customCommand,
5468
5761
  autoRestart = true
5469
5762
  } = config;
5763
+ const registry = getDevServerRegistry();
5470
5764
  if (await isPortInUse(port)) {
5471
- console.log(`[DevServerRunner] Server already running on port ${port}`);
5472
- return { success: true, alreadyRunning: true };
5765
+ const existingEntry = registry.getByPort(port);
5766
+ if (existingEntry) {
5767
+ if (existingEntry.worktreePath === projectPath && existingEntry.moduleUid === moduleUid) {
5768
+ console.log(`[DevServerRunner] EP1042: Correct server already running on port ${port} for ${moduleUid}`);
5769
+ return { success: true, alreadyRunning: true };
5770
+ }
5771
+ console.log(`[DevServerRunner] EP1042: Port ${port} owned by ${existingEntry.moduleUid} (${existingEntry.worktreePath}), killing...`);
5772
+ await this.killProcessOnPort(port);
5773
+ registry.unregister(existingEntry.moduleUid);
5774
+ this.servers.delete(existingEntry.moduleUid);
5775
+ } else {
5776
+ console.log(`[DevServerRunner] EP1042: Unknown process on port ${port}, killing...`);
5777
+ await this.killProcessOnPort(port);
5778
+ }
5779
+ await this.wait(1e3);
5780
+ if (await isPortInUse(port)) {
5781
+ return { success: false, error: `Port ${port} still in use after cleanup` };
5782
+ }
5473
5783
  }
5474
5784
  const existing = this.servers.get(moduleUid);
5475
5785
  if (existing && !existing.process.killed) {
@@ -5505,6 +5815,15 @@ var DevServerRunner = class extends import_events2.EventEmitter {
5505
5815
  this.writeToLog(logPath, "Failed to start within timeout", true);
5506
5816
  return { success: false, error: "Dev server failed to start within timeout" };
5507
5817
  }
5818
+ if (process2.pid) {
5819
+ registry.register({
5820
+ pid: process2.pid,
5821
+ port,
5822
+ worktreePath: projectPath,
5823
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5824
+ moduleUid
5825
+ });
5826
+ }
5508
5827
  console.log(`[DevServerRunner] Server started successfully on port ${port}`);
5509
5828
  this.writeToLog(logPath, "Server started successfully");
5510
5829
  this.emit("started", moduleUid, port);
@@ -5517,23 +5836,39 @@ var DevServerRunner = class extends import_events2.EventEmitter {
5517
5836
  }
5518
5837
  /**
5519
5838
  * Stop a dev server
5839
+ *
5840
+ * EP1042: Now also unregisters from persistent registry.
5520
5841
  */
5521
5842
  async stop(moduleUid) {
5522
5843
  const state = this.servers.get(moduleUid);
5523
- if (!state) {
5844
+ const registry = getDevServerRegistry();
5845
+ const registryEntry = registry.getByModule(moduleUid);
5846
+ if (!state && !registryEntry) {
5524
5847
  return;
5525
5848
  }
5526
- state.autoRestartEnabled = false;
5527
- if (!state.process.killed) {
5528
- console.log(`[DevServerRunner] Stopping server for ${moduleUid}`);
5529
- this.writeToLog(state.logFile, "Stopping server (manual stop)");
5530
- state.process.kill("SIGTERM");
5531
- await this.wait(2e3);
5849
+ if (state) {
5850
+ state.autoRestartEnabled = false;
5532
5851
  if (!state.process.killed) {
5533
- state.process.kill("SIGKILL");
5852
+ console.log(`[DevServerRunner] Stopping server for ${moduleUid}`);
5853
+ this.writeToLog(state.logFile, "Stopping server (manual stop)");
5854
+ state.process.kill("SIGTERM");
5855
+ await this.wait(2e3);
5856
+ if (!state.process.killed) {
5857
+ state.process.kill("SIGKILL");
5858
+ }
5859
+ }
5860
+ } else if (registryEntry) {
5861
+ console.log(`[DevServerRunner] EP1042: Stopping orphaned server for ${moduleUid} (PID ${registryEntry.pid})`);
5862
+ if (registry.isProcessRunning(registryEntry.pid)) {
5863
+ registry.killProcess(registryEntry.pid, "SIGTERM");
5864
+ await this.wait(2e3);
5865
+ if (registry.isProcessRunning(registryEntry.pid)) {
5866
+ registry.killProcess(registryEntry.pid, "SIGKILL");
5867
+ }
5534
5868
  }
5535
5869
  }
5536
5870
  this.servers.delete(moduleUid);
5871
+ registry.unregister(moduleUid);
5537
5872
  this.emit("stopped", moduleUid);
5538
5873
  }
5539
5874
  /**
@@ -5614,7 +5949,7 @@ var DevServerRunner = class extends import_events2.EventEmitter {
5614
5949
  */
5615
5950
  async killProcessOnPort(port) {
5616
5951
  try {
5617
- const result = (0, import_child_process9.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
5952
+ const result = (0, import_child_process10.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
5618
5953
  if (!result) {
5619
5954
  return true;
5620
5955
  }
@@ -5622,15 +5957,15 @@ var DevServerRunner = class extends import_events2.EventEmitter {
5622
5957
  console.log(`[DevServerRunner] Found ${pids.length} process(es) on port ${port}`);
5623
5958
  for (const pid of pids) {
5624
5959
  try {
5625
- (0, import_child_process9.execSync)(`kill -15 ${pid} 2>/dev/null || true`);
5960
+ (0, import_child_process10.execSync)(`kill -15 ${pid} 2>/dev/null || true`);
5626
5961
  } catch {
5627
5962
  }
5628
5963
  }
5629
5964
  await this.wait(1e3);
5630
5965
  for (const pid of pids) {
5631
5966
  try {
5632
- (0, import_child_process9.execSync)(`kill -0 ${pid} 2>/dev/null`);
5633
- (0, import_child_process9.execSync)(`kill -9 ${pid} 2>/dev/null || true`);
5967
+ (0, import_child_process10.execSync)(`kill -0 ${pid} 2>/dev/null`);
5968
+ (0, import_child_process10.execSync)(`kill -9 ${pid} 2>/dev/null || true`);
5634
5969
  } catch {
5635
5970
  }
5636
5971
  }
@@ -5654,8 +5989,8 @@ var DevServerRunner = class extends import_events2.EventEmitter {
5654
5989
  cacheTtl: 300
5655
5990
  });
5656
5991
  console.log(`[DevServerRunner] Loaded ${Object.keys(result.envVars).length} env vars`);
5657
- const envFilePath = path12.join(projectPath, ".env");
5658
- if (!fs11.existsSync(envFilePath) && Object.keys(result.envVars).length > 0) {
5992
+ const envFilePath = path13.join(projectPath, ".env");
5993
+ if (!fs12.existsSync(envFilePath) && Object.keys(result.envVars).length > 0) {
5659
5994
  console.log(`[DevServerRunner] Writing .env file`);
5660
5995
  writeEnvFile(projectPath, result.envVars);
5661
5996
  }
@@ -5679,7 +6014,7 @@ var DevServerRunner = class extends import_events2.EventEmitter {
5679
6014
  PORT: String(port),
5680
6015
  NODE_OPTIONS: enhancedNodeOptions
5681
6016
  };
5682
- const proc = (0, import_child_process9.spawn)(cmd, args, {
6017
+ const proc = (0, import_child_process10.spawn)(cmd, args, {
5683
6018
  cwd: projectPath,
5684
6019
  env: mergedEnv,
5685
6020
  stdio: ["ignore", "pipe", "pipe"],
@@ -5804,25 +6139,25 @@ var DevServerRunner = class extends import_events2.EventEmitter {
5804
6139
  return new Promise((resolve3) => setTimeout(resolve3, ms));
5805
6140
  }
5806
6141
  getLogsDir() {
5807
- const logsDir = path12.join((0, import_core8.getConfigDir)(), "logs");
5808
- if (!fs11.existsSync(logsDir)) {
5809
- fs11.mkdirSync(logsDir, { recursive: true });
6142
+ const logsDir = path13.join((0, import_core8.getConfigDir)(), "logs");
6143
+ if (!fs12.existsSync(logsDir)) {
6144
+ fs12.mkdirSync(logsDir, { recursive: true });
5810
6145
  }
5811
6146
  return logsDir;
5812
6147
  }
5813
6148
  getLogFilePath(moduleUid) {
5814
- return path12.join(this.getLogsDir(), `dev-${moduleUid}.log`);
6149
+ return path13.join(this.getLogsDir(), `dev-${moduleUid}.log`);
5815
6150
  }
5816
6151
  rotateLogIfNeeded(logPath) {
5817
6152
  try {
5818
- if (fs11.existsSync(logPath)) {
5819
- const stats = fs11.statSync(logPath);
6153
+ if (fs12.existsSync(logPath)) {
6154
+ const stats = fs12.statSync(logPath);
5820
6155
  if (stats.size > DEV_SERVER_CONSTANTS.MAX_LOG_SIZE_BYTES) {
5821
6156
  const backupPath = `${logPath}.1`;
5822
- if (fs11.existsSync(backupPath)) {
5823
- fs11.unlinkSync(backupPath);
6157
+ if (fs12.existsSync(backupPath)) {
6158
+ fs12.unlinkSync(backupPath);
5824
6159
  }
5825
- fs11.renameSync(logPath, backupPath);
6160
+ fs12.renameSync(logPath, backupPath);
5826
6161
  }
5827
6162
  }
5828
6163
  } catch {
@@ -5833,7 +6168,7 @@ var DevServerRunner = class extends import_events2.EventEmitter {
5833
6168
  try {
5834
6169
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
5835
6170
  const prefix = isError ? "ERR" : "OUT";
5836
- fs11.appendFileSync(logPath, `[${timestamp}] [${prefix}] ${line}
6171
+ fs12.appendFileSync(logPath, `[${timestamp}] [${prefix}] ${line}
5837
6172
  `);
5838
6173
  } catch {
5839
6174
  }
@@ -5861,13 +6196,55 @@ function getDevServerRunner() {
5861
6196
  }
5862
6197
 
5863
6198
  // src/utils/port-allocator.ts
6199
+ var fs13 = __toESM(require("fs"));
6200
+ var path14 = __toESM(require("path"));
6201
+ var os6 = __toESM(require("os"));
5864
6202
  var PORT_RANGE_START = 3100;
5865
6203
  var PORT_RANGE_END = 3199;
5866
6204
  var PORT_WARNING_THRESHOLD = 80;
6205
+ var PORTS_FILE = path14.join(os6.homedir(), ".episoda", "ports.json");
5867
6206
  var portAssignments = /* @__PURE__ */ new Map();
6207
+ var initialized = false;
6208
+ function loadFromDisk() {
6209
+ if (initialized) return;
6210
+ initialized = true;
6211
+ try {
6212
+ if (fs13.existsSync(PORTS_FILE)) {
6213
+ const content = fs13.readFileSync(PORTS_FILE, "utf8");
6214
+ const data = JSON.parse(content);
6215
+ for (const [moduleUid, port] of Object.entries(data)) {
6216
+ if (typeof port === "number" && port >= PORT_RANGE_START && port <= PORT_RANGE_END) {
6217
+ portAssignments.set(moduleUid, port);
6218
+ }
6219
+ }
6220
+ if (portAssignments.size > 0) {
6221
+ console.log(`[PortAllocator] EP1042: Loaded ${portAssignments.size} port assignments from disk`);
6222
+ }
6223
+ }
6224
+ } catch (error) {
6225
+ console.warn(`[PortAllocator] EP1042: Failed to load ports.json:`, error);
6226
+ }
6227
+ }
6228
+ function saveToDisk() {
6229
+ try {
6230
+ const dir = path14.dirname(PORTS_FILE);
6231
+ if (!fs13.existsSync(dir)) {
6232
+ fs13.mkdirSync(dir, { recursive: true });
6233
+ }
6234
+ const data = {};
6235
+ for (const [moduleUid, port] of portAssignments) {
6236
+ data[moduleUid] = port;
6237
+ }
6238
+ fs13.writeFileSync(PORTS_FILE, JSON.stringify(data, null, 2), "utf8");
6239
+ } catch (error) {
6240
+ console.warn(`[PortAllocator] EP1042: Failed to save ports.json:`, error);
6241
+ }
6242
+ }
5868
6243
  function allocatePort(moduleUid) {
6244
+ loadFromDisk();
5869
6245
  const existing = portAssignments.get(moduleUid);
5870
6246
  if (existing) {
6247
+ console.log(`[PortAllocator] EP1042: Returning existing port ${existing} for ${moduleUid}`);
5871
6248
  return existing;
5872
6249
  }
5873
6250
  const usedPorts = new Set(portAssignments.values());
@@ -5879,7 +6256,8 @@ function allocatePort(moduleUid) {
5879
6256
  for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
5880
6257
  if (!usedPorts.has(port)) {
5881
6258
  portAssignments.set(moduleUid, port);
5882
- console.log(`[PortAllocator] Assigned port ${port} to ${moduleUid}`);
6259
+ saveToDisk();
6260
+ console.log(`[PortAllocator] EP1042: Allocated port ${port} to ${moduleUid}`);
5883
6261
  return port;
5884
6262
  }
5885
6263
  }
@@ -5888,17 +6266,41 @@ function allocatePort(moduleUid) {
5888
6266
  );
5889
6267
  }
5890
6268
  function releasePort(moduleUid) {
6269
+ loadFromDisk();
5891
6270
  const port = portAssignments.get(moduleUid);
5892
6271
  if (port) {
5893
6272
  portAssignments.delete(moduleUid);
5894
- console.log(`[PortAllocator] Released port ${port} from ${moduleUid}`);
6273
+ saveToDisk();
6274
+ console.log(`[PortAllocator] EP1042: Released port ${port} from ${moduleUid}`);
5895
6275
  }
5896
6276
  }
5897
6277
  function clearAllPorts() {
5898
6278
  const count = portAssignments.size;
5899
6279
  portAssignments.clear();
6280
+ saveToDisk();
5900
6281
  if (count > 0) {
5901
- console.log(`[PortAllocator] Cleared ${count} port assignments`);
6282
+ console.log(`[PortAllocator] EP1042: Cleared ${count} port assignments`);
6283
+ }
6284
+ }
6285
+ function reconcileWithRegistry(activeModules) {
6286
+ loadFromDisk();
6287
+ let changed = false;
6288
+ for (const [moduleUid, port] of portAssignments) {
6289
+ if (!activeModules.has(moduleUid)) {
6290
+ console.log(`[PortAllocator] EP1042: Removing stale allocation ${moduleUid} -> ${port}`);
6291
+ portAssignments.delete(moduleUid);
6292
+ changed = true;
6293
+ }
6294
+ }
6295
+ for (const [moduleUid, port] of activeModules) {
6296
+ if (!portAssignments.has(moduleUid)) {
6297
+ console.log(`[PortAllocator] EP1042: Adding allocation from registry ${moduleUid} -> ${port}`);
6298
+ portAssignments.set(moduleUid, port);
6299
+ changed = true;
6300
+ }
6301
+ }
6302
+ if (changed) {
6303
+ saveToDisk();
5902
6304
  }
5903
6305
  }
5904
6306
 
@@ -6292,10 +6694,10 @@ function getPreviewManager() {
6292
6694
  }
6293
6695
 
6294
6696
  // src/utils/dev-server.ts
6295
- var import_child_process10 = require("child_process");
6697
+ var import_child_process11 = require("child_process");
6296
6698
  var import_core9 = __toESM(require_dist());
6297
- var fs12 = __toESM(require("fs"));
6298
- var path13 = __toESM(require("path"));
6699
+ var fs14 = __toESM(require("fs"));
6700
+ var path15 = __toESM(require("path"));
6299
6701
  var MAX_RESTART_ATTEMPTS = 5;
6300
6702
  var INITIAL_RESTART_DELAY_MS = 2e3;
6301
6703
  var MAX_RESTART_DELAY_MS = 3e4;
@@ -6303,26 +6705,26 @@ var MAX_LOG_SIZE_BYTES = 5 * 1024 * 1024;
6303
6705
  var NODE_MEMORY_LIMIT_MB = 2048;
6304
6706
  var activeServers = /* @__PURE__ */ new Map();
6305
6707
  function getLogsDir() {
6306
- const logsDir = path13.join((0, import_core9.getConfigDir)(), "logs");
6307
- if (!fs12.existsSync(logsDir)) {
6308
- fs12.mkdirSync(logsDir, { recursive: true });
6708
+ const logsDir = path15.join((0, import_core9.getConfigDir)(), "logs");
6709
+ if (!fs14.existsSync(logsDir)) {
6710
+ fs14.mkdirSync(logsDir, { recursive: true });
6309
6711
  }
6310
6712
  return logsDir;
6311
6713
  }
6312
6714
  function getLogFilePath(moduleUid) {
6313
- return path13.join(getLogsDir(), `dev-${moduleUid}.log`);
6715
+ return path15.join(getLogsDir(), `dev-${moduleUid}.log`);
6314
6716
  }
6315
6717
  function rotateLogIfNeeded(logPath) {
6316
6718
  try {
6317
- if (fs12.existsSync(logPath)) {
6318
- const stats = fs12.statSync(logPath);
6719
+ if (fs14.existsSync(logPath)) {
6720
+ const stats = fs14.statSync(logPath);
6319
6721
  if (stats.size > MAX_LOG_SIZE_BYTES) {
6320
6722
  const backupPath = `${logPath}.1`;
6321
- if (fs12.existsSync(backupPath)) {
6322
- fs12.unlinkSync(backupPath);
6723
+ if (fs14.existsSync(backupPath)) {
6724
+ fs14.unlinkSync(backupPath);
6323
6725
  }
6324
- fs12.renameSync(logPath, backupPath);
6325
- console.log(`[DevServer] EP932: Rotated log file for ${path13.basename(logPath)}`);
6726
+ fs14.renameSync(logPath, backupPath);
6727
+ console.log(`[DevServer] EP932: Rotated log file for ${path15.basename(logPath)}`);
6326
6728
  }
6327
6729
  }
6328
6730
  } catch (error) {
@@ -6335,13 +6737,13 @@ function writeToLog(logPath, line, isError = false) {
6335
6737
  const prefix = isError ? "ERR" : "OUT";
6336
6738
  const logLine = `[${timestamp}] [${prefix}] ${line}
6337
6739
  `;
6338
- fs12.appendFileSync(logPath, logLine);
6740
+ fs14.appendFileSync(logPath, logLine);
6339
6741
  } catch {
6340
6742
  }
6341
6743
  }
6342
6744
  async function killProcessOnPort(port) {
6343
6745
  try {
6344
- const result = (0, import_child_process10.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
6746
+ const result = (0, import_child_process11.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
6345
6747
  if (!result) {
6346
6748
  console.log(`[DevServer] EP929: No process found on port ${port}`);
6347
6749
  return true;
@@ -6350,7 +6752,7 @@ async function killProcessOnPort(port) {
6350
6752
  console.log(`[DevServer] EP929: Found ${pids.length} process(es) on port ${port}: ${pids.join(", ")}`);
6351
6753
  for (const pid of pids) {
6352
6754
  try {
6353
- (0, import_child_process10.execSync)(`kill -15 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
6755
+ (0, import_child_process11.execSync)(`kill -15 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
6354
6756
  console.log(`[DevServer] EP929: Sent SIGTERM to PID ${pid}`);
6355
6757
  } catch {
6356
6758
  }
@@ -6358,8 +6760,8 @@ async function killProcessOnPort(port) {
6358
6760
  await new Promise((resolve3) => setTimeout(resolve3, 1e3));
6359
6761
  for (const pid of pids) {
6360
6762
  try {
6361
- (0, import_child_process10.execSync)(`kill -0 ${pid} 2>/dev/null`, { encoding: "utf8" });
6362
- (0, import_child_process10.execSync)(`kill -9 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
6763
+ (0, import_child_process11.execSync)(`kill -0 ${pid} 2>/dev/null`, { encoding: "utf8" });
6764
+ (0, import_child_process11.execSync)(`kill -9 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
6363
6765
  console.log(`[DevServer] EP929: Force killed PID ${pid}`);
6364
6766
  } catch {
6365
6767
  }
@@ -6410,7 +6812,7 @@ function spawnDevServerProcess(projectPath, port, moduleUid, logPath, customComm
6410
6812
  if (injectedCount > 0) {
6411
6813
  console.log(`[DevServer] EP998: Injecting ${injectedCount} env vars from database`);
6412
6814
  }
6413
- const devProcess = (0, import_child_process10.spawn)(cmd, args, {
6815
+ const devProcess = (0, import_child_process11.spawn)(cmd, args, {
6414
6816
  cwd: projectPath,
6415
6817
  env: mergedEnv,
6416
6818
  stdio: ["ignore", "pipe", "pipe"],
@@ -6514,8 +6916,8 @@ async function startDevServer(projectPath, port = 3e3, moduleUid = "default", op
6514
6916
  });
6515
6917
  injectedEnvVars = result.envVars;
6516
6918
  console.log(`[DevServer] EP998: Loaded ${Object.keys(injectedEnvVars).length} env vars (from ${result.fromCache ? "cache" : "server"})`);
6517
- const envFilePath = path13.join(projectPath, ".env");
6518
- if (!fs12.existsSync(envFilePath) && Object.keys(injectedEnvVars).length > 0) {
6919
+ const envFilePath = path15.join(projectPath, ".env");
6920
+ if (!fs14.existsSync(envFilePath) && Object.keys(injectedEnvVars).length > 0) {
6519
6921
  console.log(`[DevServer] EP1004: .env file missing, writing ${Object.keys(injectedEnvVars).length} vars to ${envFilePath}`);
6520
6922
  writeEnvFile(projectPath, injectedEnvVars);
6521
6923
  }
@@ -6621,8 +7023,8 @@ function getDevServerStatus() {
6621
7023
  }
6622
7024
 
6623
7025
  // src/daemon/worktree-manager.ts
6624
- var fs13 = __toESM(require("fs"));
6625
- var path14 = __toESM(require("path"));
7026
+ var fs15 = __toESM(require("fs"));
7027
+ var path16 = __toESM(require("path"));
6626
7028
  var import_core10 = __toESM(require_dist());
6627
7029
  function validateModuleUid(moduleUid) {
6628
7030
  if (!moduleUid || typeof moduleUid !== "string" || !moduleUid.trim()) {
@@ -6646,8 +7048,8 @@ var WorktreeManager = class _WorktreeManager {
6646
7048
  // ============================================================
6647
7049
  this.lockPath = "";
6648
7050
  this.projectRoot = projectRoot;
6649
- this.bareRepoPath = path14.join(projectRoot, ".bare");
6650
- this.configPath = path14.join(projectRoot, ".episoda", "config.json");
7051
+ this.bareRepoPath = path16.join(projectRoot, ".bare");
7052
+ this.configPath = path16.join(projectRoot, ".episoda", "config.json");
6651
7053
  this.gitExecutor = new import_core10.GitExecutor();
6652
7054
  }
6653
7055
  /**
@@ -6656,10 +7058,10 @@ var WorktreeManager = class _WorktreeManager {
6656
7058
  * @returns true if valid project, false otherwise
6657
7059
  */
6658
7060
  async initialize() {
6659
- if (!fs13.existsSync(this.bareRepoPath)) {
7061
+ if (!fs15.existsSync(this.bareRepoPath)) {
6660
7062
  return false;
6661
7063
  }
6662
- if (!fs13.existsSync(this.configPath)) {
7064
+ if (!fs15.existsSync(this.configPath)) {
6663
7065
  return false;
6664
7066
  }
6665
7067
  try {
@@ -6679,10 +7081,10 @@ var WorktreeManager = class _WorktreeManager {
6679
7081
  */
6680
7082
  async ensureFetchRefspecConfigured() {
6681
7083
  try {
6682
- const { execSync: execSync8 } = require("child_process");
7084
+ const { execSync: execSync9 } = require("child_process");
6683
7085
  let fetchRefspec = null;
6684
7086
  try {
6685
- fetchRefspec = execSync8("git config --get remote.origin.fetch", {
7087
+ fetchRefspec = execSync9("git config --get remote.origin.fetch", {
6686
7088
  cwd: this.bareRepoPath,
6687
7089
  encoding: "utf-8",
6688
7090
  timeout: 5e3
@@ -6691,7 +7093,7 @@ var WorktreeManager = class _WorktreeManager {
6691
7093
  }
6692
7094
  if (!fetchRefspec) {
6693
7095
  console.log("[WorktreeManager] EP1014: Configuring missing fetch refspec for bare repo");
6694
- execSync8('git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"', {
7096
+ execSync9('git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"', {
6695
7097
  cwd: this.bareRepoPath,
6696
7098
  timeout: 5e3
6697
7099
  });
@@ -6706,8 +7108,8 @@ var WorktreeManager = class _WorktreeManager {
6706
7108
  */
6707
7109
  static async createProject(projectRoot, repoUrl, projectId, workspaceSlug, projectSlug) {
6708
7110
  const manager = new _WorktreeManager(projectRoot);
6709
- const episodaDir = path14.join(projectRoot, ".episoda");
6710
- fs13.mkdirSync(episodaDir, { recursive: true });
7111
+ const episodaDir = path16.join(projectRoot, ".episoda");
7112
+ fs15.mkdirSync(episodaDir, { recursive: true });
6711
7113
  const cloneResult = await manager.gitExecutor.execute({
6712
7114
  action: "clone_bare",
6713
7115
  url: repoUrl,
@@ -6738,7 +7140,7 @@ var WorktreeManager = class _WorktreeManager {
6738
7140
  error: `Invalid module UID: "${moduleUid}" - contains disallowed characters`
6739
7141
  };
6740
7142
  }
6741
- const worktreePath = path14.join(this.projectRoot, moduleUid);
7143
+ const worktreePath = path16.join(this.projectRoot, moduleUid);
6742
7144
  const lockAcquired = await this.acquireLock();
6743
7145
  if (!lockAcquired) {
6744
7146
  return {
@@ -6838,14 +7240,21 @@ var WorktreeManager = class _WorktreeManager {
6838
7240
  error: result.output || "Failed to remove worktree"
6839
7241
  };
6840
7242
  }
6841
- const config = this.readConfig();
6842
- if (config) {
6843
- config.worktrees = config.worktrees.filter((w) => w.moduleUid !== moduleUid);
6844
- this.writeConfig(config);
7243
+ let configUpdated = false;
7244
+ try {
7245
+ const config = this.readConfig();
7246
+ if (config) {
7247
+ config.worktrees = config.worktrees.filter((w) => w.moduleUid !== moduleUid);
7248
+ this.writeConfig(config);
7249
+ configUpdated = true;
7250
+ }
7251
+ } catch (configError) {
7252
+ console.warn(`[WorktreeManager] EP1035: Config update failed after removing ${moduleUid} (non-blocking):`, configError.message);
6845
7253
  }
6846
7254
  return {
6847
7255
  success: true,
6848
- worktreePath: existing.worktreePath
7256
+ worktreePath: existing.worktreePath,
7257
+ configUpdated
6849
7258
  };
6850
7259
  } finally {
6851
7260
  this.releaseLock();
@@ -6920,7 +7329,7 @@ var WorktreeManager = class _WorktreeManager {
6920
7329
  let prunedCount = 0;
6921
7330
  await this.updateConfigSafe((config) => {
6922
7331
  const initialCount = config.worktrees.length;
6923
- config.worktrees = config.worktrees.filter((w) => fs13.existsSync(w.worktreePath));
7332
+ config.worktrees = config.worktrees.filter((w) => fs15.existsSync(w.worktreePath));
6924
7333
  prunedCount = initialCount - config.worktrees.length;
6925
7334
  return config;
6926
7335
  });
@@ -7001,16 +7410,16 @@ var WorktreeManager = class _WorktreeManager {
7001
7410
  const retryInterval = 50;
7002
7411
  while (Date.now() - startTime < timeoutMs) {
7003
7412
  try {
7004
- fs13.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
7413
+ fs15.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
7005
7414
  return true;
7006
7415
  } catch (err) {
7007
7416
  if (err.code === "EEXIST") {
7008
7417
  try {
7009
- const stats = fs13.statSync(lockPath);
7418
+ const stats = fs15.statSync(lockPath);
7010
7419
  const lockAge = Date.now() - stats.mtimeMs;
7011
7420
  if (lockAge > 3e4) {
7012
7421
  try {
7013
- const lockContent = fs13.readFileSync(lockPath, "utf-8").trim();
7422
+ const lockContent = fs15.readFileSync(lockPath, "utf-8").trim();
7014
7423
  const lockPid = parseInt(lockContent, 10);
7015
7424
  if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
7016
7425
  await new Promise((resolve3) => setTimeout(resolve3, retryInterval));
@@ -7019,7 +7428,7 @@ var WorktreeManager = class _WorktreeManager {
7019
7428
  } catch {
7020
7429
  }
7021
7430
  try {
7022
- fs13.unlinkSync(lockPath);
7431
+ fs15.unlinkSync(lockPath);
7023
7432
  } catch {
7024
7433
  }
7025
7434
  continue;
@@ -7040,29 +7449,34 @@ var WorktreeManager = class _WorktreeManager {
7040
7449
  */
7041
7450
  releaseLock() {
7042
7451
  try {
7043
- fs13.unlinkSync(this.getLockPath());
7452
+ fs15.unlinkSync(this.getLockPath());
7044
7453
  } catch {
7045
7454
  }
7046
7455
  }
7047
7456
  readConfig() {
7048
7457
  try {
7049
- if (!fs13.existsSync(this.configPath)) {
7458
+ if (!fs15.existsSync(this.configPath)) {
7050
7459
  return null;
7051
7460
  }
7052
- const content = fs13.readFileSync(this.configPath, "utf-8");
7461
+ const content = fs15.readFileSync(this.configPath, "utf-8");
7053
7462
  return JSON.parse(content);
7054
7463
  } catch (error) {
7055
- console.error("[WorktreeManager] Failed to read config:", error);
7464
+ if (error instanceof SyntaxError) {
7465
+ console.warn(`[WorktreeManager] EP1035: Config file corrupted at ${this.configPath} - JSON parse error:`, error.message);
7466
+ console.warn("[WorktreeManager] EP1035: Config will be recovered on next pruneStaleWorktrees call");
7467
+ } else {
7468
+ console.error("[WorktreeManager] Failed to read config:", error.message);
7469
+ }
7056
7470
  return null;
7057
7471
  }
7058
7472
  }
7059
7473
  writeConfig(config) {
7060
7474
  try {
7061
- const dir = path14.dirname(this.configPath);
7062
- if (!fs13.existsSync(dir)) {
7063
- fs13.mkdirSync(dir, { recursive: true });
7475
+ const dir = path16.dirname(this.configPath);
7476
+ if (!fs15.existsSync(dir)) {
7477
+ fs15.mkdirSync(dir, { recursive: true });
7064
7478
  }
7065
- fs13.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
7479
+ fs15.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
7066
7480
  } catch (error) {
7067
7481
  console.error("[WorktreeManager] Failed to write config:", error);
7068
7482
  throw error;
@@ -7143,14 +7557,14 @@ var WorktreeManager = class _WorktreeManager {
7143
7557
  }
7144
7558
  try {
7145
7559
  for (const file of files) {
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);
7560
+ const srcPath = path16.join(mainWorktree.worktreePath, file);
7561
+ const destPath = path16.join(worktree.worktreePath, file);
7562
+ if (fs15.existsSync(srcPath)) {
7563
+ const destDir = path16.dirname(destPath);
7564
+ if (!fs15.existsSync(destDir)) {
7565
+ fs15.mkdirSync(destDir, { recursive: true });
7566
+ }
7567
+ fs15.copyFileSync(srcPath, destPath);
7154
7568
  console.log(`[WorktreeManager] EP964: Copied ${file} to ${moduleUid} (deprecated)`);
7155
7569
  } else {
7156
7570
  console.log(`[WorktreeManager] EP964: Skipped ${file} (not found in main)`);
@@ -7181,8 +7595,8 @@ var WorktreeManager = class _WorktreeManager {
7181
7595
  console.log(`[WorktreeManager] EP959: Timeout: ${TIMEOUT_MINUTES} minutes`);
7182
7596
  console.log(`[WorktreeManager] EP959: Script: ${scriptPreview}`);
7183
7597
  try {
7184
- const { execSync: execSync8 } = require("child_process");
7185
- execSync8(script, {
7598
+ const { execSync: execSync9 } = require("child_process");
7599
+ execSync9(script, {
7186
7600
  cwd: worktree.worktreePath,
7187
7601
  stdio: "inherit",
7188
7602
  timeout: TIMEOUT_MINUTES * 60 * 1e3,
@@ -7216,8 +7630,8 @@ var WorktreeManager = class _WorktreeManager {
7216
7630
  console.log(`[WorktreeManager] EP959: Timeout: ${TIMEOUT_MINUTES} minutes`);
7217
7631
  console.log(`[WorktreeManager] EP959: Script: ${scriptPreview}`);
7218
7632
  try {
7219
- const { execSync: execSync8 } = require("child_process");
7220
- execSync8(script, {
7633
+ const { execSync: execSync9 } = require("child_process");
7634
+ execSync9(script, {
7221
7635
  cwd: worktree.worktreePath,
7222
7636
  stdio: "inherit",
7223
7637
  timeout: TIMEOUT_MINUTES * 60 * 1e3,
@@ -7233,27 +7647,27 @@ var WorktreeManager = class _WorktreeManager {
7233
7647
  }
7234
7648
  };
7235
7649
  function getEpisodaRoot() {
7236
- return process.env.EPISODA_ROOT || path14.join(require("os").homedir(), "episoda");
7650
+ return process.env.EPISODA_ROOT || path16.join(require("os").homedir(), "episoda");
7237
7651
  }
7238
7652
  async function isWorktreeProject(projectRoot) {
7239
7653
  const manager = new WorktreeManager(projectRoot);
7240
7654
  return manager.initialize();
7241
7655
  }
7242
7656
  async function findProjectRoot(startPath) {
7243
- let current = path14.resolve(startPath);
7657
+ let current = path16.resolve(startPath);
7244
7658
  const episodaRoot = getEpisodaRoot();
7245
7659
  if (!current.startsWith(episodaRoot)) {
7246
7660
  return null;
7247
7661
  }
7248
7662
  for (let i = 0; i < 10; i++) {
7249
- const bareDir = path14.join(current, ".bare");
7250
- const episodaDir = path14.join(current, ".episoda");
7251
- if (fs13.existsSync(bareDir) && fs13.existsSync(episodaDir)) {
7663
+ const bareDir = path16.join(current, ".bare");
7664
+ const episodaDir = path16.join(current, ".episoda");
7665
+ if (fs15.existsSync(bareDir) && fs15.existsSync(episodaDir)) {
7252
7666
  if (await isWorktreeProject(current)) {
7253
7667
  return current;
7254
7668
  }
7255
7669
  }
7256
- const parent = path14.dirname(current);
7670
+ const parent = path16.dirname(current);
7257
7671
  if (parent === current) {
7258
7672
  break;
7259
7673
  }
@@ -7263,19 +7677,19 @@ async function findProjectRoot(startPath) {
7263
7677
  }
7264
7678
 
7265
7679
  // src/utils/worktree.ts
7266
- var path15 = __toESM(require("path"));
7267
- var fs14 = __toESM(require("fs"));
7268
- var os5 = __toESM(require("os"));
7680
+ var path17 = __toESM(require("path"));
7681
+ var fs16 = __toESM(require("fs"));
7682
+ var os7 = __toESM(require("os"));
7269
7683
  var import_core11 = __toESM(require_dist());
7270
7684
  function getEpisodaRoot2() {
7271
- return process.env.EPISODA_ROOT || path15.join(os5.homedir(), "episoda");
7685
+ return process.env.EPISODA_ROOT || path17.join(os7.homedir(), "episoda");
7272
7686
  }
7273
7687
  function getWorktreeInfo(moduleUid, workspaceSlug, projectSlug) {
7274
7688
  const root = getEpisodaRoot2();
7275
- const worktreePath = path15.join(root, workspaceSlug, projectSlug, moduleUid);
7689
+ const worktreePath = path17.join(root, workspaceSlug, projectSlug, moduleUid);
7276
7690
  return {
7277
7691
  path: worktreePath,
7278
- exists: fs14.existsSync(worktreePath),
7692
+ exists: fs16.existsSync(worktreePath),
7279
7693
  moduleUid
7280
7694
  };
7281
7695
  }
@@ -7289,61 +7703,61 @@ async function getWorktreeInfoForModule(moduleUid) {
7289
7703
  }
7290
7704
 
7291
7705
  // src/framework-detector.ts
7292
- var fs15 = __toESM(require("fs"));
7293
- var path16 = __toESM(require("path"));
7706
+ var fs17 = __toESM(require("fs"));
7707
+ var path18 = __toESM(require("path"));
7294
7708
  function getInstallCommand(cwd) {
7295
- if (fs15.existsSync(path16.join(cwd, "bun.lockb"))) {
7709
+ if (fs17.existsSync(path18.join(cwd, "bun.lockb"))) {
7296
7710
  return {
7297
7711
  command: ["bun", "install"],
7298
7712
  description: "Installing dependencies with bun",
7299
7713
  detectedFrom: "bun.lockb"
7300
7714
  };
7301
7715
  }
7302
- if (fs15.existsSync(path16.join(cwd, "pnpm-lock.yaml"))) {
7716
+ if (fs17.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) {
7303
7717
  return {
7304
7718
  command: ["pnpm", "install"],
7305
7719
  description: "Installing dependencies with pnpm",
7306
7720
  detectedFrom: "pnpm-lock.yaml"
7307
7721
  };
7308
7722
  }
7309
- if (fs15.existsSync(path16.join(cwd, "yarn.lock"))) {
7723
+ if (fs17.existsSync(path18.join(cwd, "yarn.lock"))) {
7310
7724
  return {
7311
7725
  command: ["yarn", "install"],
7312
7726
  description: "Installing dependencies with yarn",
7313
7727
  detectedFrom: "yarn.lock"
7314
7728
  };
7315
7729
  }
7316
- if (fs15.existsSync(path16.join(cwd, "package-lock.json"))) {
7730
+ if (fs17.existsSync(path18.join(cwd, "package-lock.json"))) {
7317
7731
  return {
7318
7732
  command: ["npm", "ci"],
7319
7733
  description: "Installing dependencies with npm ci",
7320
7734
  detectedFrom: "package-lock.json"
7321
7735
  };
7322
7736
  }
7323
- if (fs15.existsSync(path16.join(cwd, "package.json"))) {
7737
+ if (fs17.existsSync(path18.join(cwd, "package.json"))) {
7324
7738
  return {
7325
7739
  command: ["npm", "install"],
7326
7740
  description: "Installing dependencies with npm",
7327
7741
  detectedFrom: "package.json"
7328
7742
  };
7329
7743
  }
7330
- if (fs15.existsSync(path16.join(cwd, "Pipfile.lock")) || fs15.existsSync(path16.join(cwd, "Pipfile"))) {
7744
+ if (fs17.existsSync(path18.join(cwd, "Pipfile.lock")) || fs17.existsSync(path18.join(cwd, "Pipfile"))) {
7331
7745
  return {
7332
7746
  command: ["pipenv", "install"],
7333
7747
  description: "Installing dependencies with pipenv",
7334
- detectedFrom: fs15.existsSync(path16.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
7748
+ detectedFrom: fs17.existsSync(path18.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
7335
7749
  };
7336
7750
  }
7337
- if (fs15.existsSync(path16.join(cwd, "poetry.lock"))) {
7751
+ if (fs17.existsSync(path18.join(cwd, "poetry.lock"))) {
7338
7752
  return {
7339
7753
  command: ["poetry", "install"],
7340
7754
  description: "Installing dependencies with poetry",
7341
7755
  detectedFrom: "poetry.lock"
7342
7756
  };
7343
7757
  }
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");
7758
+ if (fs17.existsSync(path18.join(cwd, "pyproject.toml"))) {
7759
+ const pyprojectPath = path18.join(cwd, "pyproject.toml");
7760
+ const content = fs17.readFileSync(pyprojectPath, "utf-8");
7347
7761
  if (content.includes("[tool.poetry]")) {
7348
7762
  return {
7349
7763
  command: ["poetry", "install"],
@@ -7352,41 +7766,41 @@ function getInstallCommand(cwd) {
7352
7766
  };
7353
7767
  }
7354
7768
  }
7355
- if (fs15.existsSync(path16.join(cwd, "requirements.txt"))) {
7769
+ if (fs17.existsSync(path18.join(cwd, "requirements.txt"))) {
7356
7770
  return {
7357
7771
  command: ["pip", "install", "-r", "requirements.txt"],
7358
7772
  description: "Installing dependencies with pip",
7359
7773
  detectedFrom: "requirements.txt"
7360
7774
  };
7361
7775
  }
7362
- if (fs15.existsSync(path16.join(cwd, "Gemfile.lock")) || fs15.existsSync(path16.join(cwd, "Gemfile"))) {
7776
+ if (fs17.existsSync(path18.join(cwd, "Gemfile.lock")) || fs17.existsSync(path18.join(cwd, "Gemfile"))) {
7363
7777
  return {
7364
7778
  command: ["bundle", "install"],
7365
7779
  description: "Installing dependencies with bundler",
7366
- detectedFrom: fs15.existsSync(path16.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
7780
+ detectedFrom: fs17.existsSync(path18.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
7367
7781
  };
7368
7782
  }
7369
- if (fs15.existsSync(path16.join(cwd, "go.sum")) || fs15.existsSync(path16.join(cwd, "go.mod"))) {
7783
+ if (fs17.existsSync(path18.join(cwd, "go.sum")) || fs17.existsSync(path18.join(cwd, "go.mod"))) {
7370
7784
  return {
7371
7785
  command: ["go", "mod", "download"],
7372
7786
  description: "Downloading Go modules",
7373
- detectedFrom: fs15.existsSync(path16.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
7787
+ detectedFrom: fs17.existsSync(path18.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
7374
7788
  };
7375
7789
  }
7376
- if (fs15.existsSync(path16.join(cwd, "Cargo.lock")) || fs15.existsSync(path16.join(cwd, "Cargo.toml"))) {
7790
+ if (fs17.existsSync(path18.join(cwd, "Cargo.lock")) || fs17.existsSync(path18.join(cwd, "Cargo.toml"))) {
7377
7791
  return {
7378
7792
  command: ["cargo", "build"],
7379
7793
  description: "Building Rust project (downloads dependencies)",
7380
- detectedFrom: fs15.existsSync(path16.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
7794
+ detectedFrom: fs17.existsSync(path18.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
7381
7795
  };
7382
7796
  }
7383
7797
  return null;
7384
7798
  }
7385
7799
 
7386
7800
  // src/daemon/daemon-process.ts
7387
- var fs16 = __toESM(require("fs"));
7388
- var os6 = __toESM(require("os"));
7389
- var path17 = __toESM(require("path"));
7801
+ var fs18 = __toESM(require("fs"));
7802
+ var os8 = __toESM(require("os"));
7803
+ var path19 = __toESM(require("path"));
7390
7804
  var packageJson = require_package();
7391
7805
  async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
7392
7806
  const now = Date.now();
@@ -7550,6 +7964,7 @@ var Daemon = class _Daemon {
7550
7964
  this.registerIPCHandlers();
7551
7965
  await this.restoreConnections();
7552
7966
  await this.cleanupOrphanedTunnels();
7967
+ await this.cleanupOrphanedDevServers();
7553
7968
  await this.auditWorktreesOnStartup();
7554
7969
  this.startHealthCheckPolling();
7555
7970
  this.setupShutdownHandlers();
@@ -7596,9 +8011,9 @@ var Daemon = class _Daemon {
7596
8011
  machineId: this.machineId,
7597
8012
  deviceId: this.deviceId,
7598
8013
  // EP726: UUID for unified device identification
7599
- hostname: os6.hostname(),
7600
- platform: os6.platform(),
7601
- arch: os6.arch(),
8014
+ hostname: os8.hostname(),
8015
+ platform: os8.platform(),
8016
+ arch: os8.arch(),
7602
8017
  projects
7603
8018
  };
7604
8019
  });
@@ -7888,7 +8303,7 @@ var Daemon = class _Daemon {
7888
8303
  client.updateActivity();
7889
8304
  try {
7890
8305
  const gitCmd = message.command;
7891
- const bareRepoPath = path17.join(projectPath, ".bare");
8306
+ const bareRepoPath = path19.join(projectPath, ".bare");
7892
8307
  const cwd = gitCmd.worktreePath || bareRepoPath;
7893
8308
  if (gitCmd.worktreePath) {
7894
8309
  console.log(`[Daemon] Routing command to worktree: ${gitCmd.worktreePath}`);
@@ -8217,6 +8632,78 @@ var Daemon = class _Daemon {
8217
8632
  }
8218
8633
  }
8219
8634
  });
8635
+ client.on("worktree_cleanup_command", async (message) => {
8636
+ if (message.type === "worktree_cleanup_command" && message.command) {
8637
+ const cmd = message.command;
8638
+ console.log(`[Daemon] EP1035: Received worktree cleanup command for ${cmd.moduleUid}`);
8639
+ client.updateActivity();
8640
+ let result;
8641
+ try {
8642
+ const projectRootPath = await findProjectRoot(cmd.worktreePath);
8643
+ if (!projectRootPath) {
8644
+ console.warn(`[Daemon] EP1035: Cannot find project root for ${cmd.worktreePath}`);
8645
+ result = {
8646
+ success: false,
8647
+ error: `Cannot find project root for worktree path: ${cmd.worktreePath}`
8648
+ };
8649
+ } else {
8650
+ const manager = new WorktreeManager(projectRootPath);
8651
+ const initialized2 = await manager.initialize();
8652
+ if (!initialized2) {
8653
+ console.warn(`[Daemon] EP1035: Failed to initialize WorktreeManager for ${projectRootPath}`);
8654
+ result = {
8655
+ success: false,
8656
+ error: `Failed to initialize WorktreeManager for project: ${projectRootPath}`
8657
+ };
8658
+ } else {
8659
+ const removeResult = await manager.removeWorktree(cmd.moduleUid, cmd.force || false);
8660
+ if (removeResult.success) {
8661
+ if (removeResult.configUpdated === false) {
8662
+ console.warn(`[Daemon] EP1035: Worktree ${cmd.moduleUid} removed but config update failed (will sync on next prune)`);
8663
+ } else {
8664
+ console.log(`[Daemon] EP1035: Successfully cleaned up worktree for ${cmd.moduleUid}`);
8665
+ }
8666
+ result = {
8667
+ success: true,
8668
+ worktreePath: removeResult.worktreePath
8669
+ };
8670
+ } else {
8671
+ if (removeResult.error?.includes("not found") || removeResult.error?.includes("No worktree found")) {
8672
+ console.log(`[Daemon] EP1035: Worktree for ${cmd.moduleUid} already removed, syncing config`);
8673
+ await manager.pruneStaleWorktrees();
8674
+ result = {
8675
+ success: true,
8676
+ worktreePath: cmd.worktreePath
8677
+ };
8678
+ } else {
8679
+ console.error(`[Daemon] EP1035: Failed to remove worktree for ${cmd.moduleUid}: ${removeResult.error}`);
8680
+ result = {
8681
+ success: false,
8682
+ error: removeResult.error
8683
+ };
8684
+ }
8685
+ }
8686
+ }
8687
+ }
8688
+ } catch (error) {
8689
+ console.error(`[Daemon] EP1035: Worktree cleanup error for ${cmd.moduleUid}:`, error);
8690
+ result = {
8691
+ success: false,
8692
+ error: error instanceof Error ? error.message : String(error)
8693
+ };
8694
+ }
8695
+ try {
8696
+ await client.send({
8697
+ type: "worktree_cleanup_result",
8698
+ commandId: message.id,
8699
+ result
8700
+ });
8701
+ } catch (sendError) {
8702
+ console.error(`[Daemon] EP1035: Failed to send cleanup result (WebSocket may be disconnected):`, sendError);
8703
+ }
8704
+ console.log(`[Daemon] EP1035: Worktree cleanup for ${cmd.moduleUid} completed:`, result.success ? "success" : "failed");
8705
+ }
8706
+ });
8220
8707
  client.on("shutdown", async (message) => {
8221
8708
  const shutdownMessage = message;
8222
8709
  const reason = shutdownMessage.reason || "unknown";
@@ -8304,8 +8791,8 @@ var Daemon = class _Daemon {
8304
8791
  let daemonPid;
8305
8792
  try {
8306
8793
  const pidPath = getPidFilePath();
8307
- if (fs16.existsSync(pidPath)) {
8308
- const pidStr = fs16.readFileSync(pidPath, "utf-8").trim();
8794
+ if (fs18.existsSync(pidPath)) {
8795
+ const pidStr = fs18.readFileSync(pidPath, "utf-8").trim();
8309
8796
  daemonPid = parseInt(pidStr, 10);
8310
8797
  }
8311
8798
  } catch (pidError) {
@@ -8329,9 +8816,9 @@ var Daemon = class _Daemon {
8329
8816
  client.once("auth_error", errorHandler);
8330
8817
  });
8331
8818
  await client.connect(wsUrl, config.access_token, this.machineId, {
8332
- hostname: os6.hostname(),
8333
- osPlatform: os6.platform(),
8334
- osArch: os6.arch(),
8819
+ hostname: os8.hostname(),
8820
+ osPlatform: os8.platform(),
8821
+ osArch: os8.arch(),
8335
8822
  daemonPid
8336
8823
  });
8337
8824
  console.log(`[Daemon] Successfully connected to project ${projectId}`);
@@ -8384,29 +8871,29 @@ var Daemon = class _Daemon {
8384
8871
  */
8385
8872
  async configureGitUser(projectPath, userId, workspaceId, machineId, projectId, deviceId) {
8386
8873
  try {
8387
- const { execSync: execSync8 } = await import("child_process");
8388
- execSync8(`git config episoda.userId ${userId}`, {
8874
+ const { execSync: execSync9 } = await import("child_process");
8875
+ execSync9(`git config episoda.userId ${userId}`, {
8389
8876
  cwd: projectPath,
8390
8877
  encoding: "utf8",
8391
8878
  stdio: "pipe"
8392
8879
  });
8393
- execSync8(`git config episoda.workspaceId ${workspaceId}`, {
8880
+ execSync9(`git config episoda.workspaceId ${workspaceId}`, {
8394
8881
  cwd: projectPath,
8395
8882
  encoding: "utf8",
8396
8883
  stdio: "pipe"
8397
8884
  });
8398
- execSync8(`git config episoda.machineId ${machineId}`, {
8885
+ execSync9(`git config episoda.machineId ${machineId}`, {
8399
8886
  cwd: projectPath,
8400
8887
  encoding: "utf8",
8401
8888
  stdio: "pipe"
8402
8889
  });
8403
- execSync8(`git config episoda.projectId ${projectId}`, {
8890
+ execSync9(`git config episoda.projectId ${projectId}`, {
8404
8891
  cwd: projectPath,
8405
8892
  encoding: "utf8",
8406
8893
  stdio: "pipe"
8407
8894
  });
8408
8895
  if (deviceId) {
8409
- execSync8(`git config episoda.deviceId ${deviceId}`, {
8896
+ execSync9(`git config episoda.deviceId ${deviceId}`, {
8410
8897
  cwd: projectPath,
8411
8898
  encoding: "utf8",
8412
8899
  stdio: "pipe"
@@ -8426,27 +8913,27 @@ var Daemon = class _Daemon {
8426
8913
  */
8427
8914
  async installGitHooks(projectPath) {
8428
8915
  const hooks = ["post-checkout", "pre-commit", "post-commit"];
8429
- const hooksDir = path17.join(projectPath, ".git", "hooks");
8430
- if (!fs16.existsSync(hooksDir)) {
8916
+ const hooksDir = path19.join(projectPath, ".git", "hooks");
8917
+ if (!fs18.existsSync(hooksDir)) {
8431
8918
  console.warn(`[Daemon] Hooks directory not found: ${hooksDir}`);
8432
8919
  return;
8433
8920
  }
8434
8921
  for (const hookName of hooks) {
8435
8922
  try {
8436
- const hookPath = path17.join(hooksDir, hookName);
8437
- const bundledHookPath = path17.join(__dirname, "..", "hooks", hookName);
8438
- if (!fs16.existsSync(bundledHookPath)) {
8923
+ const hookPath = path19.join(hooksDir, hookName);
8924
+ const bundledHookPath = path19.join(__dirname, "..", "hooks", hookName);
8925
+ if (!fs18.existsSync(bundledHookPath)) {
8439
8926
  console.warn(`[Daemon] Bundled hook not found: ${bundledHookPath}`);
8440
8927
  continue;
8441
8928
  }
8442
- const hookContent = fs16.readFileSync(bundledHookPath, "utf-8");
8443
- if (fs16.existsSync(hookPath)) {
8444
- const existingContent = fs16.readFileSync(hookPath, "utf-8");
8929
+ const hookContent = fs18.readFileSync(bundledHookPath, "utf-8");
8930
+ if (fs18.existsSync(hookPath)) {
8931
+ const existingContent = fs18.readFileSync(hookPath, "utf-8");
8445
8932
  if (existingContent === hookContent) {
8446
8933
  continue;
8447
8934
  }
8448
8935
  }
8449
- fs16.writeFileSync(hookPath, hookContent, { mode: 493 });
8936
+ fs18.writeFileSync(hookPath, hookContent, { mode: 493 });
8450
8937
  console.log(`[Daemon] Installed git hook: ${hookName}`);
8451
8938
  } catch (error) {
8452
8939
  console.warn(`[Daemon] Failed to install ${hookName} hook:`, error instanceof Error ? error.message : error);
@@ -8762,8 +9249,8 @@ var Daemon = class _Daemon {
8762
9249
  console.log(`[Daemon] EP1002: ${installCmd.description} (detected from ${installCmd.detectedFrom})`);
8763
9250
  console.log(`[Daemon] EP1002: Running: ${installCmd.command.join(" ")}`);
8764
9251
  try {
8765
- const { execSync: execSync8 } = await import("child_process");
8766
- execSync8(installCmd.command.join(" "), {
9252
+ const { execSync: execSync9 } = await import("child_process");
9253
+ execSync9(installCmd.command.join(" "), {
8767
9254
  cwd: worktreePath,
8768
9255
  stdio: "inherit",
8769
9256
  timeout: 10 * 60 * 1e3,
@@ -8816,8 +9303,8 @@ var Daemon = class _Daemon {
8816
9303
  console.log(`[Daemon] EP986: ${installCmd.description} (detected from ${installCmd.detectedFrom})`);
8817
9304
  console.log(`[Daemon] EP986: Running: ${installCmd.command.join(" ")}`);
8818
9305
  try {
8819
- const { execSync: execSync8 } = await import("child_process");
8820
- execSync8(installCmd.command.join(" "), {
9306
+ const { execSync: execSync9 } = await import("child_process");
9307
+ execSync9(installCmd.command.join(" "), {
8821
9308
  cwd: worktreePath,
8822
9309
  stdio: "inherit",
8823
9310
  timeout: 10 * 60 * 1e3,
@@ -8951,6 +9438,65 @@ var Daemon = class _Daemon {
8951
9438
  console.error("[Daemon] EP904: Failed to clean up orphaned tunnels:", error);
8952
9439
  }
8953
9440
  }
9441
+ /**
9442
+ * EP1042: Clean up orphaned dev servers from previous daemon runs
9443
+ *
9444
+ * When the daemon crashes or is killed, dev servers may continue running.
9445
+ * This method:
9446
+ * 1. Queries the DevServerRegistry for tracked dev servers
9447
+ * 2. Queries API for modules in doing/review state
9448
+ * 3. Kills dev servers for modules NOT in doing/review
9449
+ * 4. Reconciles port allocator with registry
9450
+ */
9451
+ async cleanupOrphanedDevServers() {
9452
+ try {
9453
+ const registry = getDevServerRegistry();
9454
+ const entries = registry.getAll();
9455
+ if (entries.length === 0) {
9456
+ console.log("[Daemon] EP1042: No registered dev servers to check");
9457
+ return;
9458
+ }
9459
+ console.log(`[Daemon] EP1042: Checking ${entries.length} registered dev server(s)...`);
9460
+ const activeModuleUids = [];
9461
+ const config = await (0, import_core12.loadConfig)();
9462
+ if (config?.access_token) {
9463
+ const apiUrl = config.api_url || "https://episoda.dev";
9464
+ try {
9465
+ const response = await fetch(`${apiUrl}/api/modules?state=doing,review&dev_mode=local`, {
9466
+ headers: {
9467
+ "Authorization": `Bearer ${config.access_token}`,
9468
+ "Content-Type": "application/json"
9469
+ }
9470
+ });
9471
+ if (response.ok) {
9472
+ const data = await response.json();
9473
+ if (data.modules && Array.isArray(data.modules)) {
9474
+ for (const module2 of data.modules) {
9475
+ if (module2.uid) {
9476
+ activeModuleUids.push(module2.uid);
9477
+ }
9478
+ }
9479
+ }
9480
+ }
9481
+ } catch (fetchError) {
9482
+ console.warn("[Daemon] EP1042: Failed to fetch active modules:", fetchError);
9483
+ }
9484
+ }
9485
+ console.log(`[Daemon] EP1042: Found ${activeModuleUids.length} active module(s) in doing/review state`);
9486
+ const cleanup = await registry.cleanupOrphans(activeModuleUids);
9487
+ if (cleanup.cleaned > 0) {
9488
+ console.log(`[Daemon] EP1042: Killed ${cleanup.cleaned} orphaned dev server(s)`);
9489
+ }
9490
+ const registryPorts = /* @__PURE__ */ new Map();
9491
+ for (const entry of registry.getAll()) {
9492
+ registryPorts.set(entry.moduleUid, entry.port);
9493
+ }
9494
+ reconcileWithRegistry(registryPorts);
9495
+ console.log("[Daemon] EP1042: Orphaned dev server cleanup complete");
9496
+ } catch (error) {
9497
+ console.error("[Daemon] EP1042: Failed to clean up orphaned dev servers:", error);
9498
+ }
9499
+ }
8954
9500
  /**
8955
9501
  * EP957: Audit worktrees on daemon startup to detect orphaned worktrees
8956
9502
  *
@@ -8997,14 +9543,36 @@ var Daemon = class _Daemon {
8997
9543
  }
8998
9544
  const { orphaned } = manager.auditWorktrees(activeModuleUids);
8999
9545
  if (orphaned.length > 0) {
9000
- console.log(`[Daemon] EP957: Found ${orphaned.length} orphaned worktree(s) in ${config.workspaceSlug}/${config.projectSlug}:`);
9546
+ console.log(`[Daemon] EP1035: Found ${orphaned.length} orphaned worktree(s) in ${config.workspaceSlug}/${config.projectSlug}:`);
9001
9547
  for (const w of orphaned) {
9002
9548
  console.log(` - ${w.moduleUid} (branch: ${w.branchName})`);
9003
9549
  }
9004
- console.log('[Daemon] EP957: Run "episoda release <module>" to clean up');
9550
+ console.log("[Daemon] EP1035: Auto-cleaning orphaned worktrees...");
9551
+ for (const w of orphaned) {
9552
+ try {
9553
+ const removeResult = await manager.removeWorktree(w.moduleUid, true);
9554
+ if (removeResult.success) {
9555
+ console.log(`[Daemon] EP1035: Cleaned up orphaned worktree ${w.moduleUid}`);
9556
+ } else {
9557
+ if (removeResult.error?.includes("not found") || removeResult.error?.includes("No worktree found")) {
9558
+ console.log(`[Daemon] EP1035: Worktree ${w.moduleUid} already removed, syncing config`);
9559
+ await manager.pruneStaleWorktrees();
9560
+ } else {
9561
+ console.warn(`[Daemon] EP1035: Failed to clean up ${w.moduleUid}: ${removeResult.error}`);
9562
+ }
9563
+ }
9564
+ } catch (cleanupError) {
9565
+ console.warn(`[Daemon] EP1035: Error cleaning up ${w.moduleUid}:`, cleanupError.message);
9566
+ }
9567
+ }
9568
+ try {
9569
+ await manager.pruneStaleWorktrees();
9570
+ } catch (pruneError) {
9571
+ console.warn("[Daemon] EP1035: Failed to prune stale worktrees:", pruneError.message);
9572
+ }
9005
9573
  }
9006
9574
  } catch (error) {
9007
- console.warn(`[Daemon] EP957: Failed to audit ${projectPath}:`, error);
9575
+ console.warn(`[Daemon] EP1035: Failed to audit ${projectPath}:`, error);
9008
9576
  }
9009
9577
  }
9010
9578
  /**
@@ -9072,9 +9640,20 @@ var Daemon = class _Daemon {
9072
9640
  }
9073
9641
  /**
9074
9642
  * EP833: Check if a tunnel is healthy
9643
+ * EP1042: Now also verifies dev server ownership (correct worktree)
9075
9644
  * Verifies both the tunnel URL and local dev server respond
9076
9645
  */
9077
9646
  async checkTunnelHealth(tunnel) {
9647
+ const registry = getDevServerRegistry();
9648
+ const entry = registry.getByPort(tunnel.port);
9649
+ if (entry && entry.moduleUid !== tunnel.moduleUid) {
9650
+ console.log(`[Daemon] EP1042: Port ${tunnel.port} owned by ${entry.moduleUid}, not ${tunnel.moduleUid} - marking unhealthy`);
9651
+ return false;
9652
+ }
9653
+ if (!entry) {
9654
+ console.log(`[Daemon] EP1042: No registry entry for port ${tunnel.port} (module ${tunnel.moduleUid}) - marking unhealthy`);
9655
+ return false;
9656
+ }
9078
9657
  try {
9079
9658
  const controller = new AbortController();
9080
9659
  const timeout = setTimeout(() => controller.abort(), _Daemon.HEALTH_CHECK_TIMEOUT_MS);
@@ -9309,8 +9888,8 @@ var Daemon = class _Daemon {
9309
9888
  await this.shutdown();
9310
9889
  try {
9311
9890
  const pidPath = getPidFilePath();
9312
- if (fs16.existsSync(pidPath)) {
9313
- fs16.unlinkSync(pidPath);
9891
+ if (fs18.existsSync(pidPath)) {
9892
+ fs18.unlinkSync(pidPath);
9314
9893
  console.log("[Daemon] PID file cleaned up");
9315
9894
  }
9316
9895
  } catch (error) {