episoda 0.2.14 → 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.
@@ -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) {
@@ -1946,6 +1958,10 @@ var require_websocket_client = __commonJS({
1946
1958
  console.log("[EpisodaClient] Intentional disconnect - not reconnecting");
1947
1959
  return;
1948
1960
  }
1961
+ if (this.reconnectTimeout) {
1962
+ console.log("[EpisodaClient] Reconnection already scheduled, skipping duplicate");
1963
+ return;
1964
+ }
1949
1965
  if (this.heartbeatTimer) {
1950
1966
  clearInterval(this.heartbeatTimer);
1951
1967
  this.heartbeatTimer = void 0;
@@ -2264,7 +2280,7 @@ var require_package = __commonJS({
2264
2280
  "package.json"(exports2, module2) {
2265
2281
  module2.exports = {
2266
2282
  name: "episoda",
2267
- version: "0.2.13",
2283
+ version: "0.2.14",
2268
2284
  description: "CLI tool for Episoda local development workflow orchestration",
2269
2285
  main: "dist/index.js",
2270
2286
  types: "dist/index.d.ts",
@@ -3156,6 +3172,10 @@ var Daemon = class {
3156
3172
  // Updated by 'auth_success' (add) and 'disconnected' (remove) events
3157
3173
  this.liveConnections = /* @__PURE__ */ new Set();
3158
3174
  // projectPath
3175
+ // EP813: Track connections that are still authenticating (in progress)
3176
+ // Prevents race condition between restoreConnections() and add-project IPC
3177
+ this.pendingConnections = /* @__PURE__ */ new Set();
3178
+ // projectPath
3159
3179
  this.shuttingDown = false;
3160
3180
  this.ipcServer = new IPCServer();
3161
3181
  }
@@ -3224,18 +3244,31 @@ var Daemon = class {
3224
3244
  this.ipcServer.on("add-project", async (params) => {
3225
3245
  const { projectId, projectPath } = params;
3226
3246
  addProject(projectId, projectPath);
3227
- try {
3228
- await this.connectProject(projectId, projectPath);
3229
- const isHealthy = this.isConnectionHealthy(projectPath);
3230
- if (!isHealthy) {
3231
- console.warn(`[Daemon] Connection completed but not healthy for ${projectPath}`);
3232
- return { success: false, connected: false, error: "Connection established but not healthy" };
3247
+ const MAX_RETRIES = 3;
3248
+ const INITIAL_DELAY = 1e3;
3249
+ let lastError = "";
3250
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
3251
+ try {
3252
+ await this.connectProject(projectId, projectPath);
3253
+ const isHealthy = this.isConnectionHealthy(projectPath);
3254
+ if (!isHealthy) {
3255
+ console.warn(`[Daemon] Connection completed but not healthy for ${projectPath}`);
3256
+ lastError = "Connection established but not healthy";
3257
+ return { success: false, connected: false, error: lastError };
3258
+ }
3259
+ return { success: true, connected: true };
3260
+ } catch (error) {
3261
+ lastError = error instanceof Error ? error.message : String(error);
3262
+ console.error(`[Daemon] Connection attempt ${attempt}/${MAX_RETRIES} failed:`, lastError);
3263
+ if (attempt < MAX_RETRIES) {
3264
+ const delay = INITIAL_DELAY * Math.pow(2, attempt - 1);
3265
+ console.log(`[Daemon] Retrying in ${delay / 1e3}s...`);
3266
+ await new Promise((resolve2) => setTimeout(resolve2, delay));
3267
+ await this.disconnectProject(projectPath);
3268
+ }
3233
3269
  }
3234
- return { success: true, connected: true };
3235
- } catch (error) {
3236
- const errorMessage = error instanceof Error ? error.message : String(error);
3237
- return { success: false, connected: false, error: errorMessage };
3238
3270
  }
3271
+ return { success: false, connected: false, error: `Failed after ${MAX_RETRIES} attempts: ${lastError}` };
3239
3272
  });
3240
3273
  this.ipcServer.on("remove-project", async (params) => {
3241
3274
  const { projectPath } = params;
@@ -3310,6 +3343,19 @@ var Daemon = class {
3310
3343
  console.log(`[Daemon] Already connected to ${projectPath}`);
3311
3344
  return;
3312
3345
  }
3346
+ if (this.pendingConnections.has(projectPath)) {
3347
+ console.log(`[Daemon] Connection in progress for ${projectPath}, waiting...`);
3348
+ const maxWait = 35e3;
3349
+ const startTime = Date.now();
3350
+ while (this.pendingConnections.has(projectPath) && Date.now() - startTime < maxWait) {
3351
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
3352
+ }
3353
+ if (this.liveConnections.has(projectPath)) {
3354
+ console.log(`[Daemon] Pending connection succeeded for ${projectPath}`);
3355
+ return;
3356
+ }
3357
+ console.warn(`[Daemon] Pending connection timed out for ${projectPath}`);
3358
+ }
3313
3359
  console.warn(`[Daemon] Stale connection detected for ${projectPath}, forcing reconnection`);
3314
3360
  await this.disconnectProject(projectPath);
3315
3361
  }
@@ -3336,6 +3382,7 @@ var Daemon = class {
3336
3382
  gitExecutor
3337
3383
  };
3338
3384
  this.connections.set(projectPath, connection);
3385
+ this.pendingConnections.add(projectPath);
3339
3386
  client.on("command", async (message) => {
3340
3387
  if (message.type === "command" && message.command) {
3341
3388
  console.log(`[Daemon] Received command for ${projectId}:`, message.command);
@@ -3431,6 +3478,7 @@ var Daemon = class {
3431
3478
  console.log(`[Daemon] Authenticated for project ${projectId}`);
3432
3479
  touchProject(projectPath);
3433
3480
  this.liveConnections.add(projectPath);
3481
+ this.pendingConnections.delete(projectPath);
3434
3482
  const authMessage = message;
3435
3483
  if (authMessage.userId && authMessage.workspaceId) {
3436
3484
  await this.configureGitUser(projectPath, authMessage.userId, authMessage.workspaceId, this.machineId, projectId, authMessage.deviceId);
@@ -3474,9 +3522,9 @@ var Daemon = class {
3474
3522
  console.warn(`[Daemon] Could not read daemon PID:`, pidError instanceof Error ? pidError.message : pidError);
3475
3523
  }
3476
3524
  const authSuccessPromise = new Promise((resolve2, reject) => {
3477
- const AUTH_TIMEOUT = 1e4;
3525
+ const AUTH_TIMEOUT = 3e4;
3478
3526
  const timeout = setTimeout(() => {
3479
- reject(new Error("Authentication timeout - server did not respond with auth_success"));
3527
+ reject(new Error("Authentication timeout after 30s - server may be under heavy load. Try again in a few seconds."));
3480
3528
  }, AUTH_TIMEOUT);
3481
3529
  const authHandler = () => {
3482
3530
  clearTimeout(timeout);
@@ -3502,6 +3550,7 @@ var Daemon = class {
3502
3550
  } catch (error) {
3503
3551
  console.error(`[Daemon] Failed to connect to ${projectId}:`, error);
3504
3552
  this.connections.delete(projectPath);
3553
+ this.pendingConnections.delete(projectPath);
3505
3554
  throw error;
3506
3555
  }
3507
3556
  }
@@ -3519,6 +3568,7 @@ var Daemon = class {
3519
3568
  await connection.client.disconnect();
3520
3569
  this.connections.delete(projectPath);
3521
3570
  this.liveConnections.delete(projectPath);
3571
+ this.pendingConnections.delete(projectPath);
3522
3572
  console.log(`[Daemon] Disconnected from ${projectPath}`);
3523
3573
  }
3524
3574
  /**