episoda 0.2.39 → 0.2.41
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.
|
@@ -2726,7 +2726,7 @@ var require_package = __commonJS({
|
|
|
2726
2726
|
"package.json"(exports2, module2) {
|
|
2727
2727
|
module2.exports = {
|
|
2728
2728
|
name: "episoda",
|
|
2729
|
-
version: "0.2.
|
|
2729
|
+
version: "0.2.41",
|
|
2730
2730
|
description: "CLI tool for Episoda local development workflow orchestration",
|
|
2731
2731
|
main: "dist/index.js",
|
|
2732
2732
|
types: "dist/index.d.ts",
|
|
@@ -4018,7 +4018,134 @@ var import_events = require("events");
|
|
|
4018
4018
|
var fs6 = __toESM(require("fs"));
|
|
4019
4019
|
var path7 = __toESM(require("path"));
|
|
4020
4020
|
var os2 = __toESM(require("os"));
|
|
4021
|
+
|
|
4022
|
+
// src/tunnel/tunnel-api.ts
|
|
4023
|
+
var import_core6 = __toESM(require_dist());
|
|
4024
|
+
async function provisionNamedTunnel(moduleId) {
|
|
4025
|
+
const config = await (0, import_core6.loadConfig)();
|
|
4026
|
+
if (!config?.access_token) {
|
|
4027
|
+
return { success: false, error: "Not authenticated" };
|
|
4028
|
+
}
|
|
4029
|
+
try {
|
|
4030
|
+
const apiUrl = config.api_url || "https://episoda.dev";
|
|
4031
|
+
const response = await fetch(`${apiUrl}/api/tunnels`, {
|
|
4032
|
+
method: "POST",
|
|
4033
|
+
headers: {
|
|
4034
|
+
"Authorization": `Bearer ${config.access_token}`,
|
|
4035
|
+
"Content-Type": "application/json"
|
|
4036
|
+
},
|
|
4037
|
+
body: JSON.stringify({ module_id: moduleId })
|
|
4038
|
+
});
|
|
4039
|
+
const data = await response.json();
|
|
4040
|
+
if (!response.ok) {
|
|
4041
|
+
return {
|
|
4042
|
+
success: false,
|
|
4043
|
+
error: data.error?.message || data.message || `HTTP ${response.status}`
|
|
4044
|
+
};
|
|
4045
|
+
}
|
|
4046
|
+
return {
|
|
4047
|
+
success: true,
|
|
4048
|
+
tunnel: data.data?.tunnel,
|
|
4049
|
+
message: data.data?.message
|
|
4050
|
+
};
|
|
4051
|
+
} catch (error) {
|
|
4052
|
+
return {
|
|
4053
|
+
success: false,
|
|
4054
|
+
error: error instanceof Error ? error.message : "Failed to provision tunnel"
|
|
4055
|
+
};
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
async function provisionNamedTunnelByUid(moduleUid) {
|
|
4059
|
+
const config = await (0, import_core6.loadConfig)();
|
|
4060
|
+
if (!config?.access_token) {
|
|
4061
|
+
return { success: false, error: "Not authenticated" };
|
|
4062
|
+
}
|
|
4063
|
+
try {
|
|
4064
|
+
const apiUrl = config.api_url || "https://episoda.dev";
|
|
4065
|
+
const moduleResponse = await fetch(`${apiUrl}/api/modules/${moduleUid}`, {
|
|
4066
|
+
headers: {
|
|
4067
|
+
"Authorization": `Bearer ${config.access_token}`
|
|
4068
|
+
}
|
|
4069
|
+
});
|
|
4070
|
+
if (!moduleResponse.ok) {
|
|
4071
|
+
return {
|
|
4072
|
+
success: false,
|
|
4073
|
+
error: `Failed to find module ${moduleUid}`
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
const moduleData = await moduleResponse.json();
|
|
4077
|
+
const moduleId = moduleData.data?.id;
|
|
4078
|
+
if (!moduleId) {
|
|
4079
|
+
return {
|
|
4080
|
+
success: false,
|
|
4081
|
+
error: `Module ${moduleUid} has no ID`
|
|
4082
|
+
};
|
|
4083
|
+
}
|
|
4084
|
+
return provisionNamedTunnel(moduleId);
|
|
4085
|
+
} catch (error) {
|
|
4086
|
+
return {
|
|
4087
|
+
success: false,
|
|
4088
|
+
error: error instanceof Error ? error.message : "Failed to provision tunnel"
|
|
4089
|
+
};
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
async function updateTunnelStatus(moduleUid, status, error) {
|
|
4093
|
+
if (!moduleUid || moduleUid === "LOCAL") {
|
|
4094
|
+
return;
|
|
4095
|
+
}
|
|
4096
|
+
const config = await (0, import_core6.loadConfig)();
|
|
4097
|
+
if (!config?.access_token) {
|
|
4098
|
+
return;
|
|
4099
|
+
}
|
|
4100
|
+
try {
|
|
4101
|
+
const apiUrl = config.api_url || "https://episoda.dev";
|
|
4102
|
+
await fetch(`${apiUrl}/api/modules/${moduleUid}/tunnel`, {
|
|
4103
|
+
method: "PATCH",
|
|
4104
|
+
headers: {
|
|
4105
|
+
"Authorization": `Bearer ${config.access_token}`,
|
|
4106
|
+
"Content-Type": "application/json"
|
|
4107
|
+
},
|
|
4108
|
+
body: JSON.stringify({
|
|
4109
|
+
tunnel_health_status: status,
|
|
4110
|
+
tunnel_last_health_check: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4111
|
+
...error && { tunnel_error: error }
|
|
4112
|
+
})
|
|
4113
|
+
});
|
|
4114
|
+
} catch {
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
async function clearTunnelUrl(moduleUid) {
|
|
4118
|
+
if (!moduleUid || moduleUid === "LOCAL") {
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
const config = await (0, import_core6.loadConfig)();
|
|
4122
|
+
if (!config?.access_token) {
|
|
4123
|
+
return;
|
|
4124
|
+
}
|
|
4125
|
+
try {
|
|
4126
|
+
const apiUrl = config.api_url || "https://episoda.dev";
|
|
4127
|
+
await fetch(`${apiUrl}/api/modules/${moduleUid}/tunnel`, {
|
|
4128
|
+
method: "DELETE",
|
|
4129
|
+
headers: {
|
|
4130
|
+
"Authorization": `Bearer ${config.access_token}`
|
|
4131
|
+
}
|
|
4132
|
+
});
|
|
4133
|
+
} catch {
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
// src/tunnel/tunnel-manager.ts
|
|
4021
4138
|
var TUNNEL_PID_DIR = path7.join(os2.homedir(), ".episoda", "tunnels");
|
|
4139
|
+
var TUNNEL_TIMEOUTS = {
|
|
4140
|
+
/** Time to wait for Named Tunnel connection (includes API token fetch + connect) */
|
|
4141
|
+
NAMED_TUNNEL_CONNECT: 6e4,
|
|
4142
|
+
/** Time to wait for Quick Tunnel connection (simpler, faster connection) */
|
|
4143
|
+
QUICK_TUNNEL_CONNECT: 3e4,
|
|
4144
|
+
/** Time to wait for cloudflared process to start before giving up */
|
|
4145
|
+
PROCESS_START: 1e4,
|
|
4146
|
+
/** Grace period after starting cloudflared before checking status */
|
|
4147
|
+
STARTUP_GRACE: 2e3
|
|
4148
|
+
};
|
|
4022
4149
|
var TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
4023
4150
|
var DEFAULT_RECONNECT_CONFIG = {
|
|
4024
4151
|
maxRetries: 5,
|
|
@@ -4075,6 +4202,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4075
4202
|
}
|
|
4076
4203
|
/**
|
|
4077
4204
|
* EP877: Read PID from file
|
|
4205
|
+
* EP948: Enhanced with validation and stale file cleanup
|
|
4078
4206
|
*/
|
|
4079
4207
|
readPidFile(moduleUid) {
|
|
4080
4208
|
try {
|
|
@@ -4082,9 +4210,25 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4082
4210
|
if (!fs6.existsSync(pidPath)) {
|
|
4083
4211
|
return null;
|
|
4084
4212
|
}
|
|
4085
|
-
const
|
|
4086
|
-
|
|
4213
|
+
const content = fs6.readFileSync(pidPath, "utf8").trim();
|
|
4214
|
+
const pid = parseInt(content, 10);
|
|
4215
|
+
if (isNaN(pid) || pid <= 0) {
|
|
4216
|
+
console.warn(`[Tunnel] EP948: Invalid PID file content for ${moduleUid}: "${content}", removing stale file`);
|
|
4217
|
+
this.removePidFile(moduleUid);
|
|
4218
|
+
return null;
|
|
4219
|
+
}
|
|
4220
|
+
if (!this.isProcessRunning(pid)) {
|
|
4221
|
+
console.log(`[Tunnel] EP948: PID ${pid} for ${moduleUid} is not running, removing stale file`);
|
|
4222
|
+
this.removePidFile(moduleUid);
|
|
4223
|
+
return null;
|
|
4224
|
+
}
|
|
4225
|
+
return pid;
|
|
4087
4226
|
} catch (error) {
|
|
4227
|
+
console.warn(`[Tunnel] EP948: Failed to read PID file for ${moduleUid}: ${error.message}`);
|
|
4228
|
+
try {
|
|
4229
|
+
this.removePidFile(moduleUid);
|
|
4230
|
+
} catch {
|
|
4231
|
+
}
|
|
4088
4232
|
return null;
|
|
4089
4233
|
}
|
|
4090
4234
|
}
|
|
@@ -4288,10 +4432,190 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4288
4432
|
}, delay);
|
|
4289
4433
|
}
|
|
4290
4434
|
/**
|
|
4291
|
-
*
|
|
4292
|
-
*
|
|
4435
|
+
* EP948: Start a Named Tunnel using a pre-provisioned token
|
|
4436
|
+
* Named Tunnels connect to a persistent tunnel created via Cloudflare API
|
|
4437
|
+
*/
|
|
4438
|
+
async startNamedTunnelProcess(options, existingState) {
|
|
4439
|
+
const { moduleUid, port = 3e3, onUrl, onStatusChange, tunnelToken, previewUrl } = options;
|
|
4440
|
+
if (!tunnelToken) {
|
|
4441
|
+
return { success: false, error: "Named tunnel requires a token" };
|
|
4442
|
+
}
|
|
4443
|
+
if (!this.cloudflaredPath) {
|
|
4444
|
+
try {
|
|
4445
|
+
this.cloudflaredPath = await ensureCloudflared();
|
|
4446
|
+
} catch (error) {
|
|
4447
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4448
|
+
return { success: false, error: `Failed to get cloudflared: ${errorMessage}` };
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
return new Promise((resolve3) => {
|
|
4452
|
+
const tunnelInfo = {
|
|
4453
|
+
moduleUid,
|
|
4454
|
+
url: previewUrl || "",
|
|
4455
|
+
// Named tunnels have a known URL
|
|
4456
|
+
port,
|
|
4457
|
+
status: "starting",
|
|
4458
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
4459
|
+
process: null
|
|
4460
|
+
};
|
|
4461
|
+
console.log(`[Tunnel] EP948: Starting Named Tunnel for ${moduleUid} with preview URL ${previewUrl}`);
|
|
4462
|
+
const process2 = (0, import_child_process6.spawn)(this.cloudflaredPath, [
|
|
4463
|
+
"tunnel",
|
|
4464
|
+
"run",
|
|
4465
|
+
"--token",
|
|
4466
|
+
tunnelToken
|
|
4467
|
+
], {
|
|
4468
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4469
|
+
});
|
|
4470
|
+
tunnelInfo.process = process2;
|
|
4471
|
+
tunnelInfo.pid = process2.pid;
|
|
4472
|
+
if (process2.pid) {
|
|
4473
|
+
this.writePidFile(moduleUid, process2.pid);
|
|
4474
|
+
}
|
|
4475
|
+
const state = existingState || {
|
|
4476
|
+
info: tunnelInfo,
|
|
4477
|
+
options,
|
|
4478
|
+
intentionallyStopped: false,
|
|
4479
|
+
retryCount: 0,
|
|
4480
|
+
retryTimeoutId: null
|
|
4481
|
+
};
|
|
4482
|
+
state.info = tunnelInfo;
|
|
4483
|
+
this.tunnelStates.set(moduleUid, state);
|
|
4484
|
+
let connected = false;
|
|
4485
|
+
let stderrBuffer = "";
|
|
4486
|
+
const connectionPatterns = [
|
|
4487
|
+
/Connection.*registered/i,
|
|
4488
|
+
// "Connection [id] registered"
|
|
4489
|
+
/Registered tunnel connection/i,
|
|
4490
|
+
// Alternative format
|
|
4491
|
+
/connected to.*connector/i,
|
|
4492
|
+
// "connected to [colo] connector ID"
|
|
4493
|
+
/Tunnel is ready/i,
|
|
4494
|
+
// Some versions use this
|
|
4495
|
+
/ingress.*rules.*applied/i,
|
|
4496
|
+
// Indicates ingress rules are active
|
|
4497
|
+
/Initial.*connection.*established/i
|
|
4498
|
+
// Initial connection message
|
|
4499
|
+
];
|
|
4500
|
+
const checkConnection = (data) => {
|
|
4501
|
+
if (connected) return;
|
|
4502
|
+
const isConnected = connectionPatterns.some((pattern) => pattern.test(data));
|
|
4503
|
+
if (isConnected) {
|
|
4504
|
+
connected = true;
|
|
4505
|
+
tunnelInfo.status = "connected";
|
|
4506
|
+
tunnelInfo.url = previewUrl || "";
|
|
4507
|
+
console.log(`[Tunnel] EP948: Named Tunnel connected for ${moduleUid}: ${previewUrl}`);
|
|
4508
|
+
updateTunnelStatus(moduleUid, "healthy").catch(() => {
|
|
4509
|
+
});
|
|
4510
|
+
onStatusChange?.("connected");
|
|
4511
|
+
onUrl?.(tunnelInfo.url);
|
|
4512
|
+
this.emitEvent({
|
|
4513
|
+
type: "started",
|
|
4514
|
+
moduleUid,
|
|
4515
|
+
url: tunnelInfo.url
|
|
4516
|
+
});
|
|
4517
|
+
resolve3({ success: true, url: tunnelInfo.url });
|
|
4518
|
+
}
|
|
4519
|
+
};
|
|
4520
|
+
process2.stderr?.on("data", (data) => {
|
|
4521
|
+
stderrBuffer += data.toString();
|
|
4522
|
+
checkConnection(stderrBuffer);
|
|
4523
|
+
});
|
|
4524
|
+
process2.stdout?.on("data", (data) => {
|
|
4525
|
+
checkConnection(data.toString());
|
|
4526
|
+
});
|
|
4527
|
+
process2.on("exit", (code, signal) => {
|
|
4528
|
+
const wasConnected = tunnelInfo.status === "connected";
|
|
4529
|
+
tunnelInfo.status = "disconnected";
|
|
4530
|
+
const currentState = this.tunnelStates.get(moduleUid);
|
|
4531
|
+
if (!connected) {
|
|
4532
|
+
const errorMsg = `Named tunnel process exited with code ${code}`;
|
|
4533
|
+
tunnelInfo.status = "error";
|
|
4534
|
+
tunnelInfo.error = errorMsg;
|
|
4535
|
+
updateTunnelStatus(moduleUid, "error", errorMsg).catch(() => {
|
|
4536
|
+
});
|
|
4537
|
+
if (currentState && !currentState.intentionallyStopped) {
|
|
4538
|
+
this.attemptReconnect(moduleUid);
|
|
4539
|
+
} else {
|
|
4540
|
+
this.tunnelStates.delete(moduleUid);
|
|
4541
|
+
onStatusChange?.("error", errorMsg);
|
|
4542
|
+
this.emitEvent({ type: "error", moduleUid, error: errorMsg });
|
|
4543
|
+
}
|
|
4544
|
+
resolve3({ success: false, error: errorMsg });
|
|
4545
|
+
} else if (wasConnected) {
|
|
4546
|
+
if (currentState && !currentState.intentionallyStopped) {
|
|
4547
|
+
console.log(`[Tunnel] EP948: Named tunnel ${moduleUid} crashed unexpectedly, attempting reconnect...`);
|
|
4548
|
+
updateTunnelStatus(moduleUid, "disconnected").catch(() => {
|
|
4549
|
+
});
|
|
4550
|
+
onStatusChange?.("reconnecting");
|
|
4551
|
+
this.attemptReconnect(moduleUid);
|
|
4552
|
+
} else {
|
|
4553
|
+
updateTunnelStatus(moduleUid, "disconnected").catch(() => {
|
|
4554
|
+
});
|
|
4555
|
+
this.tunnelStates.delete(moduleUid);
|
|
4556
|
+
onStatusChange?.("disconnected");
|
|
4557
|
+
this.emitEvent({ type: "stopped", moduleUid });
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
});
|
|
4561
|
+
process2.on("error", (error) => {
|
|
4562
|
+
tunnelInfo.status = "error";
|
|
4563
|
+
tunnelInfo.error = error.message;
|
|
4564
|
+
updateTunnelStatus(moduleUid, "error", error.message).catch(() => {
|
|
4565
|
+
});
|
|
4566
|
+
const currentState = this.tunnelStates.get(moduleUid);
|
|
4567
|
+
if (currentState && !currentState.intentionallyStopped) {
|
|
4568
|
+
this.attemptReconnect(moduleUid);
|
|
4569
|
+
} else {
|
|
4570
|
+
this.tunnelStates.delete(moduleUid);
|
|
4571
|
+
onStatusChange?.("error", error.message);
|
|
4572
|
+
this.emitEvent({ type: "error", moduleUid, error: error.message });
|
|
4573
|
+
}
|
|
4574
|
+
if (!connected) {
|
|
4575
|
+
resolve3({ success: false, error: error.message });
|
|
4576
|
+
}
|
|
4577
|
+
});
|
|
4578
|
+
setTimeout(() => {
|
|
4579
|
+
if (!connected) {
|
|
4580
|
+
process2.kill();
|
|
4581
|
+
if (stderrBuffer) {
|
|
4582
|
+
console.error(`[Tunnel] EP948: Named tunnel ${moduleUid} stderr before timeout:`);
|
|
4583
|
+
console.error(stderrBuffer.slice(-2e3));
|
|
4584
|
+
}
|
|
4585
|
+
const errorMsg = "Named tunnel connection timed out after 60 seconds. Check logs for cloudflared output.";
|
|
4586
|
+
tunnelInfo.status = "error";
|
|
4587
|
+
tunnelInfo.error = errorMsg;
|
|
4588
|
+
updateTunnelStatus(moduleUid, "error", errorMsg).catch(() => {
|
|
4589
|
+
});
|
|
4590
|
+
const currentState = this.tunnelStates.get(moduleUid);
|
|
4591
|
+
if (currentState && !currentState.intentionallyStopped) {
|
|
4592
|
+
this.attemptReconnect(moduleUid);
|
|
4593
|
+
} else {
|
|
4594
|
+
this.tunnelStates.delete(moduleUid);
|
|
4595
|
+
onStatusChange?.("error", errorMsg);
|
|
4596
|
+
this.emitEvent({ type: "error", moduleUid, error: errorMsg });
|
|
4597
|
+
}
|
|
4598
|
+
resolve3({ success: false, error: errorMsg });
|
|
4599
|
+
}
|
|
4600
|
+
}, TUNNEL_TIMEOUTS.NAMED_TUNNEL_CONNECT);
|
|
4601
|
+
});
|
|
4602
|
+
}
|
|
4603
|
+
/**
|
|
4604
|
+
* EP948: Route to the appropriate tunnel process method based on mode
|
|
4293
4605
|
*/
|
|
4294
4606
|
async startTunnelProcess(options, existingState) {
|
|
4607
|
+
const mode = options.mode || "named";
|
|
4608
|
+
if (mode === "named" && options.tunnelToken) {
|
|
4609
|
+
return this.startNamedTunnelProcess(options, existingState);
|
|
4610
|
+
}
|
|
4611
|
+
console.log(`[Tunnel] EP948: Using Quick Tunnel mode for ${options.moduleUid}`);
|
|
4612
|
+
return this.startQuickTunnelProcess(options, existingState);
|
|
4613
|
+
}
|
|
4614
|
+
/**
|
|
4615
|
+
* EP672-9: Internal method to start the tunnel process (Quick Tunnel mode)
|
|
4616
|
+
* Separated from startTunnel to support reconnection
|
|
4617
|
+
*/
|
|
4618
|
+
async startQuickTunnelProcess(options, existingState) {
|
|
4295
4619
|
const { moduleUid, port = 3e3, onUrl, onStatusChange } = options;
|
|
4296
4620
|
if (!this.cloudflaredPath) {
|
|
4297
4621
|
try {
|
|
@@ -4419,7 +4743,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4419
4743
|
}
|
|
4420
4744
|
resolve3({ success: false, error: errorMsg });
|
|
4421
4745
|
}
|
|
4422
|
-
},
|
|
4746
|
+
}, TUNNEL_TIMEOUTS.QUICK_TUNNEL_CONNECT);
|
|
4423
4747
|
});
|
|
4424
4748
|
}
|
|
4425
4749
|
/**
|
|
@@ -4446,9 +4770,11 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4446
4770
|
* EP877: Internal start implementation with lock already held
|
|
4447
4771
|
* EP901: Enhanced to clean up ALL orphaned cloudflared processes before starting
|
|
4448
4772
|
* EP904: Added port-based deduplication to prevent multiple tunnels on same port
|
|
4773
|
+
* EP948: Added Named Tunnel provisioning via platform API
|
|
4449
4774
|
*/
|
|
4450
4775
|
async startTunnelWithLock(options) {
|
|
4451
4776
|
const { moduleUid, port = 3e3 } = options;
|
|
4777
|
+
let resolvedOptions = { ...options };
|
|
4452
4778
|
const existingState = this.tunnelStates.get(moduleUid);
|
|
4453
4779
|
if (existingState) {
|
|
4454
4780
|
if (existingState.info.status === "connected") {
|
|
@@ -4475,7 +4801,23 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4475
4801
|
if (cleanup.cleaned > 0) {
|
|
4476
4802
|
console.log(`[Tunnel] EP901: Pre-start cleanup removed ${cleanup.cleaned} orphaned processes`);
|
|
4477
4803
|
}
|
|
4478
|
-
|
|
4804
|
+
const mode = resolvedOptions.mode || "named";
|
|
4805
|
+
if (mode === "named" && !resolvedOptions.tunnelToken && moduleUid !== "LOCAL") {
|
|
4806
|
+
console.log(`[Tunnel] EP948: Provisioning Named Tunnel for ${moduleUid}...`);
|
|
4807
|
+
const provisionResult = await provisionNamedTunnelByUid(moduleUid);
|
|
4808
|
+
if (provisionResult.success && provisionResult.tunnel) {
|
|
4809
|
+
console.log(`[Tunnel] EP948: Named Tunnel provisioned: ${provisionResult.tunnel.preview_url}`);
|
|
4810
|
+
resolvedOptions = {
|
|
4811
|
+
...resolvedOptions,
|
|
4812
|
+
tunnelToken: provisionResult.tunnel.token,
|
|
4813
|
+
previewUrl: provisionResult.tunnel.preview_url
|
|
4814
|
+
};
|
|
4815
|
+
} else {
|
|
4816
|
+
console.warn(`[Tunnel] EP948: Named Tunnel provisioning failed: ${provisionResult.error}. Falling back to Quick Tunnel.`);
|
|
4817
|
+
resolvedOptions = { ...resolvedOptions, mode: "quick" };
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
return this.startTunnelProcess(resolvedOptions);
|
|
4479
4821
|
}
|
|
4480
4822
|
/**
|
|
4481
4823
|
* Stop a tunnel for a module
|
|
@@ -4574,28 +4916,6 @@ function getTunnelManager() {
|
|
|
4574
4916
|
return tunnelManagerInstance;
|
|
4575
4917
|
}
|
|
4576
4918
|
|
|
4577
|
-
// src/tunnel/tunnel-api.ts
|
|
4578
|
-
var import_core6 = __toESM(require_dist());
|
|
4579
|
-
async function clearTunnelUrl(moduleUid) {
|
|
4580
|
-
if (!moduleUid || moduleUid === "LOCAL") {
|
|
4581
|
-
return;
|
|
4582
|
-
}
|
|
4583
|
-
const config = await (0, import_core6.loadConfig)();
|
|
4584
|
-
if (!config?.access_token) {
|
|
4585
|
-
return;
|
|
4586
|
-
}
|
|
4587
|
-
try {
|
|
4588
|
-
const apiUrl = config.api_url || "https://episoda.dev";
|
|
4589
|
-
await fetch(`${apiUrl}/api/modules/${moduleUid}/tunnel`, {
|
|
4590
|
-
method: "DELETE",
|
|
4591
|
-
headers: {
|
|
4592
|
-
"Authorization": `Bearer ${config.access_token}`
|
|
4593
|
-
}
|
|
4594
|
-
});
|
|
4595
|
-
} catch {
|
|
4596
|
-
}
|
|
4597
|
-
}
|
|
4598
|
-
|
|
4599
4919
|
// src/agent/claude-binary.ts
|
|
4600
4920
|
var import_child_process7 = require("child_process");
|
|
4601
4921
|
var path8 = __toESM(require("path"));
|
|
@@ -6892,10 +7212,17 @@ var Daemon = class _Daemon {
|
|
|
6892
7212
|
serverUrl = config.project_settings.local_server_url;
|
|
6893
7213
|
console.log(`[Daemon] Using cached server URL: ${serverUrl}`);
|
|
6894
7214
|
}
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
7215
|
+
let wsUrl;
|
|
7216
|
+
if (config.ws_url) {
|
|
7217
|
+
wsUrl = config.ws_url;
|
|
7218
|
+
console.log(`[Daemon] Using configured ws_url: ${wsUrl}`);
|
|
7219
|
+
} else {
|
|
7220
|
+
const serverUrlObj = new URL(serverUrl);
|
|
7221
|
+
const wsProtocol = serverUrlObj.protocol === "https:" ? "wss:" : "ws:";
|
|
7222
|
+
const wsPort = process.env.EPISODA_WS_PORT || "3001";
|
|
7223
|
+
const wsHostname = serverUrlObj.hostname === "episoda.dev" ? "ws.episoda.dev" : serverUrlObj.hostname;
|
|
7224
|
+
wsUrl = `${wsProtocol}//${wsHostname}:${wsPort}`;
|
|
7225
|
+
}
|
|
6899
7226
|
console.log(`[Daemon] Connecting to ${wsUrl} for project ${projectId}...`);
|
|
6900
7227
|
const client = new import_core10.EpisodaClient();
|
|
6901
7228
|
const gitExecutor = new import_core10.GitExecutor();
|