episoda 0.2.46 → 0.2.48

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.
@@ -2693,7 +2693,7 @@ var require_package = __commonJS({
2693
2693
  "package.json"(exports2, module2) {
2694
2694
  module2.exports = {
2695
2695
  name: "episoda",
2696
- version: "0.2.46",
2696
+ version: "0.2.48",
2697
2697
  description: "CLI tool for Episoda local development workflow orchestration",
2698
2698
  main: "dist/index.js",
2699
2699
  types: "dist/index.d.ts",
@@ -4107,14 +4107,11 @@ var TUNNEL_PID_DIR = path7.join(os2.homedir(), ".episoda", "tunnels");
4107
4107
  var TUNNEL_TIMEOUTS = {
4108
4108
  /** Time to wait for Named Tunnel connection (includes API token fetch + connect) */
4109
4109
  NAMED_TUNNEL_CONNECT: 6e4,
4110
- /** Time to wait for Quick Tunnel connection (simpler, faster connection) */
4111
- QUICK_TUNNEL_CONNECT: 3e4,
4112
4110
  /** Time to wait for cloudflared process to start before giving up */
4113
4111
  PROCESS_START: 1e4,
4114
4112
  /** Grace period after starting cloudflared before checking status */
4115
4113
  STARTUP_GRACE: 2e3
4116
4114
  };
4117
- var TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
4118
4115
  var DEFAULT_RECONNECT_CONFIG = {
4119
4116
  maxRetries: 5,
4120
4117
  initialDelayMs: 1e3,
@@ -4569,151 +4566,21 @@ var TunnelManager = class extends import_events.EventEmitter {
4569
4566
  });
4570
4567
  }
4571
4568
  /**
4572
- * EP948: Route to the appropriate tunnel process method based on mode
4569
+ * EP948: Start tunnel process (Named Tunnels only)
4570
+ * EP1020: Removed Quick Tunnel fallback - Named Tunnels are the only supported mode
4573
4571
  */
4574
4572
  async startTunnelProcess(options, existingState) {
4575
- const mode = options.mode || "named";
4576
- if (mode === "named" && options.tunnelToken) {
4577
- return this.startNamedTunnelProcess(options, existingState);
4578
- }
4579
- console.log(`[Tunnel] EP948: Using Quick Tunnel mode for ${options.moduleUid}`);
4580
- return this.startQuickTunnelProcess(options, existingState);
4581
- }
4582
- /**
4583
- * EP672-9: Internal method to start the tunnel process (Quick Tunnel mode)
4584
- * Separated from startTunnel to support reconnection
4585
- */
4586
- async startQuickTunnelProcess(options, existingState) {
4587
- const { moduleUid, port = 3e3, onUrl, onStatusChange } = options;
4588
- if (!this.cloudflaredPath) {
4589
- try {
4590
- this.cloudflaredPath = await ensureCloudflared();
4591
- } catch (error) {
4592
- const errorMessage = error instanceof Error ? error.message : String(error);
4593
- return { success: false, error: `Failed to get cloudflared: ${errorMessage}` };
4594
- }
4595
- }
4596
- return new Promise((resolve3) => {
4597
- const tunnelInfo = {
4598
- moduleUid,
4599
- url: "",
4600
- port,
4601
- status: "starting",
4602
- startedAt: /* @__PURE__ */ new Date(),
4603
- process: null
4604
- // Will be set below
4605
- };
4606
- const process2 = (0, import_child_process6.spawn)(this.cloudflaredPath, [
4607
- "tunnel",
4608
- "--url",
4609
- `http://localhost:${port}`
4610
- ], {
4611
- stdio: ["ignore", "pipe", "pipe"]
4612
- });
4613
- tunnelInfo.process = process2;
4614
- tunnelInfo.pid = process2.pid;
4615
- if (process2.pid) {
4616
- this.writePidFile(moduleUid, process2.pid);
4617
- }
4618
- const state = existingState || {
4619
- info: tunnelInfo,
4620
- options,
4621
- intentionallyStopped: false,
4622
- retryCount: 0,
4623
- retryTimeoutId: null
4624
- };
4625
- state.info = tunnelInfo;
4626
- this.tunnelStates.set(moduleUid, state);
4627
- let urlFound = false;
4628
- let stdoutBuffer = "";
4629
- let stderrBuffer = "";
4630
- const parseOutput = (data) => {
4631
- if (urlFound) return;
4632
- const match = data.match(TUNNEL_URL_REGEX);
4633
- if (match) {
4634
- urlFound = true;
4635
- tunnelInfo.url = match[0];
4636
- tunnelInfo.status = "connected";
4637
- onStatusChange?.("connected");
4638
- onUrl?.(tunnelInfo.url);
4639
- this.emitEvent({
4640
- type: "started",
4641
- moduleUid,
4642
- url: tunnelInfo.url
4643
- });
4644
- resolve3({ success: true, url: tunnelInfo.url });
4645
- }
4573
+ if (!options.tunnelToken) {
4574
+ console.error(`[Tunnel] EP1020: No tunnel token available for ${options.moduleUid}`);
4575
+ return {
4576
+ success: false,
4577
+ error: "Named Tunnel token required. Quick Tunnels are no longer supported."
4646
4578
  };
4647
- process2.stdout?.on("data", (data) => {
4648
- stdoutBuffer += data.toString();
4649
- parseOutput(stdoutBuffer);
4650
- });
4651
- process2.stderr?.on("data", (data) => {
4652
- stderrBuffer += data.toString();
4653
- parseOutput(stderrBuffer);
4654
- });
4655
- process2.on("exit", (code, signal) => {
4656
- const wasConnected = tunnelInfo.status === "connected";
4657
- tunnelInfo.status = "disconnected";
4658
- const currentState = this.tunnelStates.get(moduleUid);
4659
- if (!urlFound) {
4660
- const errorMsg = `Tunnel process exited with code ${code}`;
4661
- tunnelInfo.status = "error";
4662
- tunnelInfo.error = errorMsg;
4663
- if (currentState && !currentState.intentionallyStopped) {
4664
- this.attemptReconnect(moduleUid);
4665
- } else {
4666
- this.tunnelStates.delete(moduleUid);
4667
- onStatusChange?.("error", errorMsg);
4668
- this.emitEvent({ type: "error", moduleUid, error: errorMsg });
4669
- }
4670
- resolve3({ success: false, error: errorMsg });
4671
- } else if (wasConnected) {
4672
- if (currentState && !currentState.intentionallyStopped) {
4673
- console.log(`[Tunnel] ${moduleUid} crashed unexpectedly, attempting reconnect...`);
4674
- onStatusChange?.("reconnecting");
4675
- this.attemptReconnect(moduleUid);
4676
- } else {
4677
- this.tunnelStates.delete(moduleUid);
4678
- onStatusChange?.("disconnected");
4679
- this.emitEvent({ type: "stopped", moduleUid });
4680
- }
4681
- }
4682
- });
4683
- process2.on("error", (error) => {
4684
- tunnelInfo.status = "error";
4685
- tunnelInfo.error = error.message;
4686
- const currentState = this.tunnelStates.get(moduleUid);
4687
- if (currentState && !currentState.intentionallyStopped) {
4688
- this.attemptReconnect(moduleUid);
4689
- } else {
4690
- this.tunnelStates.delete(moduleUid);
4691
- onStatusChange?.("error", error.message);
4692
- this.emitEvent({ type: "error", moduleUid, error: error.message });
4693
- }
4694
- if (!urlFound) {
4695
- resolve3({ success: false, error: error.message });
4696
- }
4697
- });
4698
- setTimeout(() => {
4699
- if (!urlFound) {
4700
- process2.kill();
4701
- const errorMsg = "Tunnel startup timed out after 30 seconds";
4702
- tunnelInfo.status = "error";
4703
- tunnelInfo.error = errorMsg;
4704
- const currentState = this.tunnelStates.get(moduleUid);
4705
- if (currentState && !currentState.intentionallyStopped) {
4706
- this.attemptReconnect(moduleUid);
4707
- } else {
4708
- this.tunnelStates.delete(moduleUid);
4709
- onStatusChange?.("error", errorMsg);
4710
- this.emitEvent({ type: "error", moduleUid, error: errorMsg });
4711
- }
4712
- resolve3({ success: false, error: errorMsg });
4713
- }
4714
- }, TUNNEL_TIMEOUTS.QUICK_TUNNEL_CONNECT);
4715
- });
4579
+ }
4580
+ return this.startNamedTunnelProcess(options, existingState);
4716
4581
  }
4582
+ // EP1020: startQuickTunnelProcess removed - Quick Tunnels no longer supported
4583
+ // All tunnels now use Named Tunnels via Cloudflare API
4717
4584
  /**
4718
4585
  * Start a tunnel for a module
4719
4586
  *
@@ -4782,8 +4649,11 @@ var TunnelManager = class extends import_events.EventEmitter {
4782
4649
  previewUrl: provisionResult.tunnel.preview_url
4783
4650
  };
4784
4651
  } else {
4785
- console.warn(`[Tunnel] EP948: Named Tunnel provisioning failed: ${provisionResult.error}. Falling back to Quick Tunnel.`);
4786
- resolvedOptions = { ...resolvedOptions, mode: "quick" };
4652
+ console.error(`[Tunnel] EP1020: Named Tunnel provisioning failed for ${moduleUid}: ${provisionResult.error}`);
4653
+ return {
4654
+ success: false,
4655
+ error: `Named Tunnel provisioning failed: ${provisionResult.error}`
4656
+ };
4787
4657
  }
4788
4658
  }
4789
4659
  return this.startTunnelProcess(resolvedOptions);
@@ -7640,9 +7510,7 @@ var Daemon = class _Daemon {
7640
7510
  // EP833: Track consecutive health check failures per tunnel
7641
7511
  this.tunnelHealthFailures = /* @__PURE__ */ new Map();
7642
7512
  // 3 second timeout for health checks
7643
- // EP911: Track last reported health status to avoid unnecessary DB writes
7644
- this.lastReportedHealthStatus = /* @__PURE__ */ new Map();
7645
- // moduleUid -> status
7513
+ // EP1020: lastReportedHealthStatus removed - health columns dropped from database
7646
7514
  // EP837: Prevent concurrent commit syncs (backpressure guard)
7647
7515
  this.commitSyncInProgress = false;
7648
7516
  // EP843: Per-module mutex for tunnel operations
@@ -7874,7 +7742,6 @@ var Daemon = class _Daemon {
7874
7742
  await tunnelManager.stopTunnel(moduleUid);
7875
7743
  await stopDevServer(moduleUid);
7876
7744
  await clearTunnelUrl(moduleUid);
7877
- this.lastReportedHealthStatus.delete(moduleUid);
7878
7745
  this.tunnelHealthFailures.delete(moduleUid);
7879
7746
  console.log(`[Daemon] EP823: Tunnel stopped for ${moduleUid}`);
7880
7747
  return { success: true };
@@ -9191,7 +9058,6 @@ var Daemon = class _Daemon {
9191
9058
  const isHealthy = await this.checkTunnelHealth(tunnel);
9192
9059
  if (isHealthy) {
9193
9060
  this.tunnelHealthFailures.delete(tunnel.moduleUid);
9194
- await this.reportTunnelHealth(tunnel.moduleUid, "healthy", config);
9195
9061
  } else {
9196
9062
  const failures = (this.tunnelHealthFailures.get(tunnel.moduleUid) || 0) + 1;
9197
9063
  this.tunnelHealthFailures.set(tunnel.moduleUid, failures);
@@ -9202,7 +9068,6 @@ var Daemon = class _Daemon {
9202
9068
  await this.restartTunnel(tunnel.moduleUid, tunnel.port);
9203
9069
  });
9204
9070
  this.tunnelHealthFailures.delete(tunnel.moduleUid);
9205
- await this.reportTunnelHealth(tunnel.moduleUid, "unhealthy", config);
9206
9071
  }
9207
9072
  }
9208
9073
  }
@@ -9265,18 +9130,6 @@ var Daemon = class _Daemon {
9265
9130
  const result2 = await previewManager.restartPreview(moduleUid);
9266
9131
  if (result2.success && result2.previewUrl) {
9267
9132
  console.log(`[Daemon] EP833: Preview restarted for ${moduleUid}: ${result2.previewUrl}`);
9268
- try {
9269
- await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}/tunnel`, {
9270
- method: "POST",
9271
- body: JSON.stringify({
9272
- tunnel_url: result2.previewUrl,
9273
- tunnel_error: null,
9274
- restart_reason: "health_check_failure"
9275
- })
9276
- });
9277
- } catch (e) {
9278
- console.warn(`[Daemon] EP833: Failed to report restarted tunnel URL`);
9279
- }
9280
9133
  } else {
9281
9134
  console.error(`[Daemon] EP833: Preview restart failed for ${moduleUid}: ${result2.error}`);
9282
9135
  }
@@ -9301,18 +9154,6 @@ var Daemon = class _Daemon {
9301
9154
  });
9302
9155
  if (result.success && result.previewUrl) {
9303
9156
  console.log(`[Daemon] EP833: Preview started for ${moduleUid}: ${result.previewUrl}`);
9304
- try {
9305
- await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}/tunnel`, {
9306
- method: "POST",
9307
- body: JSON.stringify({
9308
- tunnel_url: result.previewUrl,
9309
- tunnel_error: null,
9310
- restart_reason: "health_check_failure"
9311
- })
9312
- });
9313
- } catch (e) {
9314
- console.warn(`[Daemon] EP833: Failed to report restarted tunnel URL`);
9315
- }
9316
9157
  } else {
9317
9158
  console.error(`[Daemon] EP833: Preview start failed for ${moduleUid}: ${result.error}`);
9318
9159
  }
@@ -9320,33 +9161,8 @@ var Daemon = class _Daemon {
9320
9161
  console.error(`[Daemon] EP833: Error restarting preview for ${moduleUid}:`, error);
9321
9162
  }
9322
9163
  }
9323
- /**
9324
- * EP833: Report tunnel health status to the API
9325
- * EP904: Use fetchWithAuth for token refresh
9326
- * EP911: Only report when status CHANGES to reduce DB writes
9327
- */
9328
- async reportTunnelHealth(moduleUid, healthStatus, config) {
9329
- if (!config.access_token) {
9330
- return;
9331
- }
9332
- const lastStatus = this.lastReportedHealthStatus.get(moduleUid);
9333
- if (lastStatus === healthStatus) {
9334
- return;
9335
- }
9336
- const apiUrl = config.api_url || "https://episoda.dev";
9337
- try {
9338
- await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}/health`, {
9339
- method: "PATCH",
9340
- body: JSON.stringify({
9341
- tunnel_health_status: healthStatus,
9342
- tunnel_last_health_check: (/* @__PURE__ */ new Date()).toISOString()
9343
- })
9344
- });
9345
- this.lastReportedHealthStatus.set(moduleUid, healthStatus);
9346
- } catch (error) {
9347
- console.warn(`[Daemon] EP833: Failed to report health for ${moduleUid}:`, error instanceof Error ? error.message : error);
9348
- }
9349
- }
9164
+ // EP1020: reportTunnelHealth() removed - tunnel_health_status and tunnel_last_health_check columns dropped
9165
+ // Health checking (checkTunnelHealth) and auto-restart logic preserved for detecting dead tunnels
9350
9166
  /**
9351
9167
  * EP833: Kill processes matching a pattern
9352
9168
  * Used to clean up orphaned cloudflared processes