episoda 0.2.13 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1718,6 +1718,18 @@ var require_websocket_client = __commonJS({
1718
1718
  this.isIntentionalDisconnect = false;
1719
1719
  this.lastConnectAttemptTime = Date.now();
1720
1720
  this.lastErrorCode = void 0;
1721
+ if (this.ws) {
1722
+ try {
1723
+ this.ws.removeAllListeners();
1724
+ this.ws.terminate();
1725
+ } catch {
1726
+ }
1727
+ this.ws = void 0;
1728
+ }
1729
+ if (this.reconnectTimeout) {
1730
+ clearTimeout(this.reconnectTimeout);
1731
+ this.reconnectTimeout = void 0;
1732
+ }
1721
1733
  return new Promise((resolve2, reject) => {
1722
1734
  const connectionTimeout = setTimeout(() => {
1723
1735
  if (this.ws) {
@@ -1827,6 +1839,32 @@ var require_websocket_client = __commonJS({
1827
1839
  }
1828
1840
  this.eventHandlers.get(event).push(handler);
1829
1841
  }
1842
+ /**
1843
+ * EP812: Register a one-time event handler (removes itself after first call)
1844
+ * @param event - Event type
1845
+ * @param handler - Handler function
1846
+ */
1847
+ once(event, handler) {
1848
+ const onceHandler = (message) => {
1849
+ this.off(event, onceHandler);
1850
+ handler(message);
1851
+ };
1852
+ this.on(event, onceHandler);
1853
+ }
1854
+ /**
1855
+ * EP812: Remove an event handler
1856
+ * @param event - Event type
1857
+ * @param handler - Handler function to remove
1858
+ */
1859
+ off(event, handler) {
1860
+ const handlers = this.eventHandlers.get(event);
1861
+ if (handlers) {
1862
+ const index = handlers.indexOf(handler);
1863
+ if (index !== -1) {
1864
+ handlers.splice(index, 1);
1865
+ }
1866
+ }
1867
+ }
1830
1868
  /**
1831
1869
  * Send a message to the server
1832
1870
  * @param message - Client message to send
@@ -1920,6 +1958,10 @@ var require_websocket_client = __commonJS({
1920
1958
  console.log("[EpisodaClient] Intentional disconnect - not reconnecting");
1921
1959
  return;
1922
1960
  }
1961
+ if (this.reconnectTimeout) {
1962
+ console.log("[EpisodaClient] Reconnection already scheduled, skipping duplicate");
1963
+ return;
1964
+ }
1923
1965
  if (this.heartbeatTimer) {
1924
1966
  clearInterval(this.heartbeatTimer);
1925
1967
  this.heartbeatTimer = void 0;
@@ -2555,11 +2597,13 @@ async function startDaemon() {
2555
2597
  if (!fs2.existsSync(daemonScript)) {
2556
2598
  throw new Error(`Daemon script not found: ${daemonScript}. Make sure CLI is built.`);
2557
2599
  }
2600
+ const logPath = path2.join(configDir, "daemon.log");
2601
+ const logFd = fs2.openSync(logPath, "a");
2558
2602
  const child = (0, import_child_process.spawn)("node", [daemonScript], {
2559
2603
  detached: true,
2560
2604
  // Run independently of parent
2561
- stdio: "ignore",
2562
- // Don't inherit stdio (prevents hanging)
2605
+ stdio: ["ignore", logFd, logFd],
2606
+ // EP813: Redirect stdout/stderr to log file
2563
2607
  env: {
2564
2608
  ...process.env,
2565
2609
  EPISODA_DAEMON_MODE: "1"
@@ -2754,9 +2798,38 @@ async function devCommand(options = {}) {
2754
2798
  status.info("This will authenticate with episoda.dev and configure the CLI.");
2755
2799
  process.exit(1);
2756
2800
  }
2757
- const killedCount = killAllEpisodaProcesses();
2758
- if (killedCount > 0) {
2759
- status.info(`Cleaned up ${killedCount} stale process${killedCount > 1 ? "es" : ""}`);
2801
+ let needsRestart = false;
2802
+ const existingPid = isDaemonRunning();
2803
+ if (existingPid) {
2804
+ const reachable2 = await isDaemonReachable();
2805
+ if (reachable2) {
2806
+ try {
2807
+ const health = await verifyHealth();
2808
+ if (health.healthyConnections > 0) {
2809
+ status.debug(`Daemon already running and healthy (PID: ${existingPid})`);
2810
+ } else if (health.staleConnections > 0) {
2811
+ status.info("Daemon has stale connections, restarting...");
2812
+ needsRestart = true;
2813
+ } else {
2814
+ status.debug(`Daemon running but no connections (PID: ${existingPid})`);
2815
+ }
2816
+ } catch {
2817
+ status.debug("Health check failed, restarting daemon...");
2818
+ needsRestart = true;
2819
+ }
2820
+ } else {
2821
+ status.debug("Daemon not reachable via IPC, restarting...");
2822
+ needsRestart = true;
2823
+ }
2824
+ } else {
2825
+ needsRestart = false;
2826
+ }
2827
+ if (needsRestart) {
2828
+ const killedCount = killAllEpisodaProcesses();
2829
+ if (killedCount > 0) {
2830
+ status.info(`Cleaned up ${killedCount} stale process${killedCount > 1 ? "es" : ""}`);
2831
+ await new Promise((resolve2) => setTimeout(resolve2, 2e3));
2832
+ }
2760
2833
  }
2761
2834
  const serverUrl = config.api_url || process.env.EPISODA_API_URL || "https://episoda.dev";
2762
2835
  let projectPath;
@@ -2908,6 +2981,12 @@ async function runDevServer(command, cwd, autoRestart) {
2908
2981
  setTimeout(() => {
2909
2982
  startServer();
2910
2983
  }, 2e3);
2984
+ } else if (!shuttingDown) {
2985
+ status.info("");
2986
+ status.info("Dev server stopped, but daemon remains connected.");
2987
+ status.info("Git operations will still be executed by Episoda.");
2988
+ status.info("Press Ctrl+C to disconnect.");
2989
+ status.info("");
2911
2990
  }
2912
2991
  });
2913
2992
  devProcess.on("error", (error) => {