@episoda/cli 0.2.175 → 0.2.176
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 +446 -118
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +375 -138
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -2187,6 +2187,7 @@ var require_websocket_client = __commonJS({
|
|
|
2187
2187
|
constructor() {
|
|
2188
2188
|
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
2189
2189
|
this.reconnectAttempts = 0;
|
|
2190
|
+
this.autoReconnectEnabled = true;
|
|
2190
2191
|
this.url = "";
|
|
2191
2192
|
this.token = "";
|
|
2192
2193
|
this.isConnected = false;
|
|
@@ -2235,7 +2236,7 @@ var require_websocket_client = __commonJS({
|
|
|
2235
2236
|
clearTimeout(this.reconnectTimeout);
|
|
2236
2237
|
this.reconnectTimeout = void 0;
|
|
2237
2238
|
}
|
|
2238
|
-
return new Promise((
|
|
2239
|
+
return new Promise((resolve7, reject) => {
|
|
2239
2240
|
const connectionTimeout = setTimeout(() => {
|
|
2240
2241
|
if (this.ws) {
|
|
2241
2242
|
this.ws.terminate();
|
|
@@ -2266,7 +2267,7 @@ var require_websocket_client = __commonJS({
|
|
|
2266
2267
|
daemonPid: this.daemonPid
|
|
2267
2268
|
});
|
|
2268
2269
|
this.startHeartbeat();
|
|
2269
|
-
|
|
2270
|
+
resolve7();
|
|
2270
2271
|
});
|
|
2271
2272
|
this.ws.on("pong", () => {
|
|
2272
2273
|
if (this.heartbeatTimeoutTimer) {
|
|
@@ -2288,7 +2289,7 @@ var require_websocket_client = __commonJS({
|
|
|
2288
2289
|
this.ws.on("close", (code, reason) => {
|
|
2289
2290
|
console.log(`[EpisodaClient] WebSocket closed: ${code} ${reason.toString()}`);
|
|
2290
2291
|
this.isConnected = false;
|
|
2291
|
-
const willReconnect = !this.isDisconnecting;
|
|
2292
|
+
const willReconnect = !this.isDisconnecting && this.autoReconnectEnabled;
|
|
2292
2293
|
this.emit({
|
|
2293
2294
|
type: "disconnected",
|
|
2294
2295
|
code,
|
|
@@ -2312,6 +2313,19 @@ var require_websocket_client = __commonJS({
|
|
|
2312
2313
|
}
|
|
2313
2314
|
});
|
|
2314
2315
|
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Enable/disable automatic reconnect scheduling.
|
|
2318
|
+
*
|
|
2319
|
+
* This lets daemon startup/handshake own retry policy without competing
|
|
2320
|
+
* with client-side reconnect loops.
|
|
2321
|
+
*/
|
|
2322
|
+
setAutoReconnect(enabled) {
|
|
2323
|
+
this.autoReconnectEnabled = enabled;
|
|
2324
|
+
if (!enabled && this.reconnectTimeout) {
|
|
2325
|
+
clearTimeout(this.reconnectTimeout);
|
|
2326
|
+
this.reconnectTimeout = void 0;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2315
2329
|
/**
|
|
2316
2330
|
* Disconnect from the server
|
|
2317
2331
|
* @param intentional - If true, prevents automatic reconnection (user-initiated disconnect)
|
|
@@ -2397,13 +2411,13 @@ var require_websocket_client = __commonJS({
|
|
|
2397
2411
|
console.warn("[EpisodaClient] Cannot send - WebSocket not connected");
|
|
2398
2412
|
return false;
|
|
2399
2413
|
}
|
|
2400
|
-
return new Promise((
|
|
2414
|
+
return new Promise((resolve7) => {
|
|
2401
2415
|
this.ws.send(JSON.stringify(message), (error) => {
|
|
2402
2416
|
if (error) {
|
|
2403
2417
|
console.error("[EpisodaClient] Failed to send message:", error);
|
|
2404
|
-
|
|
2418
|
+
resolve7(false);
|
|
2405
2419
|
} else {
|
|
2406
|
-
|
|
2420
|
+
resolve7(true);
|
|
2407
2421
|
}
|
|
2408
2422
|
});
|
|
2409
2423
|
});
|
|
@@ -2526,7 +2540,12 @@ var require_websocket_client = __commonJS({
|
|
|
2526
2540
|
if (this.rateLimitBackoffUntil && Date.now() < this.rateLimitBackoffUntil) {
|
|
2527
2541
|
const waitTime = this.rateLimitBackoffUntil - Date.now();
|
|
2528
2542
|
console.log(`[EpisodaClient] Rate limited, waiting ${Math.round(waitTime / 1e3)}s before retry`);
|
|
2529
|
-
this.
|
|
2543
|
+
this.emit({
|
|
2544
|
+
type: "reconnect_scheduled",
|
|
2545
|
+
attempt: this.reconnectAttempts + 1,
|
|
2546
|
+
delayMs: waitTime,
|
|
2547
|
+
strategy: "rate_limited"
|
|
2548
|
+
});
|
|
2530
2549
|
this.reconnectTimeout = setTimeout(() => {
|
|
2531
2550
|
this.rateLimitBackoffUntil = void 0;
|
|
2532
2551
|
this.scheduleReconnect();
|
|
@@ -2534,6 +2553,7 @@ var require_websocket_client = __commonJS({
|
|
|
2534
2553
|
return;
|
|
2535
2554
|
}
|
|
2536
2555
|
let delay;
|
|
2556
|
+
let strategy = "connection_lost";
|
|
2537
2557
|
let shouldRetry = true;
|
|
2538
2558
|
const isCloudMode = this.environment === "cloud";
|
|
2539
2559
|
const MAX_CLOUD_AUTH_FAILURES = 3;
|
|
@@ -2545,20 +2565,28 @@ var require_websocket_client = __commonJS({
|
|
|
2545
2565
|
} else {
|
|
2546
2566
|
delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_CLOUD_RECONNECT_DELAY);
|
|
2547
2567
|
delay = applyJitter(delay);
|
|
2568
|
+
strategy = "cloud";
|
|
2548
2569
|
const delayStr = delay >= 6e4 ? `${Math.round(delay / 6e4)}m` : `${Math.round(delay / 1e3)}s`;
|
|
2549
2570
|
console.log(`[EpisodaClient] Cloud mode: reconnecting in ${delayStr}... (attempt ${this.reconnectAttempts + 1}, never giving up)`);
|
|
2550
2571
|
}
|
|
2551
2572
|
} else if (this.isGracefulShutdown && this.reconnectAttempts < 7) {
|
|
2552
2573
|
delay = Math.min(500 * Math.pow(2, this.reconnectAttempts), 5e3);
|
|
2553
2574
|
delay = applyJitter(delay);
|
|
2575
|
+
strategy = "graceful_shutdown";
|
|
2554
2576
|
console.log(`[EpisodaClient] Server restarting, reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/7)`);
|
|
2555
2577
|
} else {
|
|
2556
2578
|
delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_LOCAL_RECONNECT_DELAY);
|
|
2557
2579
|
delay = applyJitter(delay);
|
|
2580
|
+
strategy = "connection_lost";
|
|
2558
2581
|
const delayStr = delay >= 6e4 ? `${Math.round(delay / 6e4)}m` : `${Math.round(delay / 1e3)}s`;
|
|
2559
2582
|
console.log(`[EpisodaClient] Connection lost, retrying in ${delayStr}... (attempt ${this.reconnectAttempts + 1}, retrying until connected)`);
|
|
2560
2583
|
}
|
|
2561
2584
|
if (!shouldRetry) {
|
|
2585
|
+
this.emit({
|
|
2586
|
+
type: "reconnect_exhausted",
|
|
2587
|
+
attempts: this.reconnectAttempts,
|
|
2588
|
+
reason: `retry_exhausted_after_${this.consecutiveAuthFailures}_auth_failures`
|
|
2589
|
+
});
|
|
2562
2590
|
this.emit({
|
|
2563
2591
|
type: "disconnected",
|
|
2564
2592
|
code: 1006,
|
|
@@ -2567,9 +2595,20 @@ var require_websocket_client = __commonJS({
|
|
|
2567
2595
|
});
|
|
2568
2596
|
return;
|
|
2569
2597
|
}
|
|
2570
|
-
this.reconnectAttempts
|
|
2598
|
+
const reconnectAttempt = this.reconnectAttempts + 1;
|
|
2599
|
+
this.reconnectAttempts = reconnectAttempt;
|
|
2600
|
+
this.emit({
|
|
2601
|
+
type: "reconnect_scheduled",
|
|
2602
|
+
attempt: reconnectAttempt,
|
|
2603
|
+
delayMs: delay,
|
|
2604
|
+
strategy
|
|
2605
|
+
});
|
|
2571
2606
|
this.reconnectTimeout = setTimeout(() => {
|
|
2572
2607
|
console.log("[EpisodaClient] Attempting reconnection...");
|
|
2608
|
+
this.emit({
|
|
2609
|
+
type: "reconnect_attempt",
|
|
2610
|
+
attempt: reconnectAttempt
|
|
2611
|
+
});
|
|
2573
2612
|
this.connect(this.url, this.token, this.machineId, {
|
|
2574
2613
|
hostname: this.hostname,
|
|
2575
2614
|
osPlatform: this.osPlatform,
|
|
@@ -2582,6 +2621,11 @@ var require_websocket_client = __commonJS({
|
|
|
2582
2621
|
containerId: this.containerId
|
|
2583
2622
|
}).then(() => {
|
|
2584
2623
|
console.log("[EpisodaClient] Reconnection successful");
|
|
2624
|
+
this.emit({
|
|
2625
|
+
type: "reconnect_result",
|
|
2626
|
+
attempt: reconnectAttempt,
|
|
2627
|
+
success: true
|
|
2628
|
+
});
|
|
2585
2629
|
this.reconnectAttempts = 0;
|
|
2586
2630
|
this.isGracefulShutdown = false;
|
|
2587
2631
|
this.firstDisconnectTime = void 0;
|
|
@@ -2589,6 +2633,12 @@ var require_websocket_client = __commonJS({
|
|
|
2589
2633
|
this.consecutiveAuthFailures = 0;
|
|
2590
2634
|
}).catch((error) => {
|
|
2591
2635
|
console.error("[EpisodaClient] Reconnection failed:", error.message);
|
|
2636
|
+
this.emit({
|
|
2637
|
+
type: "reconnect_result",
|
|
2638
|
+
attempt: reconnectAttempt,
|
|
2639
|
+
success: false,
|
|
2640
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2641
|
+
});
|
|
2592
2642
|
});
|
|
2593
2643
|
}, delay);
|
|
2594
2644
|
}
|
|
@@ -2996,7 +3046,7 @@ var require_package = __commonJS({
|
|
|
2996
3046
|
"package.json"(exports2, module2) {
|
|
2997
3047
|
module2.exports = {
|
|
2998
3048
|
name: "@episoda/cli",
|
|
2999
|
-
version: "0.2.
|
|
3049
|
+
version: "0.2.176",
|
|
3000
3050
|
description: "CLI tool for Episoda local development workflow orchestration",
|
|
3001
3051
|
main: "dist/index.js",
|
|
3002
3052
|
types: "dist/index.d.ts",
|
|
@@ -3255,6 +3305,16 @@ function touchProject(projectPath) {
|
|
|
3255
3305
|
writeProjects(data);
|
|
3256
3306
|
}
|
|
3257
3307
|
}
|
|
3308
|
+
function pruneMissingProjectPaths() {
|
|
3309
|
+
const data = readProjects();
|
|
3310
|
+
const initialLength = data.projects.length;
|
|
3311
|
+
data.projects = data.projects.filter((project) => fs2.existsSync(project.path));
|
|
3312
|
+
const removedCount = initialLength - data.projects.length;
|
|
3313
|
+
if (removedCount > 0) {
|
|
3314
|
+
writeProjects(data);
|
|
3315
|
+
}
|
|
3316
|
+
return removedCount;
|
|
3317
|
+
}
|
|
3258
3318
|
|
|
3259
3319
|
// src/daemon/daemon-manager.ts
|
|
3260
3320
|
var path3 = __toESM(require("path"));
|
|
@@ -3301,10 +3361,10 @@ var IPCServer = class {
|
|
|
3301
3361
|
this.server = net.createServer((socket) => {
|
|
3302
3362
|
this.handleConnection(socket);
|
|
3303
3363
|
});
|
|
3304
|
-
return new Promise((
|
|
3364
|
+
return new Promise((resolve7, reject) => {
|
|
3305
3365
|
this.server.listen(socketPath, () => {
|
|
3306
3366
|
fs3.chmodSync(socketPath, 384);
|
|
3307
|
-
|
|
3367
|
+
resolve7();
|
|
3308
3368
|
});
|
|
3309
3369
|
this.server.on("error", reject);
|
|
3310
3370
|
});
|
|
@@ -3315,12 +3375,12 @@ var IPCServer = class {
|
|
|
3315
3375
|
async stop() {
|
|
3316
3376
|
if (!this.server) return;
|
|
3317
3377
|
const socketPath = getSocketPath();
|
|
3318
|
-
return new Promise((
|
|
3378
|
+
return new Promise((resolve7) => {
|
|
3319
3379
|
this.server.close(() => {
|
|
3320
3380
|
if (fs3.existsSync(socketPath)) {
|
|
3321
3381
|
fs3.unlinkSync(socketPath);
|
|
3322
3382
|
}
|
|
3323
|
-
|
|
3383
|
+
resolve7();
|
|
3324
3384
|
});
|
|
3325
3385
|
});
|
|
3326
3386
|
}
|
|
@@ -3389,8 +3449,24 @@ var import_child_process2 = require("child_process");
|
|
|
3389
3449
|
var import_util = require("util");
|
|
3390
3450
|
var import_core5 = __toESM(require_dist());
|
|
3391
3451
|
var execAsync = (0, import_util.promisify)(import_child_process2.exec);
|
|
3452
|
+
async function isGitRepository(projectPath) {
|
|
3453
|
+
try {
|
|
3454
|
+
await execAsync("git rev-parse --git-dir", { cwd: projectPath, timeout: 5e3 });
|
|
3455
|
+
return true;
|
|
3456
|
+
} catch {
|
|
3457
|
+
return false;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3392
3460
|
async function cleanupStaleCommits(projectPath) {
|
|
3393
3461
|
try {
|
|
3462
|
+
if (!await isGitRepository(projectPath)) {
|
|
3463
|
+
return {
|
|
3464
|
+
success: true,
|
|
3465
|
+
deleted_count: 0,
|
|
3466
|
+
kept_count: 0,
|
|
3467
|
+
message: "Skipping cleanup: project path is not a git repository"
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3394
3470
|
const machineId = await getMachineId();
|
|
3395
3471
|
const config = await (0, import_core5.loadConfig)();
|
|
3396
3472
|
if (!config?.access_token) {
|
|
@@ -3404,7 +3480,10 @@ async function cleanupStaleCommits(projectPath) {
|
|
|
3404
3480
|
try {
|
|
3405
3481
|
await execAsync("git fetch origin", { cwd: projectPath, timeout: 3e4 });
|
|
3406
3482
|
} catch (fetchError) {
|
|
3407
|
-
|
|
3483
|
+
const fetchMessage = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
3484
|
+
if (!fetchMessage.includes("not a git repository")) {
|
|
3485
|
+
console.warn("[EP950] Could not fetch origin:", fetchError);
|
|
3486
|
+
}
|
|
3408
3487
|
}
|
|
3409
3488
|
let currentBranch = "";
|
|
3410
3489
|
try {
|
|
@@ -3546,7 +3625,7 @@ function getDownloadUrl() {
|
|
|
3546
3625
|
return platformUrls[arch4] || null;
|
|
3547
3626
|
}
|
|
3548
3627
|
async function downloadFile(url, destPath) {
|
|
3549
|
-
return new Promise((
|
|
3628
|
+
return new Promise((resolve7, reject) => {
|
|
3550
3629
|
const followRedirect = (currentUrl, redirectCount = 0) => {
|
|
3551
3630
|
if (redirectCount > 5) {
|
|
3552
3631
|
reject(new Error("Too many redirects"));
|
|
@@ -3576,7 +3655,7 @@ async function downloadFile(url, destPath) {
|
|
|
3576
3655
|
response.pipe(file);
|
|
3577
3656
|
file.on("finish", () => {
|
|
3578
3657
|
file.close();
|
|
3579
|
-
|
|
3658
|
+
resolve7();
|
|
3580
3659
|
});
|
|
3581
3660
|
file.on("error", (err) => {
|
|
3582
3661
|
fs4.unlinkSync(destPath);
|
|
@@ -3990,10 +4069,10 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
3990
4069
|
const isTracked = Array.from(this.tunnelStates.values()).some((s) => s.info.pid === pid);
|
|
3991
4070
|
console.log(`[Tunnel] EP904: Found cloudflared PID ${pid} on port ${port} (tracked: ${isTracked})`);
|
|
3992
4071
|
this.killByPid(pid, "SIGTERM");
|
|
3993
|
-
await new Promise((
|
|
4072
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
3994
4073
|
if (this.isProcessRunning(pid)) {
|
|
3995
4074
|
this.killByPid(pid, "SIGKILL");
|
|
3996
|
-
await new Promise((
|
|
4075
|
+
await new Promise((resolve7) => setTimeout(resolve7, 200));
|
|
3997
4076
|
}
|
|
3998
4077
|
killed.push(pid);
|
|
3999
4078
|
}
|
|
@@ -4027,7 +4106,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4027
4106
|
if (!this.tunnelStates.has(moduleUid)) {
|
|
4028
4107
|
console.log(`[Tunnel] EP877: Found orphaned process PID ${pid} for ${moduleUid}, killing...`);
|
|
4029
4108
|
this.killByPid(pid, "SIGTERM");
|
|
4030
|
-
await new Promise((
|
|
4109
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
4031
4110
|
if (this.isProcessRunning(pid)) {
|
|
4032
4111
|
this.killByPid(pid, "SIGKILL");
|
|
4033
4112
|
}
|
|
@@ -4042,7 +4121,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4042
4121
|
if (!trackedPids.includes(pid) && !cleaned.includes(pid)) {
|
|
4043
4122
|
console.log(`[Tunnel] EP877: Found untracked cloudflared process PID ${pid}, killing...`);
|
|
4044
4123
|
this.killByPid(pid, "SIGTERM");
|
|
4045
|
-
await new Promise((
|
|
4124
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
4046
4125
|
if (this.isProcessRunning(pid)) {
|
|
4047
4126
|
this.killByPid(pid, "SIGKILL");
|
|
4048
4127
|
}
|
|
@@ -4132,7 +4211,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4132
4211
|
return { success: false, error: `Failed to get cloudflared: ${errorMessage}` };
|
|
4133
4212
|
}
|
|
4134
4213
|
}
|
|
4135
|
-
return new Promise((
|
|
4214
|
+
return new Promise((resolve7) => {
|
|
4136
4215
|
const tunnelInfo = {
|
|
4137
4216
|
moduleUid,
|
|
4138
4217
|
url: previewUrl || "",
|
|
@@ -4198,7 +4277,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4198
4277
|
moduleUid,
|
|
4199
4278
|
url: tunnelInfo.url
|
|
4200
4279
|
});
|
|
4201
|
-
|
|
4280
|
+
resolve7({ success: true, url: tunnelInfo.url });
|
|
4202
4281
|
}
|
|
4203
4282
|
};
|
|
4204
4283
|
process2.stderr?.on("data", (data) => {
|
|
@@ -4225,7 +4304,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4225
4304
|
onStatusChange?.("error", errorMsg);
|
|
4226
4305
|
this.emitEvent({ type: "error", moduleUid, error: errorMsg });
|
|
4227
4306
|
}
|
|
4228
|
-
|
|
4307
|
+
resolve7({ success: false, error: errorMsg });
|
|
4229
4308
|
} else if (wasConnected) {
|
|
4230
4309
|
if (currentState && !currentState.intentionallyStopped) {
|
|
4231
4310
|
console.log(`[Tunnel] EP948: Named tunnel ${moduleUid} crashed unexpectedly, attempting reconnect...`);
|
|
@@ -4256,7 +4335,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4256
4335
|
this.emitEvent({ type: "error", moduleUid, error: error.message });
|
|
4257
4336
|
}
|
|
4258
4337
|
if (!connected) {
|
|
4259
|
-
|
|
4338
|
+
resolve7({ success: false, error: error.message });
|
|
4260
4339
|
}
|
|
4261
4340
|
});
|
|
4262
4341
|
setTimeout(() => {
|
|
@@ -4279,7 +4358,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4279
4358
|
onStatusChange?.("error", errorMsg);
|
|
4280
4359
|
this.emitEvent({ type: "error", moduleUid, error: errorMsg });
|
|
4281
4360
|
}
|
|
4282
|
-
|
|
4361
|
+
resolve7({ success: false, error: errorMsg });
|
|
4283
4362
|
}
|
|
4284
4363
|
}, TUNNEL_TIMEOUTS.NAMED_TUNNEL_CONNECT);
|
|
4285
4364
|
});
|
|
@@ -4340,7 +4419,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4340
4419
|
if (orphanPid && this.isProcessRunning(orphanPid)) {
|
|
4341
4420
|
console.log(`[Tunnel] EP877: Killing orphaned process ${orphanPid} for ${moduleUid} before starting new tunnel`);
|
|
4342
4421
|
this.killByPid(orphanPid, "SIGTERM");
|
|
4343
|
-
await new Promise((
|
|
4422
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
4344
4423
|
if (this.isProcessRunning(orphanPid)) {
|
|
4345
4424
|
this.killByPid(orphanPid, "SIGKILL");
|
|
4346
4425
|
}
|
|
@@ -4349,7 +4428,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4349
4428
|
const killedOnPort = await this.killCloudflaredOnPort(port);
|
|
4350
4429
|
if (killedOnPort.length > 0) {
|
|
4351
4430
|
console.log(`[Tunnel] EP904: Pre-start port cleanup killed ${killedOnPort.length} process(es) on port ${port}`);
|
|
4352
|
-
await new Promise((
|
|
4431
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
4353
4432
|
}
|
|
4354
4433
|
const cleanup = await this.cleanupOrphanedProcesses();
|
|
4355
4434
|
if (cleanup.cleaned > 0) {
|
|
@@ -4389,7 +4468,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4389
4468
|
if (orphanPid && this.isProcessRunning(orphanPid)) {
|
|
4390
4469
|
console.log(`[Tunnel] EP877: Stopping orphaned process ${orphanPid} for ${moduleUid} via PID file`);
|
|
4391
4470
|
this.killByPid(orphanPid, "SIGTERM");
|
|
4392
|
-
await new Promise((
|
|
4471
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
4393
4472
|
if (this.isProcessRunning(orphanPid)) {
|
|
4394
4473
|
this.killByPid(orphanPid, "SIGKILL");
|
|
4395
4474
|
}
|
|
@@ -4405,16 +4484,16 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4405
4484
|
const tunnel = state.info;
|
|
4406
4485
|
if (tunnel.process && !tunnel.process.killed) {
|
|
4407
4486
|
tunnel.process.kill("SIGTERM");
|
|
4408
|
-
await new Promise((
|
|
4487
|
+
await new Promise((resolve7) => {
|
|
4409
4488
|
const timeout = setTimeout(() => {
|
|
4410
4489
|
if (tunnel.process && !tunnel.process.killed) {
|
|
4411
4490
|
tunnel.process.kill("SIGKILL");
|
|
4412
4491
|
}
|
|
4413
|
-
|
|
4492
|
+
resolve7();
|
|
4414
4493
|
}, 3e3);
|
|
4415
4494
|
tunnel.process.once("exit", () => {
|
|
4416
4495
|
clearTimeout(timeout);
|
|
4417
|
-
|
|
4496
|
+
resolve7();
|
|
4418
4497
|
});
|
|
4419
4498
|
});
|
|
4420
4499
|
}
|
|
@@ -4701,15 +4780,15 @@ async function writeToStdinWithBackpressure(process2, data, drainTimeoutMs, labe
|
|
|
4701
4780
|
if (!stdin || stdin.destroyed) {
|
|
4702
4781
|
throw new Error(`[${label}] stdin not available. session=${sessionId}`);
|
|
4703
4782
|
}
|
|
4704
|
-
await new Promise((
|
|
4783
|
+
await new Promise((resolve7, reject) => {
|
|
4705
4784
|
const ok = stdin.write(data);
|
|
4706
4785
|
if (ok) {
|
|
4707
|
-
|
|
4786
|
+
resolve7();
|
|
4708
4787
|
return;
|
|
4709
4788
|
}
|
|
4710
4789
|
const onDrain = () => {
|
|
4711
4790
|
cleanup();
|
|
4712
|
-
|
|
4791
|
+
resolve7();
|
|
4713
4792
|
};
|
|
4714
4793
|
const onError = (err) => {
|
|
4715
4794
|
cleanup();
|
|
@@ -4730,18 +4809,18 @@ async function writeToStdinWithBackpressure(process2, data, drainTimeoutMs, labe
|
|
|
4730
4809
|
});
|
|
4731
4810
|
}
|
|
4732
4811
|
function waitForProcessExit(process2, alive, timeoutMs) {
|
|
4733
|
-
return new Promise((
|
|
4812
|
+
return new Promise((resolve7) => {
|
|
4734
4813
|
if (!process2 || !alive) {
|
|
4735
|
-
|
|
4814
|
+
resolve7(true);
|
|
4736
4815
|
return;
|
|
4737
4816
|
}
|
|
4738
4817
|
const onExit = () => {
|
|
4739
4818
|
clearTimeout(timer);
|
|
4740
|
-
|
|
4819
|
+
resolve7(true);
|
|
4741
4820
|
};
|
|
4742
4821
|
const timer = setTimeout(() => {
|
|
4743
4822
|
process2.removeListener("exit", onExit);
|
|
4744
|
-
|
|
4823
|
+
resolve7(false);
|
|
4745
4824
|
}, timeoutMs);
|
|
4746
4825
|
process2.once("exit", onExit);
|
|
4747
4826
|
});
|
|
@@ -5737,10 +5816,10 @@ var CodexPersistentRuntime = class {
|
|
|
5737
5816
|
}
|
|
5738
5817
|
waitForThreadId(timeoutMs) {
|
|
5739
5818
|
if (this._agentSessionId) return Promise.resolve(this._agentSessionId);
|
|
5740
|
-
return new Promise((
|
|
5819
|
+
return new Promise((resolve7, reject) => {
|
|
5741
5820
|
const onId = (id) => {
|
|
5742
5821
|
clearTimeout(timer);
|
|
5743
|
-
|
|
5822
|
+
resolve7(id);
|
|
5744
5823
|
};
|
|
5745
5824
|
const timer = setTimeout(() => {
|
|
5746
5825
|
this.threadIdWaiters = this.threadIdWaiters.filter((w) => w !== onId);
|
|
@@ -5771,12 +5850,12 @@ var CodexPersistentRuntime = class {
|
|
|
5771
5850
|
sendRequest(method, params, timeoutMs = INIT_TIMEOUT_MS) {
|
|
5772
5851
|
const id = this.nextId++;
|
|
5773
5852
|
const msg = { jsonrpc: "2.0", id, method, params };
|
|
5774
|
-
return new Promise(async (
|
|
5853
|
+
return new Promise(async (resolve7, reject) => {
|
|
5775
5854
|
const timeout = setTimeout(() => {
|
|
5776
5855
|
this.pending.delete(id);
|
|
5777
5856
|
reject(new Error(`JSON-RPC timeout: ${method}`));
|
|
5778
5857
|
}, timeoutMs);
|
|
5779
|
-
this.pending.set(id, { resolve:
|
|
5858
|
+
this.pending.set(id, { resolve: resolve7, reject, timeout });
|
|
5780
5859
|
try {
|
|
5781
5860
|
await this.writeToStdin(JSON.stringify(msg) + "\n");
|
|
5782
5861
|
} catch (err) {
|
|
@@ -5926,11 +6005,11 @@ var UnifiedAgentRuntime = class {
|
|
|
5926
6005
|
if (!this.currentTurn && this.impl.turnState === "idle") {
|
|
5927
6006
|
return this.dispatchTurn(message, callbacks);
|
|
5928
6007
|
}
|
|
5929
|
-
return new Promise((
|
|
6008
|
+
return new Promise((resolve7, reject) => {
|
|
5930
6009
|
this.queuedTurns.push({
|
|
5931
6010
|
message,
|
|
5932
6011
|
callbacks,
|
|
5933
|
-
resolve:
|
|
6012
|
+
resolve: resolve7,
|
|
5934
6013
|
reject
|
|
5935
6014
|
});
|
|
5936
6015
|
});
|
|
@@ -7443,13 +7522,13 @@ async function getChildProcessRssMb(pid) {
|
|
|
7443
7522
|
if (!pid || pid <= 0) return null;
|
|
7444
7523
|
try {
|
|
7445
7524
|
if (typeof import_child_process9.execFile !== "function") return null;
|
|
7446
|
-
const stdout = await new Promise((
|
|
7525
|
+
const stdout = await new Promise((resolve7, reject) => {
|
|
7447
7526
|
(0, import_child_process9.execFile)("ps", ["-o", "rss=", "-p", String(pid)], { timeout: 1e3 }, (err, out) => {
|
|
7448
7527
|
if (err) {
|
|
7449
7528
|
reject(err);
|
|
7450
7529
|
return;
|
|
7451
7530
|
}
|
|
7452
|
-
|
|
7531
|
+
resolve7(String(out));
|
|
7453
7532
|
});
|
|
7454
7533
|
});
|
|
7455
7534
|
const kb = Number(String(stdout).trim());
|
|
@@ -7552,8 +7631,8 @@ var AgentControlPlane = class {
|
|
|
7552
7631
|
async withConfigLock(fn) {
|
|
7553
7632
|
const previousLock = this.configWriteLock;
|
|
7554
7633
|
let releaseLock;
|
|
7555
|
-
this.configWriteLock = new Promise((
|
|
7556
|
-
releaseLock =
|
|
7634
|
+
this.configWriteLock = new Promise((resolve7) => {
|
|
7635
|
+
releaseLock = resolve7;
|
|
7557
7636
|
});
|
|
7558
7637
|
try {
|
|
7559
7638
|
await previousLock;
|
|
@@ -8782,7 +8861,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
8782
8861
|
const lockContent = fs14.readFileSync(lockPath, "utf-8").trim();
|
|
8783
8862
|
const lockPid = parseInt(lockContent, 10);
|
|
8784
8863
|
if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
|
|
8785
|
-
await new Promise((
|
|
8864
|
+
await new Promise((resolve7) => setTimeout(resolve7, retryInterval));
|
|
8786
8865
|
continue;
|
|
8787
8866
|
}
|
|
8788
8867
|
} catch {
|
|
@@ -8796,7 +8875,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
8796
8875
|
} catch {
|
|
8797
8876
|
continue;
|
|
8798
8877
|
}
|
|
8799
|
-
await new Promise((
|
|
8878
|
+
await new Promise((resolve7) => setTimeout(resolve7, retryInterval));
|
|
8800
8879
|
continue;
|
|
8801
8880
|
}
|
|
8802
8881
|
throw err;
|
|
@@ -9586,6 +9665,7 @@ function getInstallCommand(cwd) {
|
|
|
9586
9665
|
var fs34 = __toESM(require("fs"));
|
|
9587
9666
|
var http2 = __toESM(require("http"));
|
|
9588
9667
|
var os15 = __toESM(require("os"));
|
|
9668
|
+
var import_child_process19 = require("child_process");
|
|
9589
9669
|
|
|
9590
9670
|
// src/daemon/ipc-router.ts
|
|
9591
9671
|
var os14 = __toESM(require("os"));
|
|
@@ -10319,7 +10399,7 @@ async function handleExec(command, projectPath) {
|
|
|
10319
10399
|
env = {}
|
|
10320
10400
|
} = command;
|
|
10321
10401
|
const effectiveTimeout = Math.min(Math.max(timeout, 1e3), MAX_TIMEOUT);
|
|
10322
|
-
return new Promise((
|
|
10402
|
+
return new Promise((resolve7) => {
|
|
10323
10403
|
let stdout = "";
|
|
10324
10404
|
let stderr = "";
|
|
10325
10405
|
let timedOut = false;
|
|
@@ -10327,7 +10407,7 @@ async function handleExec(command, projectPath) {
|
|
|
10327
10407
|
const done = (result) => {
|
|
10328
10408
|
if (resolved) return;
|
|
10329
10409
|
resolved = true;
|
|
10330
|
-
|
|
10410
|
+
resolve7(result);
|
|
10331
10411
|
};
|
|
10332
10412
|
try {
|
|
10333
10413
|
const proc = (0, import_child_process11.spawn)(cmd, {
|
|
@@ -10431,18 +10511,18 @@ var import_core12 = __toESM(require_dist());
|
|
|
10431
10511
|
// src/utils/port-check.ts
|
|
10432
10512
|
var net2 = __toESM(require("net"));
|
|
10433
10513
|
async function isPortInUse(port) {
|
|
10434
|
-
return new Promise((
|
|
10514
|
+
return new Promise((resolve7) => {
|
|
10435
10515
|
const server = net2.createServer();
|
|
10436
10516
|
server.once("error", (err) => {
|
|
10437
10517
|
if (err.code === "EADDRINUSE") {
|
|
10438
|
-
|
|
10518
|
+
resolve7(true);
|
|
10439
10519
|
} else {
|
|
10440
|
-
|
|
10520
|
+
resolve7(false);
|
|
10441
10521
|
}
|
|
10442
10522
|
});
|
|
10443
10523
|
server.once("listening", () => {
|
|
10444
10524
|
server.close();
|
|
10445
|
-
|
|
10525
|
+
resolve7(false);
|
|
10446
10526
|
});
|
|
10447
10527
|
server.listen(port);
|
|
10448
10528
|
});
|
|
@@ -10845,7 +10925,7 @@ var DevServerRegistry = class {
|
|
|
10845
10925
|
return killed;
|
|
10846
10926
|
}
|
|
10847
10927
|
wait(ms) {
|
|
10848
|
-
return new Promise((
|
|
10928
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
10849
10929
|
}
|
|
10850
10930
|
killWsServerOnPort(wsPort, worktreePath) {
|
|
10851
10931
|
const killed = [];
|
|
@@ -11419,7 +11499,7 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
11419
11499
|
return Math.min(delay, DEV_SERVER_CONSTANTS.MAX_RESTART_DELAY_MS);
|
|
11420
11500
|
}
|
|
11421
11501
|
async checkHealth(port) {
|
|
11422
|
-
return new Promise((
|
|
11502
|
+
return new Promise((resolve7) => {
|
|
11423
11503
|
const req = http.request(
|
|
11424
11504
|
{
|
|
11425
11505
|
hostname: "localhost",
|
|
@@ -11428,18 +11508,18 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
11428
11508
|
method: "HEAD",
|
|
11429
11509
|
timeout: DEV_SERVER_CONSTANTS.HEALTH_CHECK_TIMEOUT_MS
|
|
11430
11510
|
},
|
|
11431
|
-
() =>
|
|
11511
|
+
() => resolve7(true)
|
|
11432
11512
|
);
|
|
11433
|
-
req.on("error", () =>
|
|
11513
|
+
req.on("error", () => resolve7(false));
|
|
11434
11514
|
req.on("timeout", () => {
|
|
11435
11515
|
req.destroy();
|
|
11436
|
-
|
|
11516
|
+
resolve7(false);
|
|
11437
11517
|
});
|
|
11438
11518
|
req.end();
|
|
11439
11519
|
});
|
|
11440
11520
|
}
|
|
11441
11521
|
async checkWsHealth(wsPort) {
|
|
11442
|
-
return new Promise((
|
|
11522
|
+
return new Promise((resolve7) => {
|
|
11443
11523
|
const req = http.request(
|
|
11444
11524
|
{
|
|
11445
11525
|
hostname: "127.0.0.1",
|
|
@@ -11449,14 +11529,14 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
11449
11529
|
timeout: DEV_SERVER_CONSTANTS.HEALTH_CHECK_TIMEOUT_MS
|
|
11450
11530
|
},
|
|
11451
11531
|
(res) => {
|
|
11452
|
-
|
|
11532
|
+
resolve7(res.statusCode === 200);
|
|
11453
11533
|
res.resume();
|
|
11454
11534
|
}
|
|
11455
11535
|
);
|
|
11456
|
-
req.on("error", () =>
|
|
11536
|
+
req.on("error", () => resolve7(false));
|
|
11457
11537
|
req.on("timeout", () => {
|
|
11458
11538
|
req.destroy();
|
|
11459
|
-
|
|
11539
|
+
resolve7(false);
|
|
11460
11540
|
});
|
|
11461
11541
|
req.end();
|
|
11462
11542
|
});
|
|
@@ -11484,7 +11564,7 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
11484
11564
|
return false;
|
|
11485
11565
|
}
|
|
11486
11566
|
wait(ms) {
|
|
11487
|
-
return new Promise((
|
|
11567
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
11488
11568
|
}
|
|
11489
11569
|
getLogsDir() {
|
|
11490
11570
|
const logsDir = path25.join((0, import_core12.getConfigDir)(), "logs");
|
|
@@ -11683,7 +11763,7 @@ var PreviewManager = class extends import_events3.EventEmitter {
|
|
|
11683
11763
|
for (let attempt = 1; attempt <= MAX_TUNNEL_RETRIES; attempt++) {
|
|
11684
11764
|
if (attempt > 1) {
|
|
11685
11765
|
console.log(`[PreviewManager] Retrying tunnel for ${moduleUid} (attempt ${attempt}/${MAX_TUNNEL_RETRIES})...`);
|
|
11686
|
-
await new Promise((
|
|
11766
|
+
await new Promise((resolve7) => setTimeout(resolve7, 2e3));
|
|
11687
11767
|
}
|
|
11688
11768
|
tunnelResult = await this.tunnel.startTunnel({
|
|
11689
11769
|
moduleUid,
|
|
@@ -11815,7 +11895,7 @@ var PreviewManager = class extends import_events3.EventEmitter {
|
|
|
11815
11895
|
}
|
|
11816
11896
|
console.log(`[PreviewManager] Restarting preview for ${moduleUid}`);
|
|
11817
11897
|
await this.stopPreview(moduleUid);
|
|
11818
|
-
await new Promise((
|
|
11898
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
11819
11899
|
return this.startPreview({
|
|
11820
11900
|
moduleUid,
|
|
11821
11901
|
worktreePath: state.worktreePath,
|
|
@@ -12756,7 +12836,7 @@ async function killProcessOnPort(port) {
|
|
|
12756
12836
|
} catch {
|
|
12757
12837
|
}
|
|
12758
12838
|
}
|
|
12759
|
-
await new Promise((
|
|
12839
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
12760
12840
|
for (const pid of pids) {
|
|
12761
12841
|
try {
|
|
12762
12842
|
(0, import_child_process15.execSync)(`kill -0 ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
@@ -12765,7 +12845,7 @@ async function killProcessOnPort(port) {
|
|
|
12765
12845
|
} catch {
|
|
12766
12846
|
}
|
|
12767
12847
|
}
|
|
12768
|
-
await new Promise((
|
|
12848
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
12769
12849
|
const stillInUse = await isPortInUse(port);
|
|
12770
12850
|
if (stillInUse) {
|
|
12771
12851
|
console.error(`[DevServer] EP929: Port ${port} still in use after kill attempts`);
|
|
@@ -12785,7 +12865,7 @@ async function waitForPort(port, timeoutMs = 3e4) {
|
|
|
12785
12865
|
if (await isPortInUse(port)) {
|
|
12786
12866
|
return true;
|
|
12787
12867
|
}
|
|
12788
|
-
await new Promise((
|
|
12868
|
+
await new Promise((resolve7) => setTimeout(resolve7, checkInterval));
|
|
12789
12869
|
}
|
|
12790
12870
|
return false;
|
|
12791
12871
|
}
|
|
@@ -12857,7 +12937,7 @@ async function handleProcessExit(moduleUid, code, signal) {
|
|
|
12857
12937
|
const delay = calculateRestartDelay(serverInfo.restartCount);
|
|
12858
12938
|
console.log(`[DevServer] EP932: Restarting ${moduleUid} in ${delay}ms (attempt ${serverInfo.restartCount + 1}/${MAX_RESTART_ATTEMPTS})`);
|
|
12859
12939
|
writeToLog(serverInfo.logFile || "", `Scheduling restart in ${delay}ms (attempt ${serverInfo.restartCount + 1})`, false);
|
|
12860
|
-
await new Promise((
|
|
12940
|
+
await new Promise((resolve7) => setTimeout(resolve7, delay));
|
|
12861
12941
|
if (!activeServers.has(moduleUid)) {
|
|
12862
12942
|
console.log(`[DevServer] EP932: Server ${moduleUid} was removed during restart delay, aborting restart`);
|
|
12863
12943
|
return;
|
|
@@ -12982,7 +13062,7 @@ async function stopDevServer(moduleUid) {
|
|
|
12982
13062
|
writeToLog(serverInfo.logFile, `Stopping server (manual stop)`, false);
|
|
12983
13063
|
}
|
|
12984
13064
|
serverInfo.process.kill("SIGTERM");
|
|
12985
|
-
await new Promise((
|
|
13065
|
+
await new Promise((resolve7) => setTimeout(resolve7, 2e3));
|
|
12986
13066
|
if (!serverInfo.process.killed) {
|
|
12987
13067
|
serverInfo.process.kill("SIGKILL");
|
|
12988
13068
|
}
|
|
@@ -13000,7 +13080,7 @@ async function restartDevServer(moduleUid) {
|
|
|
13000
13080
|
writeToLog(logFile, `Manual restart requested`, false);
|
|
13001
13081
|
}
|
|
13002
13082
|
await stopDevServer(moduleUid);
|
|
13003
|
-
await new Promise((
|
|
13083
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
13004
13084
|
if (await isPortInUse(port)) {
|
|
13005
13085
|
await killProcessOnPort(port);
|
|
13006
13086
|
}
|
|
@@ -13062,7 +13142,6 @@ var IPCRouter = class {
|
|
|
13062
13142
|
});
|
|
13063
13143
|
this.ipcServer.on("add-project", async (params) => {
|
|
13064
13144
|
const { projectId, projectPath } = params;
|
|
13065
|
-
addProject(projectId, projectPath);
|
|
13066
13145
|
const MAX_RETRIES = 3;
|
|
13067
13146
|
const INITIAL_DELAY = 1e3;
|
|
13068
13147
|
let lastError = "";
|
|
@@ -13075,6 +13154,7 @@ var IPCRouter = class {
|
|
|
13075
13154
|
lastError = "Connection established but not healthy";
|
|
13076
13155
|
return { success: false, connected: false, error: lastError };
|
|
13077
13156
|
}
|
|
13157
|
+
addProject(projectId, projectPath);
|
|
13078
13158
|
return { success: true, connected: true };
|
|
13079
13159
|
} catch (error) {
|
|
13080
13160
|
lastError = error instanceof Error ? error.message : String(error);
|
|
@@ -13082,7 +13162,7 @@ var IPCRouter = class {
|
|
|
13082
13162
|
if (attempt < MAX_RETRIES) {
|
|
13083
13163
|
const delay = INITIAL_DELAY * Math.pow(2, attempt - 1);
|
|
13084
13164
|
console.log(`[Daemon] Retrying in ${delay / 1e3}s...`);
|
|
13085
|
-
await new Promise((
|
|
13165
|
+
await new Promise((resolve7) => setTimeout(resolve7, delay));
|
|
13086
13166
|
await this.host.disconnectProject(projectPath);
|
|
13087
13167
|
}
|
|
13088
13168
|
}
|
|
@@ -13522,6 +13602,8 @@ var UpdateManager = class _UpdateManager {
|
|
|
13522
13602
|
const child = (0, import_child_process17.spawn)("node", [this.daemonEntryFile], {
|
|
13523
13603
|
detached: true,
|
|
13524
13604
|
stdio: ["ignore", logFd, logFd],
|
|
13605
|
+
cwd: configDir,
|
|
13606
|
+
// Keep daemon process context stable across restarts.
|
|
13525
13607
|
env: { ...process.env, EPISODA_DAEMON_MODE: "1" }
|
|
13526
13608
|
});
|
|
13527
13609
|
if (!child.pid) {
|
|
@@ -14234,45 +14316,54 @@ var DaemonCore = class {
|
|
|
14234
14316
|
// src/daemon/connection-manager.ts
|
|
14235
14317
|
var ConnectionManager = class {
|
|
14236
14318
|
constructor() {
|
|
14319
|
+
// Single source-of-truth for project connectivity.
|
|
14237
14320
|
this.connections = /* @__PURE__ */ new Map();
|
|
14238
|
-
// projectPath -> connection
|
|
14239
|
-
this.liveConnections = /* @__PURE__ */ new Set();
|
|
14240
|
-
// projectPath
|
|
14241
|
-
this.pendingConnections = /* @__PURE__ */ new Set();
|
|
14242
14321
|
}
|
|
14243
|
-
// projectPath
|
|
14322
|
+
// projectPath -> connection
|
|
14244
14323
|
hasConnection(projectPath) {
|
|
14245
14324
|
return this.connections.has(projectPath);
|
|
14246
14325
|
}
|
|
14247
14326
|
hasLiveConnection(projectPath) {
|
|
14248
|
-
return this.
|
|
14327
|
+
return this.getState(projectPath) === "connected";
|
|
14249
14328
|
}
|
|
14250
14329
|
hasPendingConnection(projectPath) {
|
|
14251
|
-
|
|
14330
|
+
const state = this.getState(projectPath);
|
|
14331
|
+
return state === "connecting" || state === "authenticating";
|
|
14252
14332
|
}
|
|
14253
14333
|
isConnectionHealthy(projectPath) {
|
|
14254
|
-
return this.
|
|
14334
|
+
return this.getState(projectPath) === "connected" && this.isWebSocketOpen(projectPath);
|
|
14255
14335
|
}
|
|
14256
14336
|
getConnection(projectPath) {
|
|
14257
14337
|
return this.connections.get(projectPath);
|
|
14258
14338
|
}
|
|
14259
14339
|
setConnection(projectPath, connection) {
|
|
14260
|
-
|
|
14340
|
+
const stateful = {
|
|
14341
|
+
...connection,
|
|
14342
|
+
state: connection.state || "connecting",
|
|
14343
|
+
lastTransitionAt: connection.lastTransitionAt || Date.now()
|
|
14344
|
+
};
|
|
14345
|
+
this.connections.set(projectPath, stateful);
|
|
14261
14346
|
}
|
|
14262
14347
|
deleteConnection(projectPath) {
|
|
14263
14348
|
this.connections.delete(projectPath);
|
|
14264
14349
|
}
|
|
14265
14350
|
addLiveConnection(projectPath) {
|
|
14266
|
-
this.
|
|
14351
|
+
this.setState(projectPath, "connected");
|
|
14267
14352
|
}
|
|
14268
14353
|
removeLiveConnection(projectPath) {
|
|
14269
|
-
this.
|
|
14354
|
+
const state = this.getState(projectPath);
|
|
14355
|
+
if (state === "connected") {
|
|
14356
|
+
this.setState(projectPath, "disconnected");
|
|
14357
|
+
}
|
|
14270
14358
|
}
|
|
14271
14359
|
addPendingConnection(projectPath) {
|
|
14272
|
-
this.
|
|
14360
|
+
this.setState(projectPath, "connecting");
|
|
14273
14361
|
}
|
|
14274
14362
|
removePendingConnection(projectPath) {
|
|
14275
|
-
this.
|
|
14363
|
+
const state = this.getState(projectPath);
|
|
14364
|
+
if (state === "connecting" || state === "authenticating") {
|
|
14365
|
+
this.setState(projectPath, "disconnected");
|
|
14366
|
+
}
|
|
14276
14367
|
}
|
|
14277
14368
|
isWebSocketOpen(projectPath) {
|
|
14278
14369
|
const connection = this.connections.get(projectPath);
|
|
@@ -14283,10 +14374,18 @@ var ConnectionManager = class {
|
|
|
14283
14374
|
return this.connections.size;
|
|
14284
14375
|
}
|
|
14285
14376
|
liveConnectionCount() {
|
|
14286
|
-
|
|
14377
|
+
let count = 0;
|
|
14378
|
+
for (const connection of this.connections.values()) {
|
|
14379
|
+
if (connection.state === "connected") count++;
|
|
14380
|
+
}
|
|
14381
|
+
return count;
|
|
14287
14382
|
}
|
|
14288
14383
|
pendingConnectionCount() {
|
|
14289
|
-
|
|
14384
|
+
let count = 0;
|
|
14385
|
+
for (const connection of this.connections.values()) {
|
|
14386
|
+
if (connection.state === "connecting" || connection.state === "authenticating") count++;
|
|
14387
|
+
}
|
|
14388
|
+
return count;
|
|
14290
14389
|
}
|
|
14291
14390
|
entries() {
|
|
14292
14391
|
return this.connections.entries();
|
|
@@ -14295,12 +14394,24 @@ var ConnectionManager = class {
|
|
|
14295
14394
|
this.connections.clear();
|
|
14296
14395
|
}
|
|
14297
14396
|
clearLiveConnections() {
|
|
14298
|
-
this.
|
|
14397
|
+
for (const [projectPath, connection] of this.connections.entries()) {
|
|
14398
|
+
if (connection.state === "connected") {
|
|
14399
|
+
this.setState(projectPath, "disconnected");
|
|
14400
|
+
}
|
|
14401
|
+
}
|
|
14299
14402
|
}
|
|
14300
14403
|
clearPendingConnections() {
|
|
14301
|
-
this.
|
|
14404
|
+
for (const [projectPath, connection] of this.connections.entries()) {
|
|
14405
|
+
if (connection.state === "connecting" || connection.state === "authenticating") {
|
|
14406
|
+
this.setState(projectPath, "disconnected");
|
|
14407
|
+
}
|
|
14408
|
+
}
|
|
14302
14409
|
}
|
|
14303
14410
|
async restoreConnections(connectProject) {
|
|
14411
|
+
const pruned = pruneMissingProjectPaths();
|
|
14412
|
+
if (pruned > 0) {
|
|
14413
|
+
console.log(`[Daemon] Pruned ${pruned} stale project tracking entr${pruned === 1 ? "y" : "ies"} before restore`);
|
|
14414
|
+
}
|
|
14304
14415
|
const projects = getAllProjects();
|
|
14305
14416
|
for (const project of projects) {
|
|
14306
14417
|
try {
|
|
@@ -14310,19 +14421,31 @@ var ConnectionManager = class {
|
|
|
14310
14421
|
}
|
|
14311
14422
|
}
|
|
14312
14423
|
}
|
|
14424
|
+
getState(projectPath) {
|
|
14425
|
+
return this.connections.get(projectPath)?.state || null;
|
|
14426
|
+
}
|
|
14427
|
+
setState(projectPath, state, error) {
|
|
14428
|
+
const connection = this.connections.get(projectPath);
|
|
14429
|
+
if (!connection) return;
|
|
14430
|
+
connection.state = state;
|
|
14431
|
+
connection.lastTransitionAt = Date.now();
|
|
14432
|
+
connection.lastError = error;
|
|
14433
|
+
}
|
|
14313
14434
|
/**
|
|
14314
|
-
* Wait for auth_success or
|
|
14435
|
+
* Wait for auth_success or fail-fast auth/transport errors after connect().
|
|
14315
14436
|
*
|
|
14316
14437
|
* This keeps auth timeout lifecycle in the connection subsystem and ensures
|
|
14317
14438
|
* handlers are removed deterministically on every exit path.
|
|
14318
14439
|
*/
|
|
14319
14440
|
async waitForAuthentication(client, timeoutMs = 3e4) {
|
|
14320
|
-
await new Promise((
|
|
14441
|
+
await new Promise((resolve7, reject) => {
|
|
14321
14442
|
let settled = false;
|
|
14322
14443
|
const cleanup = () => {
|
|
14323
14444
|
clearTimeout(timeout);
|
|
14324
14445
|
client.off("auth_success", authHandler);
|
|
14325
|
-
client.off("auth_error",
|
|
14446
|
+
client.off("auth_error", authErrorHandler);
|
|
14447
|
+
client.off("error", serverErrorHandler);
|
|
14448
|
+
client.off("disconnected", disconnectedHandler);
|
|
14326
14449
|
};
|
|
14327
14450
|
const timeout = setTimeout(() => {
|
|
14328
14451
|
if (settled) return;
|
|
@@ -14334,17 +14457,41 @@ var ConnectionManager = class {
|
|
|
14334
14457
|
if (settled) return;
|
|
14335
14458
|
settled = true;
|
|
14336
14459
|
cleanup();
|
|
14337
|
-
|
|
14460
|
+
resolve7();
|
|
14338
14461
|
};
|
|
14339
|
-
const
|
|
14462
|
+
const authErrorHandler = (message) => {
|
|
14340
14463
|
if (settled) return;
|
|
14341
14464
|
settled = true;
|
|
14342
14465
|
cleanup();
|
|
14343
14466
|
const errorMsg = message;
|
|
14344
14467
|
reject(new Error(errorMsg.message || "Authentication failed"));
|
|
14345
14468
|
};
|
|
14469
|
+
const serverErrorHandler = (message) => {
|
|
14470
|
+
if (settled) return;
|
|
14471
|
+
settled = true;
|
|
14472
|
+
cleanup();
|
|
14473
|
+
const errorMsg = message;
|
|
14474
|
+
const code = errorMsg.code || "SERVER_ERROR";
|
|
14475
|
+
if (code === "TOO_SOON" && typeof errorMsg.retryAfter === "number") {
|
|
14476
|
+
reject(new Error(`Authentication deferred by server (${code}, retryAfter=${errorMsg.retryAfter}s)`));
|
|
14477
|
+
return;
|
|
14478
|
+
}
|
|
14479
|
+
reject(new Error(errorMsg.message || `Authentication failed (${code})`));
|
|
14480
|
+
};
|
|
14481
|
+
const disconnectedHandler = (message) => {
|
|
14482
|
+
if (settled) return;
|
|
14483
|
+
settled = true;
|
|
14484
|
+
cleanup();
|
|
14485
|
+
const event = message;
|
|
14486
|
+
const code = typeof event.code === "number" ? event.code : -1;
|
|
14487
|
+
const reason = event.reason || "connection closed";
|
|
14488
|
+
const suffix = event.willReconnect ? ", reconnect scheduled" : "";
|
|
14489
|
+
reject(new Error(`Connection closed before authentication (code=${code}, reason=${reason}${suffix})`));
|
|
14490
|
+
};
|
|
14346
14491
|
client.on("auth_success", authHandler);
|
|
14347
|
-
client.on("auth_error",
|
|
14492
|
+
client.on("auth_error", authErrorHandler);
|
|
14493
|
+
client.on("error", serverErrorHandler);
|
|
14494
|
+
client.on("disconnected", disconnectedHandler);
|
|
14348
14495
|
});
|
|
14349
14496
|
}
|
|
14350
14497
|
};
|
|
@@ -14911,6 +15058,8 @@ var Daemon = class _Daemon {
|
|
|
14911
15058
|
// EP1003: Prevents race conditions between server-orchestrated tunnel commands
|
|
14912
15059
|
this.tunnelOperationLocks = /* @__PURE__ */ new Map();
|
|
14913
15060
|
// moduleUid -> operation promise
|
|
15061
|
+
// EP1426: Single in-flight connect attempt per project path.
|
|
15062
|
+
this.connectProjectInFlight = /* @__PURE__ */ new Map();
|
|
14914
15063
|
// EP1210-7: Health HTTP endpoint for external monitoring
|
|
14915
15064
|
this.healthServer = null;
|
|
14916
15065
|
// EP1267: Cloud disconnect watchdog
|
|
@@ -14948,6 +15097,14 @@ var Daemon = class _Daemon {
|
|
|
14948
15097
|
this.agentEventSeq.set(sessionId, next);
|
|
14949
15098
|
return next;
|
|
14950
15099
|
}
|
|
15100
|
+
logReliabilityMetric(metric, fields) {
|
|
15101
|
+
console.log(`[Daemon][Metric] ${JSON.stringify({
|
|
15102
|
+
metric,
|
|
15103
|
+
machineId: this.machineId || "unknown",
|
|
15104
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15105
|
+
...fields
|
|
15106
|
+
})}`);
|
|
15107
|
+
}
|
|
14951
15108
|
/**
|
|
14952
15109
|
* Start the daemon
|
|
14953
15110
|
*/
|
|
@@ -15144,8 +15301,8 @@ var Daemon = class _Daemon {
|
|
|
15144
15301
|
}
|
|
15145
15302
|
}
|
|
15146
15303
|
let releaseLock;
|
|
15147
|
-
const lockPromise = new Promise((
|
|
15148
|
-
releaseLock =
|
|
15304
|
+
const lockPromise = new Promise((resolve7) => {
|
|
15305
|
+
releaseLock = resolve7;
|
|
15149
15306
|
});
|
|
15150
15307
|
this.tunnelOperationLocks.set(moduleUid, lockPromise);
|
|
15151
15308
|
try {
|
|
@@ -15162,6 +15319,25 @@ var Daemon = class _Daemon {
|
|
|
15162
15319
|
* EP1366: Made public for IPCRouterHost interface
|
|
15163
15320
|
*/
|
|
15164
15321
|
async connectProject(projectId, projectPath) {
|
|
15322
|
+
const existing = this.connectProjectInFlight.get(projectPath);
|
|
15323
|
+
if (existing) {
|
|
15324
|
+
console.log(`[Daemon] Connection attempt already in progress for ${projectPath}, joining existing attempt`);
|
|
15325
|
+
return existing;
|
|
15326
|
+
}
|
|
15327
|
+
const attempt = this.connectProjectInternal(projectId, projectPath);
|
|
15328
|
+
this.connectProjectInFlight.set(projectPath, attempt);
|
|
15329
|
+
try {
|
|
15330
|
+
await attempt;
|
|
15331
|
+
} finally {
|
|
15332
|
+
if (this.connectProjectInFlight.get(projectPath) === attempt) {
|
|
15333
|
+
this.connectProjectInFlight.delete(projectPath);
|
|
15334
|
+
}
|
|
15335
|
+
}
|
|
15336
|
+
}
|
|
15337
|
+
async connectProjectInternal(projectId, projectPath) {
|
|
15338
|
+
const initialState = this.connectionManager.getState(projectPath);
|
|
15339
|
+
const isReconnectAttempt = initialState !== null;
|
|
15340
|
+
const connectAttemptStartedAt = Date.now();
|
|
15165
15341
|
if (this.connectionManager.hasConnection(projectPath)) {
|
|
15166
15342
|
if (this.connectionManager.hasLiveConnection(projectPath)) {
|
|
15167
15343
|
console.log(`[Daemon] Already connected to ${projectPath}`);
|
|
@@ -15172,7 +15348,7 @@ var Daemon = class _Daemon {
|
|
|
15172
15348
|
const maxWait = 35e3;
|
|
15173
15349
|
const startTime = Date.now();
|
|
15174
15350
|
while (this.connectionManager.hasPendingConnection(projectPath) && Date.now() - startTime < maxWait) {
|
|
15175
|
-
await new Promise((
|
|
15351
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
15176
15352
|
}
|
|
15177
15353
|
if (this.connectionManager.hasLiveConnection(projectPath)) {
|
|
15178
15354
|
console.log(`[Daemon] Pending connection succeeded for ${projectPath}`);
|
|
@@ -15183,6 +15359,12 @@ var Daemon = class _Daemon {
|
|
|
15183
15359
|
console.warn(`[Daemon] Stale connection detected for ${projectPath}, forcing reconnection`);
|
|
15184
15360
|
await this.disconnectProject(projectPath);
|
|
15185
15361
|
}
|
|
15362
|
+
this.logReliabilityMetric("ws_connect_attempt", {
|
|
15363
|
+
projectId,
|
|
15364
|
+
projectPath,
|
|
15365
|
+
reconnect: isReconnectAttempt,
|
|
15366
|
+
previousState: initialState
|
|
15367
|
+
});
|
|
15186
15368
|
const config = await (0, import_core22.loadConfig)();
|
|
15187
15369
|
if (!config || !config.access_token) {
|
|
15188
15370
|
throw new Error("No access token found. Please run: episoda auth");
|
|
@@ -15197,12 +15379,15 @@ var Daemon = class _Daemon {
|
|
|
15197
15379
|
}
|
|
15198
15380
|
console.log(`[Daemon] Connecting to ${wsEndpoint.wsUrl} for project ${projectId} (source: ${wsEndpoint.source})...`);
|
|
15199
15381
|
const client = new import_core22.EpisodaClient();
|
|
15382
|
+
client.setAutoReconnect(false);
|
|
15200
15383
|
const gitExecutor = new import_core22.GitExecutor();
|
|
15201
15384
|
const connection = {
|
|
15202
15385
|
projectId,
|
|
15203
15386
|
projectPath,
|
|
15204
15387
|
client,
|
|
15205
|
-
gitExecutor
|
|
15388
|
+
gitExecutor,
|
|
15389
|
+
state: "connecting",
|
|
15390
|
+
lastTransitionAt: Date.now()
|
|
15206
15391
|
};
|
|
15207
15392
|
this.connectionManager.setConnection(projectPath, connection);
|
|
15208
15393
|
this.connectionManager.addPendingConnection(projectPath);
|
|
@@ -15540,6 +15725,7 @@ var Daemon = class _Daemon {
|
|
|
15540
15725
|
}
|
|
15541
15726
|
});
|
|
15542
15727
|
client.on("auth_success", async (message) => {
|
|
15728
|
+
client.setAutoReconnect(true);
|
|
15543
15729
|
console.log(`[Daemon] Authenticated for project ${projectId}`);
|
|
15544
15730
|
touchProject(projectPath);
|
|
15545
15731
|
this.connectionManager.addLiveConnection(projectPath);
|
|
@@ -15653,9 +15839,59 @@ var Daemon = class _Daemon {
|
|
|
15653
15839
|
console.error("[Daemon] EP1237: Error handling agent_reconciliation_commands:", error instanceof Error ? error.message : error);
|
|
15654
15840
|
}
|
|
15655
15841
|
});
|
|
15842
|
+
client.on("reconnect_scheduled", (event) => {
|
|
15843
|
+
const reconnectEvent = event;
|
|
15844
|
+
this.logReliabilityMetric("ws_transport_reconnect_scheduled", {
|
|
15845
|
+
projectId,
|
|
15846
|
+
projectPath,
|
|
15847
|
+
attempt: reconnectEvent.attempt,
|
|
15848
|
+
delayMs: reconnectEvent.delayMs,
|
|
15849
|
+
strategy: reconnectEvent.strategy
|
|
15850
|
+
});
|
|
15851
|
+
});
|
|
15852
|
+
client.on("reconnect_attempt", (event) => {
|
|
15853
|
+
const reconnectEvent = event;
|
|
15854
|
+
this.logReliabilityMetric("ws_transport_reconnect_attempt", {
|
|
15855
|
+
projectId,
|
|
15856
|
+
projectPath,
|
|
15857
|
+
attempt: reconnectEvent.attempt
|
|
15858
|
+
});
|
|
15859
|
+
});
|
|
15860
|
+
client.on("reconnect_result", (event) => {
|
|
15861
|
+
const reconnectEvent = event;
|
|
15862
|
+
this.logReliabilityMetric("ws_transport_reconnect_result", {
|
|
15863
|
+
projectId,
|
|
15864
|
+
projectPath,
|
|
15865
|
+
attempt: reconnectEvent.attempt,
|
|
15866
|
+
success: reconnectEvent.success,
|
|
15867
|
+
error: reconnectEvent.error
|
|
15868
|
+
});
|
|
15869
|
+
});
|
|
15870
|
+
client.on("reconnect_exhausted", (event) => {
|
|
15871
|
+
const reconnectEvent = event;
|
|
15872
|
+
this.logReliabilityMetric("ws_transport_reconnect_exhausted", {
|
|
15873
|
+
projectId,
|
|
15874
|
+
projectPath,
|
|
15875
|
+
attempts: reconnectEvent.attempts,
|
|
15876
|
+
reason: reconnectEvent.reason
|
|
15877
|
+
});
|
|
15878
|
+
this.logReliabilityMetric("manual_reconnect_needed", {
|
|
15879
|
+
source: "daemon",
|
|
15880
|
+
projectId,
|
|
15881
|
+
projectPath,
|
|
15882
|
+
reason: reconnectEvent.reason
|
|
15883
|
+
});
|
|
15884
|
+
});
|
|
15656
15885
|
client.on("disconnected", (event) => {
|
|
15657
15886
|
const disconnectEvent = event;
|
|
15658
15887
|
console.log(`[Daemon] Connection closed for ${projectId}: code=${disconnectEvent.code}, willReconnect=${disconnectEvent.willReconnect}`);
|
|
15888
|
+
this.logReliabilityMetric("ws_transport_disconnect", {
|
|
15889
|
+
projectId,
|
|
15890
|
+
projectPath,
|
|
15891
|
+
code: disconnectEvent.code,
|
|
15892
|
+
reason: disconnectEvent.reason,
|
|
15893
|
+
willReconnect: disconnectEvent.willReconnect
|
|
15894
|
+
});
|
|
15659
15895
|
this.connectionManager.removeLiveConnection(projectPath);
|
|
15660
15896
|
if (this.connectionManager.liveConnectionCount() === 0) {
|
|
15661
15897
|
this.lastDisconnectAt = this.lastDisconnectAt || Date.now();
|
|
@@ -15693,10 +15929,36 @@ var Daemon = class _Daemon {
|
|
|
15693
15929
|
containerId
|
|
15694
15930
|
});
|
|
15695
15931
|
console.log(`[Daemon] Successfully connected to project ${projectId}`);
|
|
15932
|
+
this.connectionManager.setState(projectPath, "authenticating");
|
|
15696
15933
|
await this.connectionManager.waitForAuthentication(client, 3e4);
|
|
15697
15934
|
console.log(`[Daemon] Authentication complete for project ${projectId}`);
|
|
15935
|
+
this.logReliabilityMetric("ws_connect_result", {
|
|
15936
|
+
projectId,
|
|
15937
|
+
projectPath,
|
|
15938
|
+
reconnect: isReconnectAttempt,
|
|
15939
|
+
success: true,
|
|
15940
|
+
durationMs: Date.now() - connectAttemptStartedAt
|
|
15941
|
+
});
|
|
15698
15942
|
} catch (error) {
|
|
15699
15943
|
console.error(`[Daemon] Failed to connect to ${projectId}:`, error);
|
|
15944
|
+
this.logReliabilityMetric("ws_connect_result", {
|
|
15945
|
+
projectId,
|
|
15946
|
+
projectPath,
|
|
15947
|
+
reconnect: isReconnectAttempt,
|
|
15948
|
+
success: false,
|
|
15949
|
+
durationMs: Date.now() - connectAttemptStartedAt,
|
|
15950
|
+
error: error instanceof Error ? error.message : String(error)
|
|
15951
|
+
});
|
|
15952
|
+
try {
|
|
15953
|
+
await client.disconnect(true);
|
|
15954
|
+
} catch {
|
|
15955
|
+
}
|
|
15956
|
+
client.clearAllHandlers();
|
|
15957
|
+
this.connectionManager.setState(
|
|
15958
|
+
projectPath,
|
|
15959
|
+
"disconnected",
|
|
15960
|
+
error instanceof Error ? error.message : String(error)
|
|
15961
|
+
);
|
|
15700
15962
|
this.connectionManager.deleteConnection(projectPath);
|
|
15701
15963
|
this.connectionManager.removePendingConnection(projectPath);
|
|
15702
15964
|
throw error;
|
|
@@ -16128,14 +16390,13 @@ var Daemon = class _Daemon {
|
|
|
16128
16390
|
console.log(`[Daemon] EP1002: ${installCmd.description} (detected from ${installCmd.detectedFrom})`);
|
|
16129
16391
|
console.log(`[Daemon] EP1002: Running: ${installCmd.command.join(" ")}`);
|
|
16130
16392
|
try {
|
|
16131
|
-
|
|
16132
|
-
|
|
16133
|
-
|
|
16134
|
-
|
|
16135
|
-
|
|
16136
|
-
|
|
16137
|
-
|
|
16138
|
-
});
|
|
16393
|
+
await this.runForegroundCommand(
|
|
16394
|
+
installCmd.command[0],
|
|
16395
|
+
installCmd.command.slice(1),
|
|
16396
|
+
worktreePath,
|
|
16397
|
+
{ ...process.env, CI: "true" },
|
|
16398
|
+
10 * 60 * 1e3
|
|
16399
|
+
);
|
|
16139
16400
|
installSucceeded = true;
|
|
16140
16401
|
console.log(`[Daemon] EP1002: Dependencies installed successfully`);
|
|
16141
16402
|
} catch (installError) {
|
|
@@ -16151,13 +16412,12 @@ var Daemon = class _Daemon {
|
|
|
16151
16412
|
const bootstrapCmd = buildCmd;
|
|
16152
16413
|
console.log(`[Daemon] EP1386: Bootstrapping packages after dependency install (${bootstrapCmd})`);
|
|
16153
16414
|
try {
|
|
16154
|
-
|
|
16155
|
-
|
|
16156
|
-
|
|
16157
|
-
|
|
16158
|
-
|
|
16159
|
-
|
|
16160
|
-
});
|
|
16415
|
+
await this.runShellForegroundCommand(
|
|
16416
|
+
bootstrapCmd,
|
|
16417
|
+
worktreePath,
|
|
16418
|
+
{ ...process.env, CI: "true" },
|
|
16419
|
+
10 * 60 * 1e3
|
|
16420
|
+
);
|
|
16161
16421
|
console.log("[Daemon] EP1386: Package bootstrap completed");
|
|
16162
16422
|
} catch (buildError) {
|
|
16163
16423
|
const errorMsg = buildError instanceof Error ? buildError.message : String(buildError);
|
|
@@ -16177,6 +16437,74 @@ var Daemon = class _Daemon {
|
|
|
16177
16437
|
await this.updateModuleWorktreeStatus(moduleUid, "ready", worktreePath);
|
|
16178
16438
|
console.log(`[Daemon] EP1002: Worktree setup complete for ${moduleUid}`);
|
|
16179
16439
|
}
|
|
16440
|
+
async runForegroundCommand(command, args, cwd, env, timeoutMs) {
|
|
16441
|
+
await new Promise((resolve7, reject) => {
|
|
16442
|
+
const commandLabel = `${command} ${args.join(" ")}`.trim();
|
|
16443
|
+
const child = (0, import_child_process19.spawn)(command, args, {
|
|
16444
|
+
cwd,
|
|
16445
|
+
env,
|
|
16446
|
+
stdio: "inherit",
|
|
16447
|
+
shell: false
|
|
16448
|
+
});
|
|
16449
|
+
let settled = false;
|
|
16450
|
+
let timedOut = false;
|
|
16451
|
+
const timeoutSeconds = Math.round(timeoutMs / 1e3);
|
|
16452
|
+
const termGraceMs = 5e3;
|
|
16453
|
+
let termTimer = null;
|
|
16454
|
+
let killTimer = null;
|
|
16455
|
+
const finish = (error) => {
|
|
16456
|
+
if (settled) return;
|
|
16457
|
+
settled = true;
|
|
16458
|
+
if (termTimer) clearTimeout(termTimer);
|
|
16459
|
+
if (killTimer) clearTimeout(killTimer);
|
|
16460
|
+
if (error) {
|
|
16461
|
+
reject(error);
|
|
16462
|
+
return;
|
|
16463
|
+
}
|
|
16464
|
+
resolve7();
|
|
16465
|
+
};
|
|
16466
|
+
termTimer = setTimeout(() => {
|
|
16467
|
+
timedOut = true;
|
|
16468
|
+
try {
|
|
16469
|
+
child.kill("SIGTERM");
|
|
16470
|
+
} catch {
|
|
16471
|
+
}
|
|
16472
|
+
killTimer = setTimeout(() => {
|
|
16473
|
+
if (settled) return;
|
|
16474
|
+
try {
|
|
16475
|
+
child.kill("SIGKILL");
|
|
16476
|
+
} catch {
|
|
16477
|
+
}
|
|
16478
|
+
finish(
|
|
16479
|
+
new Error(`Command timed out after ${timeoutSeconds}s (terminated with SIGKILL fallback): ${commandLabel}`)
|
|
16480
|
+
);
|
|
16481
|
+
}, termGraceMs);
|
|
16482
|
+
}, timeoutMs);
|
|
16483
|
+
child.on("error", (error) => {
|
|
16484
|
+
finish(error);
|
|
16485
|
+
});
|
|
16486
|
+
child.on("exit", (code, signal) => {
|
|
16487
|
+
if (timedOut) {
|
|
16488
|
+
finish(
|
|
16489
|
+
new Error(`Command timed out after ${timeoutSeconds}s (exit code=${code ?? "null"}, signal=${signal ?? "none"}): ${commandLabel}`)
|
|
16490
|
+
);
|
|
16491
|
+
return;
|
|
16492
|
+
}
|
|
16493
|
+
if (code === 0) {
|
|
16494
|
+
finish();
|
|
16495
|
+
return;
|
|
16496
|
+
}
|
|
16497
|
+
finish(new Error(`Command failed (code=${code ?? "null"}, signal=${signal ?? "none"}): ${commandLabel}`));
|
|
16498
|
+
});
|
|
16499
|
+
});
|
|
16500
|
+
}
|
|
16501
|
+
async runShellForegroundCommand(command, cwd, env, timeoutMs) {
|
|
16502
|
+
if (process.platform === "win32") {
|
|
16503
|
+
await this.runForegroundCommand("cmd", ["/d", "/s", "/c", command], cwd, env, timeoutMs);
|
|
16504
|
+
return;
|
|
16505
|
+
}
|
|
16506
|
+
await this.runForegroundCommand("sh", ["-lc", command], cwd, env, timeoutMs);
|
|
16507
|
+
}
|
|
16180
16508
|
// EP1003: startTunnelForModule removed - server now orchestrates via tunnel_start commands
|
|
16181
16509
|
// EP1003: autoStartTunnelsForProject removed - server now orchestrates via reconciliation
|
|
16182
16510
|
// Recovery flow: daemon sends reconciliation_report → server processes and sends commands
|