@episoda/cli 0.2.151 → 0.2.152

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.
@@ -2604,7 +2604,7 @@ var require_auth = __commonJS({
2604
2604
  };
2605
2605
  })();
2606
2606
  Object.defineProperty(exports2, "__esModule", { value: true });
2607
- exports2.getConfigDir = getConfigDir8;
2607
+ exports2.getConfigDir = getConfigDir9;
2608
2608
  exports2.getConfigPath = getConfigPath;
2609
2609
  exports2.loadConfig = loadConfig9;
2610
2610
  exports2.saveConfig = saveConfig2;
@@ -2616,14 +2616,14 @@ var require_auth = __commonJS({
2616
2616
  var DEFAULT_CONFIG_FILE = "config.json";
2617
2617
  var hasWarnedMissingProjectId = false;
2618
2618
  var hasWarnedMissingRequiredFields = false;
2619
- function getConfigDir8() {
2619
+ function getConfigDir9() {
2620
2620
  return process.env.EPISODA_CONFIG_DIR || path25.join(os10.homedir(), ".episoda");
2621
2621
  }
2622
2622
  function getConfigPath(configPath) {
2623
2623
  if (configPath) {
2624
2624
  return configPath;
2625
2625
  }
2626
- return path25.join(getConfigDir8(), DEFAULT_CONFIG_FILE);
2626
+ return path25.join(getConfigDir9(), DEFAULT_CONFIG_FILE);
2627
2627
  }
2628
2628
  function ensureConfigDir(configPath) {
2629
2629
  const dir = path25.dirname(configPath);
@@ -2913,7 +2913,7 @@ var require_package = __commonJS({
2913
2913
  "package.json"(exports2, module2) {
2914
2914
  module2.exports = {
2915
2915
  name: "@episoda/cli",
2916
- version: "0.2.151",
2916
+ version: "0.2.152",
2917
2917
  description: "CLI tool for Episoda local development workflow orchestration",
2918
2918
  main: "dist/index.js",
2919
2919
  types: "dist/index.d.ts",
@@ -3178,6 +3178,7 @@ var import_core3 = __toESM(require_dist());
3178
3178
  function getPidFilePath() {
3179
3179
  return path3.join((0, import_core3.getConfigDir)(), "daemon.pid");
3180
3180
  }
3181
+ var MAX_LOG_SIZE_BYTES = 10 * 1024 * 1024;
3181
3182
 
3182
3183
  // src/ipc/ipc-server.ts
3183
3184
  var net = __toESM(require("net"));
@@ -10406,7 +10407,7 @@ var path21 = __toESM(require("path"));
10406
10407
  var MAX_RESTART_ATTEMPTS = 5;
10407
10408
  var INITIAL_RESTART_DELAY_MS = 2e3;
10408
10409
  var MAX_RESTART_DELAY_MS = 3e4;
10409
- var MAX_LOG_SIZE_BYTES = 5 * 1024 * 1024;
10410
+ var MAX_LOG_SIZE_BYTES2 = 5 * 1024 * 1024;
10410
10411
  var NODE_MEMORY_LIMIT_MB = 2048;
10411
10412
  var activeServers = /* @__PURE__ */ new Map();
10412
10413
  function getLogsDir() {
@@ -10423,7 +10424,7 @@ function rotateLogIfNeeded(logPath) {
10423
10424
  try {
10424
10425
  if (fs20.existsSync(logPath)) {
10425
10426
  const stats = fs20.statSync(logPath);
10426
- if (stats.size > MAX_LOG_SIZE_BYTES) {
10427
+ if (stats.size > MAX_LOG_SIZE_BYTES2) {
10427
10428
  const backupPath = `${logPath}.1`;
10428
10429
  if (fs20.existsSync(backupPath)) {
10429
10430
  fs20.unlinkSync(backupPath);
@@ -11203,8 +11204,8 @@ var Daemon = class _Daemon {
11203
11204
  }
11204
11205
  console.log(`[Daemon] EP1324: Update to ${targetVersion} installed, restarting daemon...`);
11205
11206
  await this.shutdown();
11206
- const { getConfigDir: getConfigDir8 } = require_dist();
11207
- const configDir = getConfigDir8();
11207
+ const { getConfigDir: getConfigDir9 } = require_dist();
11208
+ const configDir = getConfigDir9();
11208
11209
  const logPath = path24.join(configDir, "daemon.log");
11209
11210
  const logFd = fs23.openSync(logPath, "a");
11210
11211
  const child = (0, import_child_process14.spawn)("node", [__filename], {
@@ -11499,16 +11500,29 @@ var Daemon = class _Daemon {
11499
11500
  "Content-Type": "application/json",
11500
11501
  "X-Project-Id": projectId
11501
11502
  };
11502
- const response = await fetch(`${config.api_url}/api/cli/status`, { headers });
11503
- if (response.ok) {
11504
- const data = await response.json();
11505
- serverConnected = data.connected === true;
11506
- serverMachineId = data.machine_id || null;
11507
- } else {
11508
- serverError = `Server returned ${response.status}`;
11503
+ const controller = new AbortController();
11504
+ const timeoutId = setTimeout(() => controller.abort(), 8e3);
11505
+ try {
11506
+ const response = await fetch(`${config.api_url}/api/cli/status`, {
11507
+ headers,
11508
+ signal: controller.signal
11509
+ });
11510
+ if (response.ok) {
11511
+ const data = await response.json();
11512
+ serverConnected = data.connected === true;
11513
+ serverMachineId = data.machine_id || null;
11514
+ } else {
11515
+ serverError = `Server returned ${response.status}`;
11516
+ }
11517
+ } finally {
11518
+ clearTimeout(timeoutId);
11509
11519
  }
11510
11520
  } catch (err) {
11511
- serverError = err instanceof Error ? err.message : "Network error";
11521
+ if (err instanceof Error && err.name === "AbortError") {
11522
+ serverError = "Server verification timed out (8s)";
11523
+ } else {
11524
+ serverError = err instanceof Error ? err.message : "Network error";
11525
+ }
11512
11526
  }
11513
11527
  const machineMatch = serverMachineId === this.machineId;
11514
11528
  return {
@@ -12412,23 +12426,6 @@ var Daemon = class _Daemon {
12412
12426
  } catch (pidError) {
12413
12427
  console.warn(`[Daemon] Could not read daemon PID:`, pidError instanceof Error ? pidError.message : pidError);
12414
12428
  }
12415
- const authSuccessPromise = new Promise((resolve4, reject) => {
12416
- const AUTH_TIMEOUT = 3e4;
12417
- const timeout = setTimeout(() => {
12418
- reject(new Error("Authentication timeout after 30s - server may be under heavy load. Try again in a few seconds."));
12419
- }, AUTH_TIMEOUT);
12420
- const authHandler = () => {
12421
- clearTimeout(timeout);
12422
- resolve4();
12423
- };
12424
- client.once("auth_success", authHandler);
12425
- const errorHandler = (message) => {
12426
- clearTimeout(timeout);
12427
- const errorMsg = message;
12428
- reject(new Error(errorMsg.message || "Authentication failed"));
12429
- };
12430
- client.once("auth_error", errorHandler);
12431
- });
12432
12429
  const modeConfig = getDaemonModeConfig();
12433
12430
  const environment = modeConfig.mode;
12434
12431
  const containerId = process.env.EPISODA_CONTAINER_ID;
@@ -12441,7 +12438,36 @@ var Daemon = class _Daemon {
12441
12438
  containerId
12442
12439
  });
12443
12440
  console.log(`[Daemon] Successfully connected to project ${projectId}`);
12444
- await authSuccessPromise;
12441
+ const AUTH_TIMEOUT = 3e4;
12442
+ await new Promise((resolve4, reject) => {
12443
+ let settled = false;
12444
+ const cleanup = () => {
12445
+ clearTimeout(timeout);
12446
+ client.off("auth_success", authHandler);
12447
+ client.off("auth_error", errorHandler);
12448
+ };
12449
+ const timeout = setTimeout(() => {
12450
+ if (settled) return;
12451
+ settled = true;
12452
+ cleanup();
12453
+ reject(new Error("Authentication timeout after 30s - server may be under heavy load. Try again in a few seconds."));
12454
+ }, AUTH_TIMEOUT);
12455
+ const authHandler = () => {
12456
+ if (settled) return;
12457
+ settled = true;
12458
+ cleanup();
12459
+ resolve4();
12460
+ };
12461
+ const errorHandler = (message) => {
12462
+ if (settled) return;
12463
+ settled = true;
12464
+ cleanup();
12465
+ const errorMsg = message;
12466
+ reject(new Error(errorMsg.message || "Authentication failed"));
12467
+ };
12468
+ client.on("auth_success", authHandler);
12469
+ client.on("auth_error", errorHandler);
12470
+ });
12445
12471
  console.log(`[Daemon] Authentication complete for project ${projectId}`);
12446
12472
  } catch (error) {
12447
12473
  console.error(`[Daemon] Failed to connect to ${projectId}:`, error);
@@ -13234,6 +13260,7 @@ var Daemon = class _Daemon {
13234
13260
  this.healthCheckCounter = 0;
13235
13261
  await this.auditWorktreesOnStartup();
13236
13262
  }
13263
+ this.checkAndRotateLog();
13237
13264
  } catch (error) {
13238
13265
  console.error("[Daemon] EP929: Health check error:", error instanceof Error ? error.message : error);
13239
13266
  } finally {
@@ -13251,6 +13278,50 @@ var Daemon = class _Daemon {
13251
13278
  console.log("[Daemon] EP929: Health check polling stopped");
13252
13279
  }
13253
13280
  }
13281
+ static {
13282
+ /**
13283
+ * EP1351: Check daemon.log size and rotate if it exceeds the limit.
13284
+ *
13285
+ * Since the daemon's stdout/stderr are piped to daemon.log via an open FD,
13286
+ * we cannot simply rename the file (the FD follows the inode). Instead:
13287
+ * 1. Copy the current log to daemon.log.1 (shift existing rotated files)
13288
+ * 2. Truncate daemon.log in-place (the open FD continues writing to offset 0)
13289
+ *
13290
+ * This means a small window of duplicate data between .1 and the truncated file,
13291
+ * but that's acceptable for diagnostics logs.
13292
+ */
13293
+ this.MAX_LOG_SIZE_BYTES = 10 * 1024 * 1024;
13294
+ }
13295
+ static {
13296
+ // 10 MB
13297
+ this.MAX_LOG_FILES = 3;
13298
+ }
13299
+ checkAndRotateLog() {
13300
+ try {
13301
+ const configDir = (0, import_core14.getConfigDir)();
13302
+ const logPath = path24.join(configDir, "daemon.log");
13303
+ if (!fs23.existsSync(logPath)) return;
13304
+ const stats = fs23.statSync(logPath);
13305
+ if (stats.size < _Daemon.MAX_LOG_SIZE_BYTES) return;
13306
+ console.log(`[Daemon] EP1351: Log rotation triggered (size: ${Math.round(stats.size / 1024 / 1024)}MB)`);
13307
+ const oldestPath = `${logPath}.${_Daemon.MAX_LOG_FILES}`;
13308
+ if (fs23.existsSync(oldestPath)) {
13309
+ fs23.unlinkSync(oldestPath);
13310
+ }
13311
+ for (let i = _Daemon.MAX_LOG_FILES - 1; i >= 1; i--) {
13312
+ const src = `${logPath}.${i}`;
13313
+ const dst = `${logPath}.${i + 1}`;
13314
+ if (fs23.existsSync(src)) {
13315
+ fs23.renameSync(src, dst);
13316
+ }
13317
+ }
13318
+ fs23.copyFileSync(logPath, `${logPath}.1`);
13319
+ fs23.truncateSync(logPath, 0);
13320
+ console.log("[Daemon] EP1351: Log rotated successfully");
13321
+ } catch (error) {
13322
+ console.warn("[Daemon] EP1351: Log rotation failed:", error instanceof Error ? error.message : error);
13323
+ }
13324
+ }
13254
13325
  /**
13255
13326
  * EP822: Clean up orphaned tunnels from previous daemon runs
13256
13327
  * EP904: Enhanced to aggressively clean ALL cloudflared processes then restart
@@ -13773,6 +13844,21 @@ var Daemon = class _Daemon {
13773
13844
  process.exit(0);
13774
13845
  }
13775
13846
  };
13847
+ var daemonStartedAt = Date.now();
13848
+ function logCrashDiagnostics(label, error) {
13849
+ const uptimeMs = Date.now() - daemonStartedAt;
13850
+ const mem = process.memoryUsage();
13851
+ console.error(`[Daemon] ${label}:`, error);
13852
+ console.error(`[Daemon] Crash diagnostics: uptime=${Math.round(uptimeMs / 1e3)}s, rss=${Math.round(mem.rss / 1024 / 1024)}MB, heap=${Math.round(mem.heapUsed / 1024 / 1024)}/${Math.round(mem.heapTotal / 1024 / 1024)}MB`);
13853
+ }
13854
+ process.on("unhandledRejection", (reason) => {
13855
+ logCrashDiagnostics("Unhandled rejection", reason);
13856
+ process.exit(1);
13857
+ });
13858
+ process.on("uncaughtException", (error) => {
13859
+ logCrashDiagnostics("Uncaught exception", error);
13860
+ process.exit(1);
13861
+ });
13776
13862
  async function main() {
13777
13863
  if (!process.env.EPISODA_DAEMON_MODE) {
13778
13864
  console.error("This script should only be run by daemon-manager");