@episoda/cli 0.2.174 → 0.2.176

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
@@ -1640,15 +1640,15 @@ var require_git_executor = __commonJS({
1640
1640
  try {
1641
1641
  const { stdout: gitDir } = await execAsync("git rev-parse --git-dir", { cwd, timeout: 5e3 });
1642
1642
  const gitDirPath = gitDir.trim();
1643
- const fs16 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1643
+ const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1644
1644
  const rebaseMergePath = `${gitDirPath}/rebase-merge`;
1645
1645
  const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
1646
1646
  try {
1647
- await fs16.access(rebaseMergePath);
1647
+ await fs17.access(rebaseMergePath);
1648
1648
  inRebase = true;
1649
1649
  } catch {
1650
1650
  try {
1651
- await fs16.access(rebaseApplyPath);
1651
+ await fs17.access(rebaseApplyPath);
1652
1652
  inRebase = true;
1653
1653
  } catch {
1654
1654
  inRebase = false;
@@ -1704,9 +1704,9 @@ var require_git_executor = __commonJS({
1704
1704
  };
1705
1705
  }
1706
1706
  }
1707
- const fs16 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1707
+ const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1708
1708
  try {
1709
- await fs16.access(command.path);
1709
+ await fs17.access(command.path);
1710
1710
  return {
1711
1711
  success: false,
1712
1712
  error: "WORKTREE_EXISTS",
@@ -1765,9 +1765,9 @@ var require_git_executor = __commonJS({
1765
1765
  */
1766
1766
  async executeWorktreeRemove(command, cwd, options) {
1767
1767
  try {
1768
- const fs16 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1768
+ const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1769
1769
  try {
1770
- await fs16.access(command.path);
1770
+ await fs17.access(command.path);
1771
1771
  } catch {
1772
1772
  return {
1773
1773
  success: false,
@@ -1802,7 +1802,7 @@ var require_git_executor = __commonJS({
1802
1802
  const result = await this.runGitCommand(args, cwd, options);
1803
1803
  if (result.success) {
1804
1804
  try {
1805
- await fs16.rm(command.path, { recursive: true, force: true });
1805
+ await fs17.rm(command.path, { recursive: true, force: true });
1806
1806
  } catch {
1807
1807
  }
1808
1808
  return {
@@ -1936,10 +1936,10 @@ var require_git_executor = __commonJS({
1936
1936
  */
1937
1937
  async executeCloneBare(command, options) {
1938
1938
  try {
1939
- const fs16 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1940
- const path19 = await Promise.resolve().then(() => __importStar(require("path")));
1939
+ const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1940
+ const path20 = await Promise.resolve().then(() => __importStar(require("path")));
1941
1941
  try {
1942
- await fs16.access(command.path);
1942
+ await fs17.access(command.path);
1943
1943
  return {
1944
1944
  success: false,
1945
1945
  error: "BRANCH_ALREADY_EXISTS",
@@ -1948,9 +1948,9 @@ var require_git_executor = __commonJS({
1948
1948
  };
1949
1949
  } catch {
1950
1950
  }
1951
- const parentDir = path19.dirname(command.path);
1951
+ const parentDir = path20.dirname(command.path);
1952
1952
  try {
1953
- await fs16.mkdir(parentDir, { recursive: true });
1953
+ await fs17.mkdir(parentDir, { recursive: true });
1954
1954
  } catch {
1955
1955
  }
1956
1956
  const { stdout, stderr } = await execAsync(
@@ -1998,22 +1998,22 @@ var require_git_executor = __commonJS({
1998
1998
  */
1999
1999
  async executeProjectInfo(cwd, options) {
2000
2000
  try {
2001
- const fs16 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
2002
- const path19 = await Promise.resolve().then(() => __importStar(require("path")));
2001
+ const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
2002
+ const path20 = await Promise.resolve().then(() => __importStar(require("path")));
2003
2003
  let currentPath = cwd;
2004
2004
  let projectPath = cwd;
2005
2005
  let bareRepoPath;
2006
2006
  for (let i = 0; i < 10; i++) {
2007
- const bareDir = path19.join(currentPath, ".bare");
2008
- const episodaDir = path19.join(currentPath, ".episoda");
2007
+ const bareDir = path20.join(currentPath, ".bare");
2008
+ const episodaDir = path20.join(currentPath, ".episoda");
2009
2009
  try {
2010
- await fs16.access(bareDir);
2011
- await fs16.access(episodaDir);
2010
+ await fs17.access(bareDir);
2011
+ await fs17.access(episodaDir);
2012
2012
  projectPath = currentPath;
2013
2013
  bareRepoPath = bareDir;
2014
2014
  break;
2015
2015
  } catch {
2016
- const parentPath = path19.dirname(currentPath);
2016
+ const parentPath = path20.dirname(currentPath);
2017
2017
  if (parentPath === currentPath) {
2018
2018
  break;
2019
2019
  }
@@ -2187,6 +2187,7 @@ var require_websocket_client = __commonJS({
2187
2187
  constructor() {
2188
2188
  this.eventHandlers = /* @__PURE__ */ new Map();
2189
2189
  this.reconnectAttempts = 0;
2190
+ this.autoReconnectEnabled = true;
2190
2191
  this.url = "";
2191
2192
  this.token = "";
2192
2193
  this.isConnected = false;
@@ -2235,7 +2236,7 @@ var require_websocket_client = __commonJS({
2235
2236
  clearTimeout(this.reconnectTimeout);
2236
2237
  this.reconnectTimeout = void 0;
2237
2238
  }
2238
- return new Promise((resolve5, reject) => {
2239
+ return new Promise((resolve6, reject) => {
2239
2240
  const connectionTimeout = setTimeout(() => {
2240
2241
  if (this.ws) {
2241
2242
  this.ws.terminate();
@@ -2266,7 +2267,7 @@ var require_websocket_client = __commonJS({
2266
2267
  daemonPid: this.daemonPid
2267
2268
  });
2268
2269
  this.startHeartbeat();
2269
- resolve5();
2270
+ resolve6();
2270
2271
  });
2271
2272
  this.ws.on("pong", () => {
2272
2273
  if (this.heartbeatTimeoutTimer) {
@@ -2288,7 +2289,7 @@ var require_websocket_client = __commonJS({
2288
2289
  this.ws.on("close", (code, reason) => {
2289
2290
  console.log(`[EpisodaClient] WebSocket closed: ${code} ${reason.toString()}`);
2290
2291
  this.isConnected = false;
2291
- const willReconnect = !this.isDisconnecting;
2292
+ const willReconnect = !this.isDisconnecting && this.autoReconnectEnabled;
2292
2293
  this.emit({
2293
2294
  type: "disconnected",
2294
2295
  code,
@@ -2312,6 +2313,19 @@ var require_websocket_client = __commonJS({
2312
2313
  }
2313
2314
  });
2314
2315
  }
2316
+ /**
2317
+ * Enable/disable automatic reconnect scheduling.
2318
+ *
2319
+ * This lets daemon startup/handshake own retry policy without competing
2320
+ * with client-side reconnect loops.
2321
+ */
2322
+ setAutoReconnect(enabled) {
2323
+ this.autoReconnectEnabled = enabled;
2324
+ if (!enabled && this.reconnectTimeout) {
2325
+ clearTimeout(this.reconnectTimeout);
2326
+ this.reconnectTimeout = void 0;
2327
+ }
2328
+ }
2315
2329
  /**
2316
2330
  * Disconnect from the server
2317
2331
  * @param intentional - If true, prevents automatic reconnection (user-initiated disconnect)
@@ -2397,13 +2411,13 @@ var require_websocket_client = __commonJS({
2397
2411
  console.warn("[EpisodaClient] Cannot send - WebSocket not connected");
2398
2412
  return false;
2399
2413
  }
2400
- return new Promise((resolve5) => {
2414
+ return new Promise((resolve6) => {
2401
2415
  this.ws.send(JSON.stringify(message), (error) => {
2402
2416
  if (error) {
2403
2417
  console.error("[EpisodaClient] Failed to send message:", error);
2404
- resolve5(false);
2418
+ resolve6(false);
2405
2419
  } else {
2406
- resolve5(true);
2420
+ resolve6(true);
2407
2421
  }
2408
2422
  });
2409
2423
  });
@@ -2526,7 +2540,12 @@ var require_websocket_client = __commonJS({
2526
2540
  if (this.rateLimitBackoffUntil && Date.now() < this.rateLimitBackoffUntil) {
2527
2541
  const waitTime = this.rateLimitBackoffUntil - Date.now();
2528
2542
  console.log(`[EpisodaClient] Rate limited, waiting ${Math.round(waitTime / 1e3)}s before retry`);
2529
- this.reconnectAttempts++;
2543
+ this.emit({
2544
+ type: "reconnect_scheduled",
2545
+ attempt: this.reconnectAttempts + 1,
2546
+ delayMs: waitTime,
2547
+ strategy: "rate_limited"
2548
+ });
2530
2549
  this.reconnectTimeout = setTimeout(() => {
2531
2550
  this.rateLimitBackoffUntil = void 0;
2532
2551
  this.scheduleReconnect();
@@ -2534,6 +2553,7 @@ var require_websocket_client = __commonJS({
2534
2553
  return;
2535
2554
  }
2536
2555
  let delay;
2556
+ let strategy = "connection_lost";
2537
2557
  let shouldRetry = true;
2538
2558
  const isCloudMode = this.environment === "cloud";
2539
2559
  const MAX_CLOUD_AUTH_FAILURES = 3;
@@ -2545,20 +2565,28 @@ var require_websocket_client = __commonJS({
2545
2565
  } else {
2546
2566
  delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_CLOUD_RECONNECT_DELAY);
2547
2567
  delay = applyJitter(delay);
2568
+ strategy = "cloud";
2548
2569
  const delayStr = delay >= 6e4 ? `${Math.round(delay / 6e4)}m` : `${Math.round(delay / 1e3)}s`;
2549
2570
  console.log(`[EpisodaClient] Cloud mode: reconnecting in ${delayStr}... (attempt ${this.reconnectAttempts + 1}, never giving up)`);
2550
2571
  }
2551
2572
  } else if (this.isGracefulShutdown && this.reconnectAttempts < 7) {
2552
2573
  delay = Math.min(500 * Math.pow(2, this.reconnectAttempts), 5e3);
2553
2574
  delay = applyJitter(delay);
2575
+ strategy = "graceful_shutdown";
2554
2576
  console.log(`[EpisodaClient] Server restarting, reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/7)`);
2555
2577
  } else {
2556
2578
  delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_LOCAL_RECONNECT_DELAY);
2557
2579
  delay = applyJitter(delay);
2580
+ strategy = "connection_lost";
2558
2581
  const delayStr = delay >= 6e4 ? `${Math.round(delay / 6e4)}m` : `${Math.round(delay / 1e3)}s`;
2559
2582
  console.log(`[EpisodaClient] Connection lost, retrying in ${delayStr}... (attempt ${this.reconnectAttempts + 1}, retrying until connected)`);
2560
2583
  }
2561
2584
  if (!shouldRetry) {
2585
+ this.emit({
2586
+ type: "reconnect_exhausted",
2587
+ attempts: this.reconnectAttempts,
2588
+ reason: `retry_exhausted_after_${this.consecutiveAuthFailures}_auth_failures`
2589
+ });
2562
2590
  this.emit({
2563
2591
  type: "disconnected",
2564
2592
  code: 1006,
@@ -2567,9 +2595,20 @@ var require_websocket_client = __commonJS({
2567
2595
  });
2568
2596
  return;
2569
2597
  }
2570
- this.reconnectAttempts++;
2598
+ const reconnectAttempt = this.reconnectAttempts + 1;
2599
+ this.reconnectAttempts = reconnectAttempt;
2600
+ this.emit({
2601
+ type: "reconnect_scheduled",
2602
+ attempt: reconnectAttempt,
2603
+ delayMs: delay,
2604
+ strategy
2605
+ });
2571
2606
  this.reconnectTimeout = setTimeout(() => {
2572
2607
  console.log("[EpisodaClient] Attempting reconnection...");
2608
+ this.emit({
2609
+ type: "reconnect_attempt",
2610
+ attempt: reconnectAttempt
2611
+ });
2573
2612
  this.connect(this.url, this.token, this.machineId, {
2574
2613
  hostname: this.hostname,
2575
2614
  osPlatform: this.osPlatform,
@@ -2582,6 +2621,11 @@ var require_websocket_client = __commonJS({
2582
2621
  containerId: this.containerId
2583
2622
  }).then(() => {
2584
2623
  console.log("[EpisodaClient] Reconnection successful");
2624
+ this.emit({
2625
+ type: "reconnect_result",
2626
+ attempt: reconnectAttempt,
2627
+ success: true
2628
+ });
2585
2629
  this.reconnectAttempts = 0;
2586
2630
  this.isGracefulShutdown = false;
2587
2631
  this.firstDisconnectTime = void 0;
@@ -2589,6 +2633,12 @@ var require_websocket_client = __commonJS({
2589
2633
  this.consecutiveAuthFailures = 0;
2590
2634
  }).catch((error) => {
2591
2635
  console.error("[EpisodaClient] Reconnection failed:", error.message);
2636
+ this.emit({
2637
+ type: "reconnect_result",
2638
+ attempt: reconnectAttempt,
2639
+ success: false,
2640
+ error: error instanceof Error ? error.message : String(error)
2641
+ });
2592
2642
  });
2593
2643
  }, delay);
2594
2644
  }
@@ -2669,38 +2719,38 @@ var require_auth = __commonJS({
2669
2719
  };
2670
2720
  })();
2671
2721
  Object.defineProperty(exports2, "__esModule", { value: true });
2672
- exports2.getConfigDir = getConfigDir6;
2722
+ exports2.getConfigDir = getConfigDir7;
2673
2723
  exports2.getConfigPath = getConfigPath4;
2674
2724
  exports2.loadConfig = loadConfig9;
2675
2725
  exports2.saveConfig = saveConfig3;
2676
2726
  exports2.validateToken = validateToken;
2677
- var fs16 = __importStar(require("fs"));
2678
- var path19 = __importStar(require("path"));
2727
+ var fs17 = __importStar(require("fs"));
2728
+ var path20 = __importStar(require("path"));
2679
2729
  var os5 = __importStar(require("os"));
2680
2730
  var child_process_1 = require("child_process");
2681
2731
  var DEFAULT_CONFIG_FILE = "config.json";
2682
2732
  var hasWarnedMissingProjectId = false;
2683
2733
  var hasWarnedMissingRequiredFields = false;
2684
- function getConfigDir6() {
2685
- return process.env.EPISODA_CONFIG_DIR || path19.join(os5.homedir(), ".episoda");
2734
+ function getConfigDir7() {
2735
+ return process.env.EPISODA_CONFIG_DIR || path20.join(os5.homedir(), ".episoda");
2686
2736
  }
2687
2737
  function getConfigPath4(configPath) {
2688
2738
  if (configPath) {
2689
2739
  return configPath;
2690
2740
  }
2691
- return path19.join(getConfigDir6(), DEFAULT_CONFIG_FILE);
2741
+ return path20.join(getConfigDir7(), DEFAULT_CONFIG_FILE);
2692
2742
  }
2693
2743
  function ensureConfigDir(configPath) {
2694
- const dir = path19.dirname(configPath);
2695
- const isNew = !fs16.existsSync(dir);
2744
+ const dir = path20.dirname(configPath);
2745
+ const isNew = !fs17.existsSync(dir);
2696
2746
  if (isNew) {
2697
- fs16.mkdirSync(dir, { recursive: true, mode: 448 });
2747
+ fs17.mkdirSync(dir, { recursive: true, mode: 448 });
2698
2748
  }
2699
2749
  if (process.platform === "darwin") {
2700
- const nosyncPath = path19.join(dir, ".nosync");
2701
- if (isNew || !fs16.existsSync(nosyncPath)) {
2750
+ const nosyncPath = path20.join(dir, ".nosync");
2751
+ if (isNew || !fs17.existsSync(nosyncPath)) {
2702
2752
  try {
2703
- fs16.writeFileSync(nosyncPath, "", { mode: 384 });
2753
+ fs17.writeFileSync(nosyncPath, "", { mode: 384 });
2704
2754
  (0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
2705
2755
  stdio: "ignore",
2706
2756
  timeout: 5e3
@@ -2714,11 +2764,11 @@ var require_auth = __commonJS({
2714
2764
  const fullPath = getConfigPath4(configPath);
2715
2765
  const isCloudMode = process.env.EPISODA_MODE === "cloud";
2716
2766
  const readConfigFile = (pathToFile) => {
2717
- if (!fs16.existsSync(pathToFile)) {
2767
+ if (!fs17.existsSync(pathToFile)) {
2718
2768
  return null;
2719
2769
  }
2720
2770
  try {
2721
- const content = fs16.readFileSync(pathToFile, "utf8");
2771
+ const content = fs17.readFileSync(pathToFile, "utf8");
2722
2772
  return JSON.parse(content);
2723
2773
  } catch (error) {
2724
2774
  console.error("Error loading config:", error);
@@ -2757,11 +2807,11 @@ var require_auth = __commonJS({
2757
2807
  }
2758
2808
  const homeDir = process.env.HOME || require("os").homedir();
2759
2809
  const workspaceConfigPath = require("path").join(homeDir, "episoda", process.env.EPISODA_WORKSPACE, ".episoda", "config.json");
2760
- if (!fs16.existsSync(workspaceConfigPath)) {
2810
+ if (!fs17.existsSync(workspaceConfigPath)) {
2761
2811
  return null;
2762
2812
  }
2763
2813
  try {
2764
- const content = fs16.readFileSync(workspaceConfigPath, "utf8");
2814
+ const content = fs17.readFileSync(workspaceConfigPath, "utf8");
2765
2815
  const workspaceConfig2 = JSON.parse(content);
2766
2816
  const expiresAtEnv = envValue(process.env.EPISODA_ACCESS_TOKEN_EXPIRES_AT);
2767
2817
  return {
@@ -2789,11 +2839,11 @@ var require_auth = __commonJS({
2789
2839
  }
2790
2840
  const homeDir = process.env.HOME || require("os").homedir();
2791
2841
  const projectConfigPath = require("path").join(homeDir, "episoda", workspaceSlug, projectSlug, ".episoda", "config.json");
2792
- if (!fs16.existsSync(projectConfigPath)) {
2842
+ if (!fs17.existsSync(projectConfigPath)) {
2793
2843
  return null;
2794
2844
  }
2795
2845
  try {
2796
- const content = fs16.readFileSync(projectConfigPath, "utf8");
2846
+ const content = fs17.readFileSync(projectConfigPath, "utf8");
2797
2847
  const projectConfig2 = JSON.parse(content);
2798
2848
  return {
2799
2849
  project_id: projectConfig2.projectId || projectConfig2.project_id,
@@ -2861,7 +2911,7 @@ var require_auth = __commonJS({
2861
2911
  ensureConfigDir(fullPath);
2862
2912
  try {
2863
2913
  const content = JSON.stringify(config, null, 2);
2864
- fs16.writeFileSync(fullPath, content, { mode: 384 });
2914
+ fs17.writeFileSync(fullPath, content, { mode: 384 });
2865
2915
  } catch (error) {
2866
2916
  throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
2867
2917
  }
@@ -3020,6 +3070,7 @@ var status = {
3020
3070
  // src/daemon/daemon-manager.ts
3021
3071
  var fs = __toESM(require("fs"));
3022
3072
  var path = __toESM(require("path"));
3073
+ var net = __toESM(require("net"));
3023
3074
  var import_child_process = require("child_process");
3024
3075
  var import_crypto = require("crypto");
3025
3076
  var import_core = __toESM(require_dist());
@@ -3032,6 +3083,20 @@ function logSignalDispatch(input) {
3032
3083
  };
3033
3084
  console.log(`[DaemonManager] SignalDispatch ${JSON.stringify(payload)}`);
3034
3085
  }
3086
+ function logReliabilityMetric(metric, fields) {
3087
+ const line = `[Daemon][Metric] ${JSON.stringify({
3088
+ metric,
3089
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3090
+ ...fields
3091
+ })}`;
3092
+ console.log(line);
3093
+ try {
3094
+ const daemonLogPath = path.join((0, import_core.getConfigDir)(), "daemon.log");
3095
+ fs.appendFileSync(daemonLogPath, `${line}
3096
+ `, "utf-8");
3097
+ } catch {
3098
+ }
3099
+ }
3035
3100
  function killAllEpisodaProcesses() {
3036
3101
  const currentPid = process.pid;
3037
3102
  let killedCount = 0;
@@ -3084,6 +3149,111 @@ function killAllEpisodaProcesses() {
3084
3149
  function getPidFilePath() {
3085
3150
  return path.join((0, import_core.getConfigDir)(), "daemon.pid");
3086
3151
  }
3152
+ function resolveDaemonEntryScript() {
3153
+ const packagedPath = path.join(__dirname, "daemon", "daemon-process.js");
3154
+ if (fs.existsSync(packagedPath)) {
3155
+ return packagedPath;
3156
+ }
3157
+ const sourceFallbackPath = path.resolve(__dirname, "../../dist/daemon/daemon-process.js");
3158
+ if (fs.existsSync(sourceFallbackPath)) {
3159
+ return sourceFallbackPath;
3160
+ }
3161
+ throw new Error(
3162
+ `Daemon script not found. Checked: ${packagedPath}, ${sourceFallbackPath}. Make sure CLI is built.`
3163
+ );
3164
+ }
3165
+ async function pingDaemonSocket(socketPath, timeoutMs) {
3166
+ await new Promise((resolve6, reject) => {
3167
+ const socket = net.createConnection(socketPath);
3168
+ const requestId = (0, import_crypto.randomUUID)();
3169
+ let settled = false;
3170
+ let buffer = "";
3171
+ const finish = (fn) => {
3172
+ if (settled) return;
3173
+ settled = true;
3174
+ clearTimeout(timer);
3175
+ socket.removeAllListeners();
3176
+ socket.destroy();
3177
+ fn();
3178
+ };
3179
+ const timer = setTimeout(() => {
3180
+ finish(() => reject(new Error("IPC ping timeout")));
3181
+ }, timeoutMs);
3182
+ socket.on("connect", () => {
3183
+ socket.write(JSON.stringify({ id: requestId, command: "ping", params: {} }) + "\n");
3184
+ });
3185
+ socket.on("data", (chunk) => {
3186
+ buffer += chunk.toString("utf8");
3187
+ const parsed = consumeIpcPingBufferForRequest(buffer, requestId);
3188
+ buffer = parsed.remainingBuffer;
3189
+ if (parsed.result === "pending") return;
3190
+ if (parsed.result === "success") {
3191
+ finish(() => resolve6());
3192
+ return;
3193
+ }
3194
+ finish(() => reject(new Error(parsed.error || "IPC ping failed")));
3195
+ });
3196
+ socket.on("error", (error) => {
3197
+ finish(() => reject(error));
3198
+ });
3199
+ });
3200
+ }
3201
+ function consumeIpcPingBufferForRequest(buffer, requestId) {
3202
+ let remaining = buffer;
3203
+ while (true) {
3204
+ const newlineIndex = remaining.indexOf("\n");
3205
+ if (newlineIndex === -1) {
3206
+ return { result: "pending", remainingBuffer: remaining };
3207
+ }
3208
+ const frame = remaining.slice(0, newlineIndex).trim();
3209
+ remaining = remaining.slice(newlineIndex + 1);
3210
+ if (!frame) continue;
3211
+ let parsed = null;
3212
+ try {
3213
+ parsed = JSON.parse(frame);
3214
+ } catch {
3215
+ continue;
3216
+ }
3217
+ if (!parsed || parsed.id !== requestId) {
3218
+ continue;
3219
+ }
3220
+ if (parsed.success) {
3221
+ return { result: "success", remainingBuffer: remaining };
3222
+ }
3223
+ return {
3224
+ result: "error",
3225
+ remainingBuffer: remaining,
3226
+ error: parsed.error || "IPC ping failed"
3227
+ };
3228
+ }
3229
+ }
3230
+ async function waitForDaemonIpcReady(timeoutMs = 15e3) {
3231
+ const socketPath = path.join((0, import_core.getConfigDir)(), "daemon.sock");
3232
+ const deadline = Date.now() + timeoutMs;
3233
+ let lastError = "unknown";
3234
+ while (Date.now() < deadline) {
3235
+ try {
3236
+ await pingDaemonSocket(socketPath, 1e3);
3237
+ return;
3238
+ } catch (error) {
3239
+ lastError = error instanceof Error ? error.message : String(error);
3240
+ await new Promise((resolve6) => setTimeout(resolve6, 200));
3241
+ }
3242
+ }
3243
+ throw new Error(`Daemon IPC not ready within ${Math.round(timeoutMs / 1e3)}s (${lastError})`);
3244
+ }
3245
+ async function waitForProcessStart(pid, timeoutMs = 5e3) {
3246
+ const deadline = Date.now() + timeoutMs;
3247
+ while (Date.now() < deadline) {
3248
+ try {
3249
+ process.kill(pid, 0);
3250
+ return;
3251
+ } catch {
3252
+ await new Promise((resolve6) => setTimeout(resolve6, 100));
3253
+ }
3254
+ }
3255
+ throw new Error(`Daemon process did not become alive within ${Math.round(timeoutMs / 1e3)}s`);
3256
+ }
3087
3257
  function looksLikeEpisodaDaemonProcess(pid) {
3088
3258
  if (process.platform === "win32") {
3089
3259
  return true;
@@ -3128,6 +3298,7 @@ function isDaemonRunning() {
3128
3298
  }
3129
3299
  }
3130
3300
  async function startDaemon() {
3301
+ const startTime = Date.now();
3131
3302
  const existingPid = isDaemonRunning();
3132
3303
  if (existingPid) {
3133
3304
  throw new Error(`Daemon already running (PID: ${existingPid})`);
@@ -3136,10 +3307,7 @@ async function startDaemon() {
3136
3307
  if (!fs.existsSync(configDir)) {
3137
3308
  fs.mkdirSync(configDir, { recursive: true });
3138
3309
  }
3139
- const daemonScript = path.join(__dirname, "daemon", "daemon-process.js");
3140
- if (!fs.existsSync(daemonScript)) {
3141
- throw new Error(`Daemon script not found: ${daemonScript}. Make sure CLI is built.`);
3142
- }
3310
+ const daemonScript = resolveDaemonEntryScript();
3143
3311
  const logPath = path.join(configDir, "daemon.log");
3144
3312
  rotateDaemonLog(logPath);
3145
3313
  const logFd = fs.openSync(logPath, "a");
@@ -3148,6 +3316,8 @@ async function startDaemon() {
3148
3316
  // Run independently of parent
3149
3317
  stdio: ["ignore", logFd, logFd],
3150
3318
  // EP813: Redirect stdout/stderr to log file
3319
+ cwd: configDir,
3320
+ // Stable daemon context; never inherit transient shell cwd
3151
3321
  env: {
3152
3322
  ...process.env,
3153
3323
  EPISODA_DAEMON_MODE: "1"
@@ -3158,10 +3328,29 @@ async function startDaemon() {
3158
3328
  const pid = child.pid;
3159
3329
  const pidPath = getPidFilePath();
3160
3330
  fs.writeFileSync(pidPath, pid.toString(), "utf-8");
3161
- await new Promise((resolve5) => setTimeout(resolve5, 500));
3162
- const runningPid = isDaemonRunning();
3163
- if (!runningPid) {
3164
- throw new Error("Daemon failed to start");
3331
+ await waitForProcessStart(pid, 5e3);
3332
+ try {
3333
+ await waitForDaemonIpcReady(15e3);
3334
+ logReliabilityMetric("daemon_startup_ready", {
3335
+ pid,
3336
+ startupReadyLatencyMs: Date.now() - startTime
3337
+ });
3338
+ } catch (error) {
3339
+ try {
3340
+ logSignalDispatch({
3341
+ initiator: "startDaemon",
3342
+ pid,
3343
+ signal: "SIGTERM",
3344
+ reason: "ipc_readiness_failed"
3345
+ });
3346
+ process.kill(pid, "SIGTERM");
3347
+ } catch {
3348
+ }
3349
+ try {
3350
+ if (fs.existsSync(pidPath)) fs.unlinkSync(pidPath);
3351
+ } catch {
3352
+ }
3353
+ throw error;
3165
3354
  }
3166
3355
  return pid;
3167
3356
  }
@@ -3186,7 +3375,7 @@ async function stopDaemon(timeout = 5e3) {
3186
3375
  while (Date.now() - startTime < timeout) {
3187
3376
  try {
3188
3377
  process.kill(pid, 0);
3189
- await new Promise((resolve5) => setTimeout(resolve5, 100));
3378
+ await new Promise((resolve6) => setTimeout(resolve6, 100));
3190
3379
  } catch (error) {
3191
3380
  const pidPath2 = getPidFilePath();
3192
3381
  if (fs.existsSync(pidPath2)) {
@@ -3240,15 +3429,15 @@ function rotateDaemonLog(logPath) {
3240
3429
  }
3241
3430
 
3242
3431
  // src/ipc/ipc-client.ts
3243
- var net = __toESM(require("net"));
3432
+ var net2 = __toESM(require("net"));
3244
3433
  var path2 = __toESM(require("path"));
3245
3434
  var crypto = __toESM(require("crypto"));
3246
3435
  var import_core2 = __toESM(require_dist());
3247
3436
  var getSocketPath = () => path2.join((0, import_core2.getConfigDir)(), "daemon.sock");
3248
3437
  var DEFAULT_TIMEOUT = 15e3;
3249
3438
  async function sendCommand(command, params, timeout = DEFAULT_TIMEOUT) {
3250
- return new Promise((resolve5, reject) => {
3251
- const socket = net.createConnection(getSocketPath());
3439
+ return new Promise((resolve6, reject) => {
3440
+ const socket = net2.createConnection(getSocketPath());
3252
3441
  const requestId = crypto.randomUUID();
3253
3442
  let buffer = "";
3254
3443
  let timeoutHandle;
@@ -3278,7 +3467,7 @@ async function sendCommand(command, params, timeout = DEFAULT_TIMEOUT) {
3278
3467
  clearTimeout(timeoutHandle);
3279
3468
  socket.end();
3280
3469
  if (response.success) {
3281
- resolve5(response.data);
3470
+ resolve6(response.data);
3282
3471
  } else {
3283
3472
  reject(new Error(response.error || "Command failed"));
3284
3473
  }
@@ -3305,7 +3494,7 @@ async function sendCommand(command, params, timeout = DEFAULT_TIMEOUT) {
3305
3494
  }
3306
3495
  async function isDaemonReachable() {
3307
3496
  try {
3308
- await sendCommand("ping", {}, 1e3);
3497
+ await sendCommand("ping", {}, 3e3);
3309
3498
  return true;
3310
3499
  } catch (error) {
3311
3500
  return false;
@@ -3947,7 +4136,7 @@ var WorktreeManager = class _WorktreeManager {
3947
4136
  const lockContent = fs2.readFileSync(lockPath, "utf-8").trim();
3948
4137
  const lockPid = parseInt(lockContent, 10);
3949
4138
  if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
3950
- await new Promise((resolve5) => setTimeout(resolve5, retryInterval));
4139
+ await new Promise((resolve6) => setTimeout(resolve6, retryInterval));
3951
4140
  continue;
3952
4141
  }
3953
4142
  } catch {
@@ -3961,7 +4150,7 @@ var WorktreeManager = class _WorktreeManager {
3961
4150
  } catch {
3962
4151
  continue;
3963
4152
  }
3964
- await new Promise((resolve5) => setTimeout(resolve5, retryInterval));
4153
+ await new Promise((resolve6) => setTimeout(resolve6, retryInterval));
3965
4154
  continue;
3966
4155
  }
3967
4156
  throw err;
@@ -4298,7 +4487,7 @@ async function fetchProjectPath(config, projectId) {
4298
4487
  return null;
4299
4488
  }
4300
4489
  }
4301
- async function syncProjectPath(config, projectId, path19) {
4490
+ async function syncProjectPath(config, projectId, path20) {
4302
4491
  const machineUuid = config.machine_uuid || config.device_id;
4303
4492
  if (!machineUuid || !config.access_token) {
4304
4493
  return false;
@@ -4312,7 +4501,7 @@ async function syncProjectPath(config, projectId, path19) {
4312
4501
  "Authorization": `Bearer ${config.access_token}`,
4313
4502
  "Content-Type": "application/json"
4314
4503
  },
4315
- body: JSON.stringify({ path: path19 })
4504
+ body: JSON.stringify({ path: path20 })
4316
4505
  });
4317
4506
  if (!response.ok) {
4318
4507
  console.debug(`[MachineSettings] syncProjectPath failed: ${response.status} ${response.statusText}`);
@@ -4566,6 +4755,29 @@ async function ensureProtocolHandlerRegistered() {
4566
4755
  }
4567
4756
  }
4568
4757
 
4758
+ // src/utils/daemon-readiness.ts
4759
+ async function waitForDaemonReachable(isReachable, timeoutMs = 15e3, pollMs = 250, options = {}) {
4760
+ const deadline = Date.now() + timeoutMs;
4761
+ const backoffFactor = options.backoffFactor ?? 1.25;
4762
+ const maxPollMs = options.maxPollMs ?? 1e3;
4763
+ const random = options.random ?? Math.random;
4764
+ let currentPollMs = pollMs;
4765
+ while (Date.now() < deadline) {
4766
+ if (await isReachable()) {
4767
+ return true;
4768
+ }
4769
+ const remainingMs = Math.max(0, deadline - Date.now());
4770
+ const jitterMs = options.jitterMs ?? Math.min(50, Math.floor(currentPollMs * 0.2));
4771
+ const jitter = jitterMs > 0 ? Math.floor(random() * jitterMs) : 0;
4772
+ const sleepMs = Math.min(remainingMs, currentPollMs + jitter);
4773
+ if (sleepMs > 0) {
4774
+ await new Promise((resolve6) => setTimeout(resolve6, sleepMs));
4775
+ }
4776
+ currentPollMs = Math.min(maxPollMs, Math.max(1, Math.round(currentPollMs * backoffFactor)));
4777
+ }
4778
+ return false;
4779
+ }
4780
+
4569
4781
  // src/commands/daemon.ts
4570
4782
  var CONNECTION_MAX_RETRIES = 3;
4571
4783
  var FOREGROUND_DAEMON_MONITOR_INTERVAL_MS = 5e3;
@@ -4620,7 +4832,7 @@ async function daemonCommand(options = {}) {
4620
4832
  const killedCount = killAllEpisodaProcesses();
4621
4833
  if (killedCount > 0) {
4622
4834
  status.info(`Cleaned up ${killedCount} stale process${killedCount > 1 ? "es" : ""}`);
4623
- await new Promise((resolve5) => setTimeout(resolve5, 2e3));
4835
+ await new Promise((resolve6) => setTimeout(resolve6, 2e3));
4624
4836
  }
4625
4837
  }
4626
4838
  let projectPath = null;
@@ -4685,7 +4897,7 @@ async function daemonCommand(options = {}) {
4685
4897
  } else {
4686
4898
  status.debug(`Daemon already running (PID: ${daemonPid})`);
4687
4899
  }
4688
- const reachable = await isDaemonReachable();
4900
+ const reachable = await waitForDaemonReachable(isDaemonReachable, 15e3, 250);
4689
4901
  if (!reachable) {
4690
4902
  status.error("Daemon is running but not responding. Try: episoda stop && episoda dev");
4691
4903
  process.exit(1);
@@ -4709,7 +4921,7 @@ async function daemonCommand(options = {}) {
4709
4921
  for (let retry = 0; retry < CONNECTION_MAX_RETRIES && !connected; retry++) {
4710
4922
  if (retry > 0) {
4711
4923
  status.info(`Retrying connection (attempt ${retry + 1}/${CONNECTION_MAX_RETRIES})...`);
4712
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
4924
+ await new Promise((resolve6) => setTimeout(resolve6, 1e3));
4713
4925
  }
4714
4926
  try {
4715
4927
  const result = await addProject(connectId, connectPath);
@@ -5284,10 +5496,10 @@ async function initiateDeviceFlow(apiUrl, machineId) {
5284
5496
  return await response.json();
5285
5497
  }
5286
5498
  async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
5287
- return new Promise((resolve5) => {
5499
+ return new Promise((resolve6) => {
5288
5500
  const timeout = setTimeout(() => {
5289
5501
  status.error("Authorization timed out");
5290
- resolve5(false);
5502
+ resolve6(false);
5291
5503
  }, expiresIn * 1e3);
5292
5504
  const url = `${apiUrl}/api/oauth/authorize-stream?device_code=${deviceCode}`;
5293
5505
  const curlProcess = (0, import_child_process7.spawn)("curl", ["-N", url]);
@@ -5313,26 +5525,26 @@ async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
5313
5525
  if (eventType === "authorized") {
5314
5526
  clearTimeout(timeout);
5315
5527
  curlProcess.kill();
5316
- resolve5(true);
5528
+ resolve6(true);
5317
5529
  return;
5318
5530
  } else if (eventType === "denied") {
5319
5531
  clearTimeout(timeout);
5320
5532
  curlProcess.kill();
5321
5533
  status.error("Authorization denied by user");
5322
- resolve5(false);
5534
+ resolve6(false);
5323
5535
  return;
5324
5536
  } else if (eventType === "expired") {
5325
5537
  clearTimeout(timeout);
5326
5538
  curlProcess.kill();
5327
5539
  status.error("Authorization code expired");
5328
- resolve5(false);
5540
+ resolve6(false);
5329
5541
  return;
5330
5542
  } else if (eventType === "error") {
5331
5543
  const errorData = JSON.parse(data);
5332
5544
  clearTimeout(timeout);
5333
5545
  curlProcess.kill();
5334
5546
  status.error(`Authorization error: ${errorData.error_description || errorData.error || "Unknown error"}`);
5335
- resolve5(false);
5547
+ resolve6(false);
5336
5548
  return;
5337
5549
  }
5338
5550
  } catch (error) {
@@ -5342,13 +5554,13 @@ async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
5342
5554
  curlProcess.on("error", (error) => {
5343
5555
  clearTimeout(timeout);
5344
5556
  status.error(`Failed to monitor authorization: ${error.message}`);
5345
- resolve5(false);
5557
+ resolve6(false);
5346
5558
  });
5347
5559
  curlProcess.on("close", (code) => {
5348
5560
  clearTimeout(timeout);
5349
5561
  if (code !== 0 && code !== null) {
5350
5562
  status.error(`Authorization monitoring failed with code ${code}`);
5351
- resolve5(false);
5563
+ resolve6(false);
5352
5564
  }
5353
5565
  });
5354
5566
  });
@@ -5672,7 +5884,7 @@ async function restartDaemon() {
5672
5884
  stdio: ["pipe", "pipe", "pipe"],
5673
5885
  timeout: 1e4
5674
5886
  });
5675
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
5887
+ await new Promise((resolve6) => setTimeout(resolve6, 1e3));
5676
5888
  const child = (0, import_child_process9.spawn)("episoda", ["dev"], {
5677
5889
  detached: true,
5678
5890
  stdio: "ignore",
@@ -5683,7 +5895,7 @@ async function restartDaemon() {
5683
5895
  const pollIntervalMs = 500;
5684
5896
  const startTime = Date.now();
5685
5897
  while (Date.now() - startTime < maxWaitMs) {
5686
- await new Promise((resolve5) => setTimeout(resolve5, pollIntervalMs));
5898
+ await new Promise((resolve6) => setTimeout(resolve6, pollIntervalMs));
5687
5899
  try {
5688
5900
  const reachable = await isDaemonReachable();
5689
5901
  if (reachable) {
@@ -5790,6 +6002,21 @@ function sanitizeGithubToken(token) {
5790
6002
  }
5791
6003
 
5792
6004
  // src/commands/status.ts
6005
+ var fs9 = __toESM(require("fs"));
6006
+ var path10 = __toESM(require("path"));
6007
+ function logCliReliabilityMetric(metric, fields) {
6008
+ const line = `[CLI][Metric] ${JSON.stringify({
6009
+ metric,
6010
+ at: (/* @__PURE__ */ new Date()).toISOString(),
6011
+ ...fields
6012
+ })}`;
6013
+ console.warn(line);
6014
+ try {
6015
+ fs9.appendFileSync(path10.join((0, import_core8.getConfigDir)(), "daemon.log"), `${line}
6016
+ `, "utf-8");
6017
+ } catch {
6018
+ }
6019
+ }
5793
6020
  async function statusCommand(options = {}) {
5794
6021
  status.info("Checking CLI status...");
5795
6022
  status.info("");
@@ -5810,7 +6037,7 @@ async function statusCommand(options = {}) {
5810
6037
  status.info(` API URL: ${config.api_url}`);
5811
6038
  const effectiveCliVersion = resolveEffectiveCliVersion(import_core8.VERSION);
5812
6039
  const installChannel = detectCliInstallChannel(import_core8.VERSION);
5813
- if (!options.skipUpdateCheck) {
6040
+ if (options.update) {
5814
6041
  const updateResult = await checkForUpdates(effectiveCliVersion);
5815
6042
  if (updateResult.isLinked) {
5816
6043
  status.info(` CLI Version: ${effectiveCliVersion} (linked)`);
@@ -5833,6 +6060,7 @@ async function statusCommand(options = {}) {
5833
6060
  }
5834
6061
  } else {
5835
6062
  status.info(` CLI Version: ${effectiveCliVersion}`);
6063
+ status.info(' Run "episoda update" (or "episoda status --update") to apply updates.');
5836
6064
  }
5837
6065
  if (installChannel.legacyOnly) {
5838
6066
  status.warning(` \u26A0 Legacy package channel detected (episoda@${installChannel.legacyVersion})`);
@@ -5853,6 +6081,10 @@ async function statusCommand(options = {}) {
5853
6081
  if (!daemonStatus) {
5854
6082
  const daemonPid = isDaemonRunning();
5855
6083
  if (daemonPid) {
6084
+ logCliReliabilityMetric("manual_reconnect_needed", {
6085
+ reason: "daemon_running_ipc_unreachable",
6086
+ daemonPid
6087
+ });
5856
6088
  status.error("\u2717 Daemon process is running but IPC is not reachable");
5857
6089
  status.info(` PID: ${daemonPid}`);
5858
6090
  status.info(' Run "episoda dev" to re-establish IPC/connection (or "episoda stop && episoda dev").');
@@ -5874,6 +6106,11 @@ async function statusCommand(options = {}) {
5874
6106
  status.info(` Platform: ${daemonStatus.platform}/${daemonStatus.arch}`);
5875
6107
  status.info(` Project: ${connectedProject.name}`);
5876
6108
  } else if (serverCheck.localConnected && !serverCheck.serverConnected) {
6109
+ logCliReliabilityMetric("manual_reconnect_needed", {
6110
+ reason: "local_connected_server_disagrees",
6111
+ machineId: serverCheck.machineId,
6112
+ serverMachineId: serverCheck.serverMachineId || null
6113
+ });
5877
6114
  status.error("\u2717 Not connected to server");
5878
6115
  status.info(` Device: ${deviceDisplayName}`);
5879
6116
  status.info(` Platform: ${daemonStatus.platform}/${daemonStatus.arch}`);
@@ -6028,24 +6265,24 @@ async function stopCommand(options = {}) {
6028
6265
  }
6029
6266
 
6030
6267
  // src/commands/clone.ts
6031
- var fs10 = __toESM(require("fs"));
6032
- var path11 = __toESM(require("path"));
6268
+ var fs11 = __toESM(require("fs"));
6269
+ var path12 = __toESM(require("path"));
6033
6270
  var import_core10 = __toESM(require_dist());
6034
6271
 
6035
6272
  // src/daemon/project-tracker.ts
6036
- var fs9 = __toESM(require("fs"));
6037
- var path10 = __toESM(require("path"));
6273
+ var fs10 = __toESM(require("fs"));
6274
+ var path11 = __toESM(require("path"));
6038
6275
  var import_core9 = __toESM(require_dist());
6039
6276
  function getProjectsFilePath() {
6040
- return path10.join((0, import_core9.getConfigDir)(), "projects.json");
6277
+ return path11.join((0, import_core9.getConfigDir)(), "projects.json");
6041
6278
  }
6042
6279
  function readProjects() {
6043
6280
  const projectsPath = getProjectsFilePath();
6044
6281
  try {
6045
- if (!fs9.existsSync(projectsPath)) {
6282
+ if (!fs10.existsSync(projectsPath)) {
6046
6283
  return { projects: [] };
6047
6284
  }
6048
- const content = fs9.readFileSync(projectsPath, "utf-8");
6285
+ const content = fs10.readFileSync(projectsPath, "utf-8");
6049
6286
  const data = JSON.parse(content);
6050
6287
  if (!data.projects || !Array.isArray(data.projects)) {
6051
6288
  console.warn("Invalid projects.json structure, resetting");
@@ -6060,11 +6297,11 @@ function readProjects() {
6060
6297
  function writeProjects(data) {
6061
6298
  const projectsPath = getProjectsFilePath();
6062
6299
  try {
6063
- const dir = path10.dirname(projectsPath);
6064
- if (!fs9.existsSync(dir)) {
6065
- fs9.mkdirSync(dir, { recursive: true });
6300
+ const dir = path11.dirname(projectsPath);
6301
+ if (!fs10.existsSync(dir)) {
6302
+ fs10.mkdirSync(dir, { recursive: true });
6066
6303
  }
6067
- fs9.writeFileSync(projectsPath, JSON.stringify(data, null, 2), "utf-8");
6304
+ fs10.writeFileSync(projectsPath, JSON.stringify(data, null, 2), "utf-8");
6068
6305
  } catch (error) {
6069
6306
  throw new Error(`Failed to write projects.json: ${error}`);
6070
6307
  }
@@ -6088,7 +6325,7 @@ function addProject2(projectId, projectPath, options) {
6088
6325
  console.log(`[ProjectTracker] Replacing project entry: ${existingById.path} -> ${projectPath}`);
6089
6326
  data.projects.splice(existingByIdIndex, 1);
6090
6327
  }
6091
- const projectName = path10.basename(projectPath);
6328
+ const projectName = path11.basename(projectPath);
6092
6329
  const newProject = {
6093
6330
  id: projectId,
6094
6331
  path: projectPath,
@@ -6122,9 +6359,9 @@ async function cloneCommand(slugArg, options = {}) {
6122
6359
  }
6123
6360
  const apiUrl = options.apiUrl || config.api_url || "https://episoda.dev";
6124
6361
  const projectPath = getProjectPath(workspaceSlug, projectSlug);
6125
- if (fs10.existsSync(projectPath)) {
6126
- const bareRepoPath = path11.join(projectPath, ".bare");
6127
- if (fs10.existsSync(bareRepoPath)) {
6362
+ if (fs11.existsSync(projectPath)) {
6363
+ const bareRepoPath = path12.join(projectPath, ".bare");
6364
+ if (fs11.existsSync(bareRepoPath)) {
6128
6365
  status.warning(`Project already cloned at ${projectPath}`);
6129
6366
  status.info("");
6130
6367
  status.info("Next steps:");
@@ -6156,7 +6393,7 @@ Please configure a repository in the project settings on episoda.dev.`
6156
6393
  status.info("");
6157
6394
  status.info("Creating project directory...");
6158
6395
  const episodaRoot = getEpisodaRoot();
6159
- fs10.mkdirSync(projectPath, { recursive: true });
6396
+ fs11.mkdirSync(projectPath, { recursive: true });
6160
6397
  status.success(`\u2713 Created ${projectPath}`);
6161
6398
  status.info("Cloning repository (bare)...");
6162
6399
  try {
@@ -6194,9 +6431,9 @@ Please configure a repository in the project settings on episoda.dev.`
6194
6431
  status.info(" 3. episoda checkout {moduleUid} # e.g., episoda checkout EP100");
6195
6432
  status.info("");
6196
6433
  } catch (error) {
6197
- if (fs10.existsSync(projectPath)) {
6434
+ if (fs11.existsSync(projectPath)) {
6198
6435
  try {
6199
- fs10.rmSync(projectPath, { recursive: true, force: true });
6436
+ fs11.rmSync(projectPath, { recursive: true, force: true });
6200
6437
  } catch {
6201
6438
  }
6202
6439
  }
@@ -6290,15 +6527,15 @@ async function fetchWithRetry(url, options, maxRetries = 3) {
6290
6527
  lastError = error instanceof Error ? error : new Error("Network error");
6291
6528
  }
6292
6529
  if (attempt < maxRetries - 1) {
6293
- await new Promise((resolve5) => setTimeout(resolve5, Math.pow(2, attempt) * 1e3));
6530
+ await new Promise((resolve6) => setTimeout(resolve6, Math.pow(2, attempt) * 1e3));
6294
6531
  }
6295
6532
  }
6296
6533
  throw lastError || new Error("Request failed after retries");
6297
6534
  }
6298
6535
 
6299
6536
  // src/utils/env-setup.ts
6300
- var fs11 = __toESM(require("fs"));
6301
- var path12 = __toESM(require("path"));
6537
+ var fs12 = __toESM(require("fs"));
6538
+ var path13 = __toESM(require("path"));
6302
6539
  async function fetchEnvVars(apiUrl, accessToken) {
6303
6540
  try {
6304
6541
  const url = `${apiUrl}/api/cli/env-vars`;
@@ -6333,8 +6570,8 @@ function writeEnvFile(targetPath, envVars) {
6333
6570
  }
6334
6571
  return `${key}=${value}`;
6335
6572
  }).join("\n") + "\n";
6336
- const envPath = path12.join(targetPath, ".env");
6337
- fs11.writeFileSync(envPath, envContent, { mode: 384 });
6573
+ const envPath = path13.join(targetPath, ".env");
6574
+ fs12.writeFileSync(envPath, envContent, { mode: 384 });
6338
6575
  console.log(`[env-setup] Wrote ${Object.keys(envVars).length} env vars to ${envPath}`);
6339
6576
  }
6340
6577
  async function setupWorktreeEnv(worktreePath, apiUrl, accessToken) {
@@ -6552,7 +6789,7 @@ async function updateModuleCheckout(apiUrl, moduleId, accessToken, branchName, w
6552
6789
  }
6553
6790
 
6554
6791
  // src/commands/release.ts
6555
- var path13 = __toESM(require("path"));
6792
+ var path14 = __toESM(require("path"));
6556
6793
  var import_core12 = __toESM(require_dist());
6557
6794
  async function releaseCommand(moduleUid, options = {}) {
6558
6795
  if (!moduleUid || !moduleUid.match(/^EP\d+$/)) {
@@ -6599,7 +6836,7 @@ Commit or stash your changes first, or use --force to discard them.`
6599
6836
  );
6600
6837
  }
6601
6838
  }
6602
- const currentPath = path13.resolve(process.cwd());
6839
+ const currentPath = path14.resolve(process.cwd());
6603
6840
  if (currentPath.startsWith(existing.worktreePath)) {
6604
6841
  status.warning("You are inside the worktree being released.");
6605
6842
  status.info(`Please cd to ${projectRoot} first.`);
@@ -6673,8 +6910,8 @@ async function updateModuleRelease(apiUrl, projectId, workspaceSlug, moduleUid,
6673
6910
  }
6674
6911
 
6675
6912
  // src/commands/list.ts
6676
- var fs12 = __toESM(require("fs"));
6677
- var path14 = __toESM(require("path"));
6913
+ var fs13 = __toESM(require("fs"));
6914
+ var path15 = __toESM(require("path"));
6678
6915
  var import_chalk2 = __toESM(require("chalk"));
6679
6916
  var import_core13 = __toESM(require_dist());
6680
6917
  async function listCommand(subcommand, options = {}) {
@@ -6686,7 +6923,7 @@ async function listCommand(subcommand, options = {}) {
6686
6923
  }
6687
6924
  async function listProjects(options) {
6688
6925
  const episodaRoot = getEpisodaRoot();
6689
- if (!fs12.existsSync(episodaRoot)) {
6926
+ if (!fs13.existsSync(episodaRoot)) {
6690
6927
  status.info("No projects cloned yet.");
6691
6928
  status.info("");
6692
6929
  status.info("Clone a project with:");
@@ -6694,12 +6931,12 @@ async function listProjects(options) {
6694
6931
  return;
6695
6932
  }
6696
6933
  const projects = [];
6697
- const workspaces = fs12.readdirSync(episodaRoot, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
6934
+ const workspaces = fs13.readdirSync(episodaRoot, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
6698
6935
  for (const workspace of workspaces) {
6699
- const workspacePath = path14.join(episodaRoot, workspace.name);
6700
- const projectDirs = fs12.readdirSync(workspacePath, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
6936
+ const workspacePath = path15.join(episodaRoot, workspace.name);
6937
+ const projectDirs = fs13.readdirSync(workspacePath, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
6701
6938
  for (const projectDir of projectDirs) {
6702
- const projectPath = path14.join(workspacePath, projectDir.name);
6939
+ const projectPath = path15.join(workspacePath, projectDir.name);
6703
6940
  if (await isWorktreeProject(projectPath)) {
6704
6941
  const manager = new WorktreeManager(projectPath);
6705
6942
  await manager.initialize();
@@ -6805,7 +7042,7 @@ async function listWorktrees(options) {
6805
7042
  console.log("");
6806
7043
  console.log(import_chalk2.default.red(" Orphaned worktrees (in git but not in config):"));
6807
7044
  for (const wtPath of orphaned) {
6808
- const moduleUid = path14.basename(wtPath);
7045
+ const moduleUid = path15.basename(wtPath);
6809
7046
  console.log(import_chalk2.default.red(` \u2717 ${moduleUid} - ${wtPath}`));
6810
7047
  }
6811
7048
  console.log(import_chalk2.default.gray(" These may be from crashed sessions. Run `episoda release <uid>` to clean up"));
@@ -6955,7 +7192,7 @@ async function updateCommand(options = {}) {
6955
7192
  }
6956
7193
  if (options.restart && daemonWasRunning) {
6957
7194
  status.info("Restarting daemon...");
6958
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
7195
+ await new Promise((resolve6) => setTimeout(resolve6, 1e3));
6959
7196
  if (startDaemon2()) {
6960
7197
  status.success("\u2713 Daemon restarted");
6961
7198
  status.info("");
@@ -6999,26 +7236,26 @@ var import_child_process12 = require("child_process");
6999
7236
  var import_core15 = __toESM(require_dist());
7000
7237
 
7001
7238
  // src/utils/env-cache.ts
7002
- var fs13 = __toESM(require("fs"));
7003
- var path15 = __toESM(require("path"));
7239
+ var fs14 = __toESM(require("fs"));
7240
+ var path16 = __toESM(require("path"));
7004
7241
  var os4 = __toESM(require("os"));
7005
7242
  var DEFAULT_CACHE_TTL = 60;
7006
- var CACHE_DIR = path15.join(os4.homedir(), ".episoda", "cache");
7243
+ var CACHE_DIR = path16.join(os4.homedir(), ".episoda", "cache");
7007
7244
  function getCacheFilePath(projectId) {
7008
- return path15.join(CACHE_DIR, `env-vars-${projectId}.json`);
7245
+ return path16.join(CACHE_DIR, `env-vars-${projectId}.json`);
7009
7246
  }
7010
7247
  function ensureCacheDir() {
7011
- if (!fs13.existsSync(CACHE_DIR)) {
7012
- fs13.mkdirSync(CACHE_DIR, { recursive: true, mode: 448 });
7248
+ if (!fs14.existsSync(CACHE_DIR)) {
7249
+ fs14.mkdirSync(CACHE_DIR, { recursive: true, mode: 448 });
7013
7250
  }
7014
7251
  }
7015
7252
  function readCache(projectId) {
7016
7253
  try {
7017
7254
  const cacheFile = getCacheFilePath(projectId);
7018
- if (!fs13.existsSync(cacheFile)) {
7255
+ if (!fs14.existsSync(cacheFile)) {
7019
7256
  return null;
7020
7257
  }
7021
- const content = fs13.readFileSync(cacheFile, "utf-8");
7258
+ const content = fs14.readFileSync(cacheFile, "utf-8");
7022
7259
  const data = JSON.parse(content);
7023
7260
  if (!data.vars || typeof data.vars !== "object" || !data.fetchedAt) {
7024
7261
  return null;
@@ -7037,7 +7274,7 @@ function writeCache(projectId, vars) {
7037
7274
  fetchedAt: Date.now(),
7038
7275
  projectId
7039
7276
  };
7040
- fs13.writeFileSync(cacheFile, JSON.stringify(data, null, 2), { mode: 384 });
7277
+ fs14.writeFileSync(cacheFile, JSON.stringify(data, null, 2), { mode: 384 });
7041
7278
  } catch (error) {
7042
7279
  console.warn("[env-cache] Failed to write cache:", error instanceof Error ? error.message : error);
7043
7280
  }
@@ -7273,8 +7510,8 @@ function printExports(envVars) {
7273
7510
  }
7274
7511
 
7275
7512
  // src/commands/env.ts
7276
- var fs14 = __toESM(require("fs"));
7277
- var path16 = __toESM(require("path"));
7513
+ var fs15 = __toESM(require("fs"));
7514
+ var path17 = __toESM(require("path"));
7278
7515
  var readline = __toESM(require("readline"));
7279
7516
  var import_core17 = __toESM(require_dist());
7280
7517
  async function envListCommand(options = {}) {
@@ -7474,27 +7711,27 @@ async function envPullCommand(options = {}) {
7474
7711
  return;
7475
7712
  }
7476
7713
  const filename = options.file || ".env";
7477
- const filepath = path16.resolve(process.cwd(), filename);
7478
- fs14.writeFileSync(filepath, fullContent, { mode: 384 });
7714
+ const filepath = path17.resolve(process.cwd(), filename);
7715
+ fs15.writeFileSync(filepath, fullContent, { mode: 384 });
7479
7716
  status.success(`Wrote ${Object.keys(visibleEnvVars).length} env vars to ${filename}`);
7480
7717
  }
7481
7718
  async function promptForValue(key) {
7482
7719
  if (!process.stdin.isTTY) {
7483
- return new Promise((resolve5, reject) => {
7720
+ return new Promise((resolve6, reject) => {
7484
7721
  const rl = readline.createInterface({
7485
7722
  input: process.stdin,
7486
7723
  output: process.stdout
7487
7724
  });
7488
7725
  rl.question(`Enter value for ${key}: `, (answer) => {
7489
7726
  rl.close();
7490
- resolve5(answer);
7727
+ resolve6(answer);
7491
7728
  });
7492
7729
  rl.on("close", () => {
7493
7730
  reject(new Error("Input closed"));
7494
7731
  });
7495
7732
  });
7496
7733
  }
7497
- return new Promise((resolve5, reject) => {
7734
+ return new Promise((resolve6, reject) => {
7498
7735
  const rl = readline.createInterface({
7499
7736
  input: process.stdin,
7500
7737
  output: process.stdout
@@ -7514,7 +7751,7 @@ async function promptForValue(key) {
7514
7751
  resolved = true;
7515
7752
  cleanup();
7516
7753
  console.log("");
7517
- resolve5(value);
7754
+ resolve6(value);
7518
7755
  } else if (str === "") {
7519
7756
  cleanup();
7520
7757
  reject(new Error("Cancelled"));
@@ -7743,8 +7980,8 @@ async function migrationsFixCommand(options) {
7743
7980
  }
7744
7981
 
7745
7982
  // src/utils/legacy-channel-notice.ts
7746
- var fs15 = __toESM(require("fs"));
7747
- var path18 = __toESM(require("path"));
7983
+ var fs16 = __toESM(require("fs"));
7984
+ var path19 = __toESM(require("path"));
7748
7985
  var import_core18 = __toESM(require_dist());
7749
7986
  var LEGACY_NOTICE_FILE = "legacy-channel-notice.json";
7750
7987
  var LEGACY_NOTICE_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -7757,7 +7994,7 @@ var SUPPRESSED_COMMANDS = /* @__PURE__ */ new Set([
7757
7994
  "migrations"
7758
7995
  ]);
7759
7996
  function getNoticeFilePath() {
7760
- return path18.join((0, import_core18.getConfigDir)(), LEGACY_NOTICE_FILE);
7997
+ return path19.join((0, import_core18.getConfigDir)(), LEGACY_NOTICE_FILE);
7761
7998
  }
7762
7999
  function getPrimaryCommand(argv) {
7763
8000
  const args = argv.slice(2);
@@ -7765,7 +8002,7 @@ function getPrimaryCommand(argv) {
7765
8002
  }
7766
8003
  function readNoticeRecord() {
7767
8004
  try {
7768
- const raw = fs15.readFileSync(getNoticeFilePath(), "utf-8");
8005
+ const raw = fs16.readFileSync(getNoticeFilePath(), "utf-8");
7769
8006
  const parsed = JSON.parse(raw);
7770
8007
  if (!parsed?.legacyVersion || typeof parsed.shownAt !== "number") {
7771
8008
  return null;
@@ -7777,8 +8014,8 @@ function readNoticeRecord() {
7777
8014
  }
7778
8015
  function writeNoticeRecord(record) {
7779
8016
  try {
7780
- fs15.mkdirSync((0, import_core18.getConfigDir)(), { recursive: true });
7781
- fs15.writeFileSync(getNoticeFilePath(), JSON.stringify(record), "utf-8");
8017
+ fs16.mkdirSync((0, import_core18.getConfigDir)(), { recursive: true });
8018
+ fs16.writeFileSync(getNoticeFilePath(), JSON.stringify(record), "utf-8");
7782
8019
  } catch {
7783
8020
  }
7784
8021
  }
@@ -7874,12 +8111,12 @@ import_commander.program.command("setup").description("Register episoda:// proto
7874
8111
  process.exit(1);
7875
8112
  }
7876
8113
  });
7877
- import_commander.program.command("status").description("Show connection status").option("--verify", "Verify connection is healthy (not just connected)").option("--local", "Only check local daemon state (faster, but may be stale)").option("--skip-update-check", "Skip CLI version update check (faster)").action(async (options) => {
8114
+ import_commander.program.command("status").description("Show connection status").option("--verify", "Verify connection is healthy (not just connected)").option("--local", "Only check local daemon state (faster, but may be stale)").option("--update", "Check and apply CLI update if available (mutating)").action(async (options) => {
7878
8115
  try {
7879
8116
  await statusCommand({
7880
8117
  verify: options.verify,
7881
8118
  local: options.local,
7882
- skipUpdateCheck: options.skipUpdateCheck
8119
+ update: options.update
7883
8120
  });
7884
8121
  } catch (error) {
7885
8122
  status.error(`Status check failed: ${error instanceof Error ? error.message : String(error)}`);