@episoda/cli 0.2.209 → 0.2.211

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
@@ -3093,6 +3093,7 @@ var status = {
3093
3093
  var fs2 = __toESM(require("fs"));
3094
3094
  var path3 = __toESM(require("path"));
3095
3095
  var net2 = __toESM(require("net"));
3096
+ var http = __toESM(require("http"));
3096
3097
  var import_child_process2 = require("child_process");
3097
3098
  var import_crypto = require("crypto");
3098
3099
  var import_core3 = __toESM(require_dist());
@@ -3375,6 +3376,8 @@ function getSocketFilePath() {
3375
3376
  function getProjectsFilePath() {
3376
3377
  return path3.join((0, import_core3.getConfigDir)(), "projects.json");
3377
3378
  }
3379
+ var DAEMON_HEALTH_PORT = 9999;
3380
+ var DAEMON_REUSE_LOG = "Daemon already running, reusing existing connection";
3378
3381
  function removeTransientDaemonArtifacts() {
3379
3382
  const removed = [];
3380
3383
  const runtimePaths = [
@@ -3522,6 +3525,60 @@ async function waitForDaemonIpcReady(timeoutMs = 15e3) {
3522
3525
  }
3523
3526
  throw new Error(`Daemon IPC not ready within ${Math.round(timeoutMs / 1e3)}s (${lastError})`);
3524
3527
  }
3528
+ async function pingDaemonHealthEndpoint(timeoutMs) {
3529
+ return await new Promise((resolve10) => {
3530
+ const request = http.get({
3531
+ host: "127.0.0.1",
3532
+ port: DAEMON_HEALTH_PORT,
3533
+ path: "/health",
3534
+ timeout: timeoutMs
3535
+ }, (response) => {
3536
+ let buffer = "";
3537
+ response.setEncoding("utf8");
3538
+ response.on("data", (chunk) => {
3539
+ buffer += chunk;
3540
+ });
3541
+ response.on("end", () => {
3542
+ if (response.statusCode !== 200) {
3543
+ resolve10(false);
3544
+ return;
3545
+ }
3546
+ try {
3547
+ const payload = JSON.parse(buffer);
3548
+ resolve10(payload.connected === true || payload.status === "healthy");
3549
+ } catch {
3550
+ resolve10(false);
3551
+ }
3552
+ });
3553
+ });
3554
+ request.on("timeout", () => {
3555
+ request.destroy();
3556
+ resolve10(false);
3557
+ });
3558
+ request.on("error", () => {
3559
+ resolve10(false);
3560
+ });
3561
+ });
3562
+ }
3563
+ async function findHealthyDaemonInstance() {
3564
+ const existingPid = isDaemonRunning();
3565
+ if (existingPid) {
3566
+ return { found: true, pid: existingPid };
3567
+ }
3568
+ const socketPath = getSocketFilePath();
3569
+ if (fs2.existsSync(socketPath)) {
3570
+ try {
3571
+ await pingDaemonSocket(socketPath, 1e3);
3572
+ return { found: true, pid: isDaemonRunning() };
3573
+ } catch {
3574
+ }
3575
+ }
3576
+ const healthEndpointHealthy = await pingDaemonHealthEndpoint(1e3);
3577
+ if (healthEndpointHealthy) {
3578
+ return { found: true, pid: isDaemonRunning() };
3579
+ }
3580
+ return { found: false, pid: null };
3581
+ }
3525
3582
  async function waitForProcessStart(pid, timeoutMs = 5e3) {
3526
3583
  const deadline = Date.now() + timeoutMs;
3527
3584
  while (Date.now() < deadline) {
@@ -3587,9 +3644,10 @@ function getInstallChannelEnvValue() {
3587
3644
  async function startDaemon(options = {}) {
3588
3645
  const startTime = Date.now();
3589
3646
  const startReason = options.reason ?? "manual_start";
3590
- const existingPid = isDaemonRunning();
3591
- if (existingPid) {
3592
- throw new Error(`Daemon already running (PID: ${existingPid})`);
3647
+ const reusableDaemon = await findHealthyDaemonInstance();
3648
+ if (reusableDaemon.found) {
3649
+ console.log(DAEMON_REUSE_LOG);
3650
+ return reusableDaemon.pid;
3593
3651
  }
3594
3652
  const removedArtifacts = removeTransientDaemonArtifacts();
3595
3653
  if (removedArtifacts.length > 0) {
@@ -5087,13 +5145,21 @@ async function daemonCommand(options = {}) {
5087
5145
  }
5088
5146
  let daemonPid = isDaemonRunning();
5089
5147
  if (!daemonPid) {
5090
- status.info("Starting Episoda daemon...");
5091
- try {
5092
- daemonPid = await startDaemon({ reason: needsRestart ? "ipc_reconnect" : "manual_start" });
5093
- status.success(`Daemon started (PID: ${daemonPid})`);
5094
- } catch (error) {
5095
- status.error(`Failed to start daemon: ${error instanceof Error ? error.message : String(error)}`);
5096
- process.exit(1);
5148
+ const reusableDaemon = await findHealthyDaemonInstance();
5149
+ if (reusableDaemon.found) {
5150
+ console.log("Daemon already running, reusing existing connection");
5151
+ daemonPid = reusableDaemon.pid;
5152
+ } else {
5153
+ status.info("Starting Episoda daemon...");
5154
+ try {
5155
+ daemonPid = await startDaemon({ reason: needsRestart ? "ipc_reconnect" : "manual_start" });
5156
+ if (daemonPid) {
5157
+ status.success(`Daemon started (PID: ${daemonPid})`);
5158
+ }
5159
+ } catch (error) {
5160
+ status.error(`Failed to start daemon: ${error instanceof Error ? error.message : String(error)}`);
5161
+ process.exit(1);
5162
+ }
5097
5163
  }
5098
5164
  } else {
5099
5165
  status.debug(`Daemon already running (PID: ${daemonPid})`);
@@ -7468,7 +7534,9 @@ async function restartCommand(options = {}) {
7468
7534
  status.info("Daemon is not running; starting a fresh daemon...");
7469
7535
  }
7470
7536
  const pid = await startDaemon({ reason: wasRunning ? "manual_restart" : "manual_start" });
7471
- status.success(`Daemon running (PID: ${pid})`);
7537
+ if (pid) {
7538
+ status.success(`Daemon running (PID: ${pid})`);
7539
+ }
7472
7540
  }
7473
7541
 
7474
7542
  // src/commands/reset.ts
@@ -7544,8 +7612,16 @@ async function attachCommand(options = {}) {
7544
7612
  const startDir = options.path ? path19.resolve(options.path) : process.cwd();
7545
7613
  const projectPath = await resolveCurrentEpisodaProjectPath(startDir);
7546
7614
  if (!isDaemonRunning()) {
7547
- status.info("Starting Episoda daemon...");
7548
- await startDaemon({ reason: "manual_attach" });
7615
+ const reusableDaemon = await findHealthyDaemonInstance();
7616
+ if (reusableDaemon.found) {
7617
+ console.log("Daemon already running, reusing existing connection");
7618
+ } else {
7619
+ status.info("Starting Episoda daemon...");
7620
+ const pid = await startDaemon({ reason: "manual_attach" });
7621
+ if (pid) {
7622
+ status.success(`Daemon started (PID: ${pid})`);
7623
+ }
7624
+ }
7549
7625
  } else if (!await isDaemonReachable()) {
7550
7626
  status.error("Daemon is running but not reachable. Run `episoda restart`.");
7551
7627
  process.exit(1);