@episoda/cli 0.2.209 → 0.2.210
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/daemon/daemon-process.js +1 -1
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +89 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
3591
|
-
if (
|
|
3592
|
-
|
|
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
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
}
|
|
5095
|
-
status.
|
|
5096
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7548
|
-
|
|
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);
|