episoda 0.2.15 → 0.2.16
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 +831 -45
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +683 -27
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
|
@@ -1489,15 +1489,15 @@ var require_git_executor = __commonJS({
|
|
|
1489
1489
|
try {
|
|
1490
1490
|
const { stdout: gitDir } = await execAsync("git rev-parse --git-dir", { cwd, timeout: 5e3 });
|
|
1491
1491
|
const gitDirPath = gitDir.trim();
|
|
1492
|
-
const
|
|
1492
|
+
const fs8 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1493
1493
|
const rebaseMergePath = `${gitDirPath}/rebase-merge`;
|
|
1494
1494
|
const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
|
|
1495
1495
|
try {
|
|
1496
|
-
await
|
|
1496
|
+
await fs8.access(rebaseMergePath);
|
|
1497
1497
|
inRebase = true;
|
|
1498
1498
|
} catch {
|
|
1499
1499
|
try {
|
|
1500
|
-
await
|
|
1500
|
+
await fs8.access(rebaseApplyPath);
|
|
1501
1501
|
inRebase = true;
|
|
1502
1502
|
} catch {
|
|
1503
1503
|
inRebase = false;
|
|
@@ -2118,31 +2118,31 @@ var require_auth = __commonJS({
|
|
|
2118
2118
|
exports2.loadConfig = loadConfig2;
|
|
2119
2119
|
exports2.saveConfig = saveConfig2;
|
|
2120
2120
|
exports2.validateToken = validateToken;
|
|
2121
|
-
var
|
|
2122
|
-
var
|
|
2123
|
-
var
|
|
2121
|
+
var fs8 = __importStar(require("fs"));
|
|
2122
|
+
var path9 = __importStar(require("path"));
|
|
2123
|
+
var os3 = __importStar(require("os"));
|
|
2124
2124
|
var child_process_1 = require("child_process");
|
|
2125
2125
|
var DEFAULT_CONFIG_FILE = "config.json";
|
|
2126
2126
|
function getConfigDir5() {
|
|
2127
|
-
return process.env.EPISODA_CONFIG_DIR ||
|
|
2127
|
+
return process.env.EPISODA_CONFIG_DIR || path9.join(os3.homedir(), ".episoda");
|
|
2128
2128
|
}
|
|
2129
2129
|
function getConfigPath(configPath) {
|
|
2130
2130
|
if (configPath) {
|
|
2131
2131
|
return configPath;
|
|
2132
2132
|
}
|
|
2133
|
-
return
|
|
2133
|
+
return path9.join(getConfigDir5(), DEFAULT_CONFIG_FILE);
|
|
2134
2134
|
}
|
|
2135
2135
|
function ensureConfigDir(configPath) {
|
|
2136
|
-
const dir =
|
|
2137
|
-
const isNew = !
|
|
2136
|
+
const dir = path9.dirname(configPath);
|
|
2137
|
+
const isNew = !fs8.existsSync(dir);
|
|
2138
2138
|
if (isNew) {
|
|
2139
|
-
|
|
2139
|
+
fs8.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2140
2140
|
}
|
|
2141
2141
|
if (process.platform === "darwin") {
|
|
2142
|
-
const nosyncPath =
|
|
2143
|
-
if (isNew || !
|
|
2142
|
+
const nosyncPath = path9.join(dir, ".nosync");
|
|
2143
|
+
if (isNew || !fs8.existsSync(nosyncPath)) {
|
|
2144
2144
|
try {
|
|
2145
|
-
|
|
2145
|
+
fs8.writeFileSync(nosyncPath, "", { mode: 384 });
|
|
2146
2146
|
(0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
|
|
2147
2147
|
stdio: "ignore",
|
|
2148
2148
|
timeout: 5e3
|
|
@@ -2154,11 +2154,11 @@ var require_auth = __commonJS({
|
|
|
2154
2154
|
}
|
|
2155
2155
|
async function loadConfig2(configPath) {
|
|
2156
2156
|
const fullPath = getConfigPath(configPath);
|
|
2157
|
-
if (!
|
|
2157
|
+
if (!fs8.existsSync(fullPath)) {
|
|
2158
2158
|
return null;
|
|
2159
2159
|
}
|
|
2160
2160
|
try {
|
|
2161
|
-
const content =
|
|
2161
|
+
const content = fs8.readFileSync(fullPath, "utf8");
|
|
2162
2162
|
const config = JSON.parse(content);
|
|
2163
2163
|
return config;
|
|
2164
2164
|
} catch (error) {
|
|
@@ -2171,7 +2171,7 @@ var require_auth = __commonJS({
|
|
|
2171
2171
|
ensureConfigDir(fullPath);
|
|
2172
2172
|
try {
|
|
2173
2173
|
const content = JSON.stringify(config, null, 2);
|
|
2174
|
-
|
|
2174
|
+
fs8.writeFileSync(fullPath, content, { mode: 384 });
|
|
2175
2175
|
} catch (error) {
|
|
2176
2176
|
throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
|
|
2177
2177
|
}
|
|
@@ -2280,7 +2280,7 @@ var require_package = __commonJS({
|
|
|
2280
2280
|
"package.json"(exports2, module2) {
|
|
2281
2281
|
module2.exports = {
|
|
2282
2282
|
name: "episoda",
|
|
2283
|
-
version: "0.2.
|
|
2283
|
+
version: "0.2.15",
|
|
2284
2284
|
description: "CLI tool for Episoda local development workflow orchestration",
|
|
2285
2285
|
main: "dist/index.js",
|
|
2286
2286
|
types: "dist/index.d.ts",
|
|
@@ -2307,6 +2307,7 @@ var require_package = __commonJS({
|
|
|
2307
2307
|
commander: "^11.1.0",
|
|
2308
2308
|
ora: "^5.4.1",
|
|
2309
2309
|
semver: "7.7.3",
|
|
2310
|
+
tar: "7.5.2",
|
|
2310
2311
|
ws: "^8.18.0",
|
|
2311
2312
|
zod: "^4.0.10"
|
|
2312
2313
|
},
|
|
@@ -2314,6 +2315,7 @@ var require_package = __commonJS({
|
|
|
2314
2315
|
"@episoda/core": "*",
|
|
2315
2316
|
"@types/node": "^20.11.24",
|
|
2316
2317
|
"@types/semver": "7.7.1",
|
|
2318
|
+
"@types/tar": "6.1.13",
|
|
2317
2319
|
"@types/ws": "^8.5.10",
|
|
2318
2320
|
tsup: "8.5.1",
|
|
2319
2321
|
typescript: "^5.3.3"
|
|
@@ -3152,10 +3154,643 @@ async function handleExec(command, projectPath) {
|
|
|
3152
3154
|
});
|
|
3153
3155
|
}
|
|
3154
3156
|
|
|
3155
|
-
// src/
|
|
3157
|
+
// src/tunnel/cloudflared-manager.ts
|
|
3158
|
+
var import_child_process4 = require("child_process");
|
|
3156
3159
|
var fs5 = __toESM(require("fs"));
|
|
3157
|
-
var os = __toESM(require("os"));
|
|
3158
3160
|
var path6 = __toESM(require("path"));
|
|
3161
|
+
var os = __toESM(require("os"));
|
|
3162
|
+
var https = __toESM(require("https"));
|
|
3163
|
+
var tar = __toESM(require("tar"));
|
|
3164
|
+
var DOWNLOAD_URLS = {
|
|
3165
|
+
darwin: {
|
|
3166
|
+
arm64: "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz",
|
|
3167
|
+
x64: "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz"
|
|
3168
|
+
},
|
|
3169
|
+
linux: {
|
|
3170
|
+
arm64: "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64",
|
|
3171
|
+
x64: "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64"
|
|
3172
|
+
},
|
|
3173
|
+
win32: {
|
|
3174
|
+
x64: "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe",
|
|
3175
|
+
ia32: "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-386.exe"
|
|
3176
|
+
}
|
|
3177
|
+
};
|
|
3178
|
+
function getEpisodaBinDir() {
|
|
3179
|
+
return path6.join(os.homedir(), ".episoda", "bin");
|
|
3180
|
+
}
|
|
3181
|
+
function getCloudflaredPath() {
|
|
3182
|
+
const binaryName = os.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
3183
|
+
return path6.join(getEpisodaBinDir(), binaryName);
|
|
3184
|
+
}
|
|
3185
|
+
function isCloudflaredInPath() {
|
|
3186
|
+
try {
|
|
3187
|
+
const command = os.platform() === "win32" ? "where" : "which";
|
|
3188
|
+
const binaryName = os.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
3189
|
+
const result = (0, import_child_process4.spawnSync)(command, [binaryName], { encoding: "utf-8" });
|
|
3190
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
3191
|
+
return result.stdout.trim().split("\n")[0].trim();
|
|
3192
|
+
}
|
|
3193
|
+
} catch {
|
|
3194
|
+
}
|
|
3195
|
+
return null;
|
|
3196
|
+
}
|
|
3197
|
+
function isCloudflaredInstalled() {
|
|
3198
|
+
const cloudflaredPath = getCloudflaredPath();
|
|
3199
|
+
try {
|
|
3200
|
+
fs5.accessSync(cloudflaredPath, fs5.constants.X_OK);
|
|
3201
|
+
return true;
|
|
3202
|
+
} catch {
|
|
3203
|
+
return false;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
function verifyCloudflared(binaryPath) {
|
|
3207
|
+
try {
|
|
3208
|
+
const result = (0, import_child_process4.spawnSync)(binaryPath, ["version"], { encoding: "utf-8", timeout: 5e3 });
|
|
3209
|
+
return result.status === 0 && result.stdout.includes("cloudflared");
|
|
3210
|
+
} catch {
|
|
3211
|
+
return false;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
function getDownloadUrl() {
|
|
3215
|
+
const platform3 = os.platform();
|
|
3216
|
+
const arch3 = os.arch();
|
|
3217
|
+
const platformUrls = DOWNLOAD_URLS[platform3];
|
|
3218
|
+
if (!platformUrls) {
|
|
3219
|
+
return null;
|
|
3220
|
+
}
|
|
3221
|
+
return platformUrls[arch3] || null;
|
|
3222
|
+
}
|
|
3223
|
+
async function downloadFile(url, destPath) {
|
|
3224
|
+
return new Promise((resolve2, reject) => {
|
|
3225
|
+
const followRedirect = (currentUrl, redirectCount = 0) => {
|
|
3226
|
+
if (redirectCount > 5) {
|
|
3227
|
+
reject(new Error("Too many redirects"));
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3230
|
+
const urlObj = new URL(currentUrl);
|
|
3231
|
+
const options = {
|
|
3232
|
+
hostname: urlObj.hostname,
|
|
3233
|
+
path: urlObj.pathname + urlObj.search,
|
|
3234
|
+
headers: {
|
|
3235
|
+
"User-Agent": "episoda-cli"
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
3238
|
+
https.get(options, (response) => {
|
|
3239
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
3240
|
+
const redirectUrl = response.headers.location;
|
|
3241
|
+
if (redirectUrl) {
|
|
3242
|
+
followRedirect(redirectUrl, redirectCount + 1);
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
if (response.statusCode !== 200) {
|
|
3247
|
+
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
const file = fs5.createWriteStream(destPath);
|
|
3251
|
+
response.pipe(file);
|
|
3252
|
+
file.on("finish", () => {
|
|
3253
|
+
file.close();
|
|
3254
|
+
resolve2();
|
|
3255
|
+
});
|
|
3256
|
+
file.on("error", (err) => {
|
|
3257
|
+
fs5.unlinkSync(destPath);
|
|
3258
|
+
reject(err);
|
|
3259
|
+
});
|
|
3260
|
+
}).on("error", reject);
|
|
3261
|
+
};
|
|
3262
|
+
followRedirect(url);
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
async function extractTgz(archivePath, destDir) {
|
|
3266
|
+
return tar.x({
|
|
3267
|
+
file: archivePath,
|
|
3268
|
+
cwd: destDir
|
|
3269
|
+
});
|
|
3270
|
+
}
|
|
3271
|
+
async function downloadCloudflared() {
|
|
3272
|
+
const url = getDownloadUrl();
|
|
3273
|
+
if (!url) {
|
|
3274
|
+
throw new Error(`Unsupported platform: ${os.platform()} ${os.arch()}`);
|
|
3275
|
+
}
|
|
3276
|
+
const binDir = getEpisodaBinDir();
|
|
3277
|
+
const cloudflaredPath = getCloudflaredPath();
|
|
3278
|
+
fs5.mkdirSync(binDir, { recursive: true });
|
|
3279
|
+
const isTgz = url.endsWith(".tgz");
|
|
3280
|
+
if (isTgz) {
|
|
3281
|
+
const tempFile = path6.join(binDir, "cloudflared.tgz");
|
|
3282
|
+
console.log(`[Tunnel] Downloading cloudflared from ${url}...`);
|
|
3283
|
+
await downloadFile(url, tempFile);
|
|
3284
|
+
console.log("[Tunnel] Extracting cloudflared...");
|
|
3285
|
+
await extractTgz(tempFile, binDir);
|
|
3286
|
+
fs5.unlinkSync(tempFile);
|
|
3287
|
+
} else {
|
|
3288
|
+
console.log(`[Tunnel] Downloading cloudflared from ${url}...`);
|
|
3289
|
+
await downloadFile(url, cloudflaredPath);
|
|
3290
|
+
}
|
|
3291
|
+
if (os.platform() !== "win32") {
|
|
3292
|
+
fs5.chmodSync(cloudflaredPath, 493);
|
|
3293
|
+
}
|
|
3294
|
+
if (!verifyCloudflared(cloudflaredPath)) {
|
|
3295
|
+
throw new Error("Downloaded cloudflared binary failed verification");
|
|
3296
|
+
}
|
|
3297
|
+
console.log("[Tunnel] cloudflared installed successfully");
|
|
3298
|
+
return cloudflaredPath;
|
|
3299
|
+
}
|
|
3300
|
+
async function ensureCloudflared() {
|
|
3301
|
+
const pathBinary = isCloudflaredInPath();
|
|
3302
|
+
if (pathBinary && verifyCloudflared(pathBinary)) {
|
|
3303
|
+
return pathBinary;
|
|
3304
|
+
}
|
|
3305
|
+
const episodaBinary = getCloudflaredPath();
|
|
3306
|
+
if (isCloudflaredInstalled() && verifyCloudflared(episodaBinary)) {
|
|
3307
|
+
return episodaBinary;
|
|
3308
|
+
}
|
|
3309
|
+
return downloadCloudflared();
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
// src/tunnel/tunnel-manager.ts
|
|
3313
|
+
var import_child_process5 = require("child_process");
|
|
3314
|
+
var import_events = require("events");
|
|
3315
|
+
var TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
3316
|
+
var DEFAULT_RECONNECT_CONFIG = {
|
|
3317
|
+
maxRetries: 5,
|
|
3318
|
+
initialDelayMs: 1e3,
|
|
3319
|
+
maxDelayMs: 3e4,
|
|
3320
|
+
backoffMultiplier: 2
|
|
3321
|
+
};
|
|
3322
|
+
var TunnelManager = class extends import_events.EventEmitter {
|
|
3323
|
+
constructor(config) {
|
|
3324
|
+
super();
|
|
3325
|
+
this.tunnelStates = /* @__PURE__ */ new Map();
|
|
3326
|
+
this.cloudflaredPath = null;
|
|
3327
|
+
this.reconnectConfig = { ...DEFAULT_RECONNECT_CONFIG, ...config };
|
|
3328
|
+
}
|
|
3329
|
+
/**
|
|
3330
|
+
* Emit typed tunnel events
|
|
3331
|
+
*/
|
|
3332
|
+
emitEvent(event) {
|
|
3333
|
+
this.emit("tunnel", event);
|
|
3334
|
+
}
|
|
3335
|
+
/**
|
|
3336
|
+
* Initialize the tunnel manager
|
|
3337
|
+
* Ensures cloudflared is available
|
|
3338
|
+
*/
|
|
3339
|
+
async initialize() {
|
|
3340
|
+
this.cloudflaredPath = await ensureCloudflared();
|
|
3341
|
+
}
|
|
3342
|
+
/**
|
|
3343
|
+
* EP672-9: Calculate delay for exponential backoff
|
|
3344
|
+
*/
|
|
3345
|
+
calculateBackoffDelay(retryCount) {
|
|
3346
|
+
const delay = this.reconnectConfig.initialDelayMs * Math.pow(this.reconnectConfig.backoffMultiplier, retryCount);
|
|
3347
|
+
return Math.min(delay, this.reconnectConfig.maxDelayMs);
|
|
3348
|
+
}
|
|
3349
|
+
/**
|
|
3350
|
+
* EP672-9: Attempt to reconnect a crashed tunnel
|
|
3351
|
+
*/
|
|
3352
|
+
async attemptReconnect(moduleUid) {
|
|
3353
|
+
const state = this.tunnelStates.get(moduleUid);
|
|
3354
|
+
if (!state || state.intentionallyStopped) {
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
if (state.retryCount >= this.reconnectConfig.maxRetries) {
|
|
3358
|
+
console.log(`[Tunnel] Max retries (${this.reconnectConfig.maxRetries}) reached for ${moduleUid}, giving up`);
|
|
3359
|
+
this.emitEvent({
|
|
3360
|
+
type: "error",
|
|
3361
|
+
moduleUid,
|
|
3362
|
+
error: `Tunnel failed after ${this.reconnectConfig.maxRetries} reconnection attempts`
|
|
3363
|
+
});
|
|
3364
|
+
state.options.onStatusChange?.("error", "Max reconnection attempts reached");
|
|
3365
|
+
this.tunnelStates.delete(moduleUid);
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
const delay = this.calculateBackoffDelay(state.retryCount);
|
|
3369
|
+
console.log(`[Tunnel] Reconnecting ${moduleUid} in ${delay}ms (attempt ${state.retryCount + 1}/${this.reconnectConfig.maxRetries})`);
|
|
3370
|
+
this.emitEvent({ type: "reconnecting", moduleUid });
|
|
3371
|
+
state.options.onStatusChange?.("reconnecting");
|
|
3372
|
+
state.retryTimeoutId = setTimeout(async () => {
|
|
3373
|
+
state.retryCount++;
|
|
3374
|
+
const result = await this.startTunnelProcess(state.options, state);
|
|
3375
|
+
if (result.success) {
|
|
3376
|
+
console.log(`[Tunnel] Reconnected ${moduleUid} successfully with new URL: ${result.url}`);
|
|
3377
|
+
state.retryCount = 0;
|
|
3378
|
+
}
|
|
3379
|
+
}, delay);
|
|
3380
|
+
}
|
|
3381
|
+
/**
|
|
3382
|
+
* EP672-9: Internal method to start the tunnel process
|
|
3383
|
+
* Separated from startTunnel to support reconnection
|
|
3384
|
+
*/
|
|
3385
|
+
async startTunnelProcess(options, existingState) {
|
|
3386
|
+
const { moduleUid, port = 3e3, onUrl, onStatusChange } = options;
|
|
3387
|
+
if (!this.cloudflaredPath) {
|
|
3388
|
+
try {
|
|
3389
|
+
this.cloudflaredPath = await ensureCloudflared();
|
|
3390
|
+
} catch (error) {
|
|
3391
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3392
|
+
return { success: false, error: `Failed to get cloudflared: ${errorMessage}` };
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
return new Promise((resolve2) => {
|
|
3396
|
+
const tunnelInfo = {
|
|
3397
|
+
moduleUid,
|
|
3398
|
+
url: "",
|
|
3399
|
+
port,
|
|
3400
|
+
status: "starting",
|
|
3401
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
3402
|
+
process: null
|
|
3403
|
+
// Will be set below
|
|
3404
|
+
};
|
|
3405
|
+
const process2 = (0, import_child_process5.spawn)(this.cloudflaredPath, [
|
|
3406
|
+
"tunnel",
|
|
3407
|
+
"--url",
|
|
3408
|
+
`http://localhost:${port}`
|
|
3409
|
+
], {
|
|
3410
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3411
|
+
});
|
|
3412
|
+
tunnelInfo.process = process2;
|
|
3413
|
+
tunnelInfo.pid = process2.pid;
|
|
3414
|
+
const state = existingState || {
|
|
3415
|
+
info: tunnelInfo,
|
|
3416
|
+
options,
|
|
3417
|
+
intentionallyStopped: false,
|
|
3418
|
+
retryCount: 0,
|
|
3419
|
+
retryTimeoutId: null
|
|
3420
|
+
};
|
|
3421
|
+
state.info = tunnelInfo;
|
|
3422
|
+
this.tunnelStates.set(moduleUid, state);
|
|
3423
|
+
let urlFound = false;
|
|
3424
|
+
let stdoutBuffer = "";
|
|
3425
|
+
let stderrBuffer = "";
|
|
3426
|
+
const parseOutput = (data) => {
|
|
3427
|
+
if (urlFound) return;
|
|
3428
|
+
const match = data.match(TUNNEL_URL_REGEX);
|
|
3429
|
+
if (match) {
|
|
3430
|
+
urlFound = true;
|
|
3431
|
+
tunnelInfo.url = match[0];
|
|
3432
|
+
tunnelInfo.status = "connected";
|
|
3433
|
+
onStatusChange?.("connected");
|
|
3434
|
+
onUrl?.(tunnelInfo.url);
|
|
3435
|
+
this.emitEvent({
|
|
3436
|
+
type: "started",
|
|
3437
|
+
moduleUid,
|
|
3438
|
+
url: tunnelInfo.url
|
|
3439
|
+
});
|
|
3440
|
+
resolve2({ success: true, url: tunnelInfo.url });
|
|
3441
|
+
}
|
|
3442
|
+
};
|
|
3443
|
+
process2.stdout?.on("data", (data) => {
|
|
3444
|
+
stdoutBuffer += data.toString();
|
|
3445
|
+
parseOutput(stdoutBuffer);
|
|
3446
|
+
});
|
|
3447
|
+
process2.stderr?.on("data", (data) => {
|
|
3448
|
+
stderrBuffer += data.toString();
|
|
3449
|
+
parseOutput(stderrBuffer);
|
|
3450
|
+
});
|
|
3451
|
+
process2.on("exit", (code, signal) => {
|
|
3452
|
+
const wasConnected = tunnelInfo.status === "connected";
|
|
3453
|
+
tunnelInfo.status = "disconnected";
|
|
3454
|
+
const currentState = this.tunnelStates.get(moduleUid);
|
|
3455
|
+
if (!urlFound) {
|
|
3456
|
+
const errorMsg = `Tunnel process exited with code ${code}`;
|
|
3457
|
+
tunnelInfo.status = "error";
|
|
3458
|
+
tunnelInfo.error = errorMsg;
|
|
3459
|
+
if (currentState && !currentState.intentionallyStopped) {
|
|
3460
|
+
this.attemptReconnect(moduleUid);
|
|
3461
|
+
} else {
|
|
3462
|
+
this.tunnelStates.delete(moduleUid);
|
|
3463
|
+
onStatusChange?.("error", errorMsg);
|
|
3464
|
+
this.emitEvent({ type: "error", moduleUid, error: errorMsg });
|
|
3465
|
+
}
|
|
3466
|
+
resolve2({ success: false, error: errorMsg });
|
|
3467
|
+
} else if (wasConnected) {
|
|
3468
|
+
if (currentState && !currentState.intentionallyStopped) {
|
|
3469
|
+
console.log(`[Tunnel] ${moduleUid} crashed unexpectedly, attempting reconnect...`);
|
|
3470
|
+
onStatusChange?.("reconnecting");
|
|
3471
|
+
this.attemptReconnect(moduleUid);
|
|
3472
|
+
} else {
|
|
3473
|
+
this.tunnelStates.delete(moduleUid);
|
|
3474
|
+
onStatusChange?.("disconnected");
|
|
3475
|
+
this.emitEvent({ type: "stopped", moduleUid });
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
});
|
|
3479
|
+
process2.on("error", (error) => {
|
|
3480
|
+
tunnelInfo.status = "error";
|
|
3481
|
+
tunnelInfo.error = error.message;
|
|
3482
|
+
const currentState = this.tunnelStates.get(moduleUid);
|
|
3483
|
+
if (currentState && !currentState.intentionallyStopped) {
|
|
3484
|
+
this.attemptReconnect(moduleUid);
|
|
3485
|
+
} else {
|
|
3486
|
+
this.tunnelStates.delete(moduleUid);
|
|
3487
|
+
onStatusChange?.("error", error.message);
|
|
3488
|
+
this.emitEvent({ type: "error", moduleUid, error: error.message });
|
|
3489
|
+
}
|
|
3490
|
+
if (!urlFound) {
|
|
3491
|
+
resolve2({ success: false, error: error.message });
|
|
3492
|
+
}
|
|
3493
|
+
});
|
|
3494
|
+
setTimeout(() => {
|
|
3495
|
+
if (!urlFound) {
|
|
3496
|
+
process2.kill();
|
|
3497
|
+
const errorMsg = "Tunnel startup timed out after 30 seconds";
|
|
3498
|
+
tunnelInfo.status = "error";
|
|
3499
|
+
tunnelInfo.error = errorMsg;
|
|
3500
|
+
const currentState = this.tunnelStates.get(moduleUid);
|
|
3501
|
+
if (currentState && !currentState.intentionallyStopped) {
|
|
3502
|
+
this.attemptReconnect(moduleUid);
|
|
3503
|
+
} else {
|
|
3504
|
+
this.tunnelStates.delete(moduleUid);
|
|
3505
|
+
onStatusChange?.("error", errorMsg);
|
|
3506
|
+
this.emitEvent({ type: "error", moduleUid, error: errorMsg });
|
|
3507
|
+
}
|
|
3508
|
+
resolve2({ success: false, error: errorMsg });
|
|
3509
|
+
}
|
|
3510
|
+
}, 3e4);
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
/**
|
|
3514
|
+
* Start a tunnel for a module
|
|
3515
|
+
*/
|
|
3516
|
+
async startTunnel(options) {
|
|
3517
|
+
const { moduleUid } = options;
|
|
3518
|
+
const existingState = this.tunnelStates.get(moduleUid);
|
|
3519
|
+
if (existingState) {
|
|
3520
|
+
if (existingState.info.status === "connected") {
|
|
3521
|
+
return { success: true, url: existingState.info.url };
|
|
3522
|
+
}
|
|
3523
|
+
await this.stopTunnel(moduleUid);
|
|
3524
|
+
}
|
|
3525
|
+
return this.startTunnelProcess(options);
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
3528
|
+
* Stop a tunnel for a module
|
|
3529
|
+
*/
|
|
3530
|
+
async stopTunnel(moduleUid) {
|
|
3531
|
+
const state = this.tunnelStates.get(moduleUid);
|
|
3532
|
+
if (!state) {
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
state.intentionallyStopped = true;
|
|
3536
|
+
if (state.retryTimeoutId) {
|
|
3537
|
+
clearTimeout(state.retryTimeoutId);
|
|
3538
|
+
state.retryTimeoutId = null;
|
|
3539
|
+
}
|
|
3540
|
+
const tunnel = state.info;
|
|
3541
|
+
if (tunnel.process && !tunnel.process.killed) {
|
|
3542
|
+
tunnel.process.kill("SIGTERM");
|
|
3543
|
+
await new Promise((resolve2) => {
|
|
3544
|
+
const timeout = setTimeout(() => {
|
|
3545
|
+
if (tunnel.process && !tunnel.process.killed) {
|
|
3546
|
+
tunnel.process.kill("SIGKILL");
|
|
3547
|
+
}
|
|
3548
|
+
resolve2();
|
|
3549
|
+
}, 3e3);
|
|
3550
|
+
tunnel.process.once("exit", () => {
|
|
3551
|
+
clearTimeout(timeout);
|
|
3552
|
+
resolve2();
|
|
3553
|
+
});
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
3556
|
+
this.tunnelStates.delete(moduleUid);
|
|
3557
|
+
this.emitEvent({ type: "stopped", moduleUid });
|
|
3558
|
+
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Stop all active tunnels
|
|
3561
|
+
*/
|
|
3562
|
+
async stopAllTunnels() {
|
|
3563
|
+
const moduleUids = Array.from(this.tunnelStates.keys());
|
|
3564
|
+
await Promise.all(moduleUids.map((uid) => this.stopTunnel(uid)));
|
|
3565
|
+
}
|
|
3566
|
+
/**
|
|
3567
|
+
* Get information about an active tunnel
|
|
3568
|
+
*/
|
|
3569
|
+
getTunnel(moduleUid) {
|
|
3570
|
+
const state = this.tunnelStates.get(moduleUid);
|
|
3571
|
+
if (!state) return null;
|
|
3572
|
+
const { process: process2, ...info } = state.info;
|
|
3573
|
+
return info;
|
|
3574
|
+
}
|
|
3575
|
+
/**
|
|
3576
|
+
* Get all active tunnels
|
|
3577
|
+
*/
|
|
3578
|
+
getAllTunnels() {
|
|
3579
|
+
return Array.from(this.tunnelStates.values()).map((state) => {
|
|
3580
|
+
const { process: process2, ...info } = state.info;
|
|
3581
|
+
return info;
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
/**
|
|
3585
|
+
* Check if a tunnel is active for a module
|
|
3586
|
+
*/
|
|
3587
|
+
hasTunnel(moduleUid) {
|
|
3588
|
+
return this.tunnelStates.has(moduleUid);
|
|
3589
|
+
}
|
|
3590
|
+
/**
|
|
3591
|
+
* Get the URL for an active tunnel
|
|
3592
|
+
*/
|
|
3593
|
+
getTunnelUrl(moduleUid) {
|
|
3594
|
+
return this.tunnelStates.get(moduleUid)?.info.url || null;
|
|
3595
|
+
}
|
|
3596
|
+
};
|
|
3597
|
+
var tunnelManagerInstance = null;
|
|
3598
|
+
function getTunnelManager() {
|
|
3599
|
+
if (!tunnelManagerInstance) {
|
|
3600
|
+
tunnelManagerInstance = new TunnelManager();
|
|
3601
|
+
}
|
|
3602
|
+
return tunnelManagerInstance;
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
// src/utils/dev-server.ts
|
|
3606
|
+
var import_child_process6 = require("child_process");
|
|
3607
|
+
|
|
3608
|
+
// src/utils/port-check.ts
|
|
3609
|
+
var net2 = __toESM(require("net"));
|
|
3610
|
+
async function isPortInUse(port) {
|
|
3611
|
+
return new Promise((resolve2) => {
|
|
3612
|
+
const server = net2.createServer();
|
|
3613
|
+
server.once("error", (err) => {
|
|
3614
|
+
if (err.code === "EADDRINUSE") {
|
|
3615
|
+
resolve2(true);
|
|
3616
|
+
} else {
|
|
3617
|
+
resolve2(false);
|
|
3618
|
+
}
|
|
3619
|
+
});
|
|
3620
|
+
server.once("listening", () => {
|
|
3621
|
+
server.close();
|
|
3622
|
+
resolve2(false);
|
|
3623
|
+
});
|
|
3624
|
+
server.listen(port);
|
|
3625
|
+
});
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
// src/utils/dev-server.ts
|
|
3629
|
+
var activeServers = /* @__PURE__ */ new Map();
|
|
3630
|
+
async function waitForPort(port, timeoutMs = 3e4) {
|
|
3631
|
+
const startTime = Date.now();
|
|
3632
|
+
const checkInterval = 500;
|
|
3633
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
3634
|
+
if (await isPortInUse(port)) {
|
|
3635
|
+
return true;
|
|
3636
|
+
}
|
|
3637
|
+
await new Promise((resolve2) => setTimeout(resolve2, checkInterval));
|
|
3638
|
+
}
|
|
3639
|
+
return false;
|
|
3640
|
+
}
|
|
3641
|
+
async function startDevServer(projectPath, port = 3e3, moduleUid = "default") {
|
|
3642
|
+
if (await isPortInUse(port)) {
|
|
3643
|
+
console.log(`[DevServer] Server already running on port ${port}`);
|
|
3644
|
+
return { success: true, alreadyRunning: true };
|
|
3645
|
+
}
|
|
3646
|
+
if (activeServers.has(moduleUid)) {
|
|
3647
|
+
const existing = activeServers.get(moduleUid);
|
|
3648
|
+
if (existing && !existing.killed) {
|
|
3649
|
+
console.log(`[DevServer] Process already exists for ${moduleUid}`);
|
|
3650
|
+
return { success: true, alreadyRunning: true };
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
console.log(`[DevServer] Starting dev server for ${moduleUid} on port ${port}...`);
|
|
3654
|
+
try {
|
|
3655
|
+
const devProcess = (0, import_child_process6.spawn)("npm", ["run", "dev"], {
|
|
3656
|
+
cwd: projectPath,
|
|
3657
|
+
env: {
|
|
3658
|
+
...process.env,
|
|
3659
|
+
PORT: String(port)
|
|
3660
|
+
},
|
|
3661
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3662
|
+
detached: false
|
|
3663
|
+
});
|
|
3664
|
+
activeServers.set(moduleUid, devProcess);
|
|
3665
|
+
devProcess.stdout?.on("data", (data) => {
|
|
3666
|
+
const line = data.toString().trim();
|
|
3667
|
+
if (line) {
|
|
3668
|
+
console.log(`[DevServer:${moduleUid}] ${line}`);
|
|
3669
|
+
}
|
|
3670
|
+
});
|
|
3671
|
+
devProcess.stderr?.on("data", (data) => {
|
|
3672
|
+
const line = data.toString().trim();
|
|
3673
|
+
if (line) {
|
|
3674
|
+
console.error(`[DevServer:${moduleUid}] ${line}`);
|
|
3675
|
+
}
|
|
3676
|
+
});
|
|
3677
|
+
devProcess.on("exit", (code, signal) => {
|
|
3678
|
+
console.log(`[DevServer] Process for ${moduleUid} exited with code ${code}, signal ${signal}`);
|
|
3679
|
+
activeServers.delete(moduleUid);
|
|
3680
|
+
});
|
|
3681
|
+
devProcess.on("error", (error) => {
|
|
3682
|
+
console.error(`[DevServer] Process error for ${moduleUid}:`, error);
|
|
3683
|
+
activeServers.delete(moduleUid);
|
|
3684
|
+
});
|
|
3685
|
+
console.log(`[DevServer] Waiting for server to start on port ${port}...`);
|
|
3686
|
+
const serverReady = await waitForPort(port, 6e4);
|
|
3687
|
+
if (!serverReady) {
|
|
3688
|
+
devProcess.kill();
|
|
3689
|
+
activeServers.delete(moduleUid);
|
|
3690
|
+
return { success: false, error: "Dev server failed to start within timeout" };
|
|
3691
|
+
}
|
|
3692
|
+
console.log(`[DevServer] Server started successfully on port ${port}`);
|
|
3693
|
+
return { success: true };
|
|
3694
|
+
} catch (error) {
|
|
3695
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3696
|
+
console.error(`[DevServer] Failed to start:`, error);
|
|
3697
|
+
return { success: false, error: errorMsg };
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
async function stopDevServer(moduleUid) {
|
|
3701
|
+
const process2 = activeServers.get(moduleUid);
|
|
3702
|
+
if (process2 && !process2.killed) {
|
|
3703
|
+
console.log(`[DevServer] Stopping server for ${moduleUid}`);
|
|
3704
|
+
process2.kill("SIGTERM");
|
|
3705
|
+
await new Promise((resolve2) => setTimeout(resolve2, 2e3));
|
|
3706
|
+
if (!process2.killed) {
|
|
3707
|
+
process2.kill("SIGKILL");
|
|
3708
|
+
}
|
|
3709
|
+
activeServers.delete(moduleUid);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
async function ensureDevServer(projectPath, port = 3e3, moduleUid = "default") {
|
|
3713
|
+
if (await isPortInUse(port)) {
|
|
3714
|
+
return { success: true };
|
|
3715
|
+
}
|
|
3716
|
+
return startDevServer(projectPath, port, moduleUid);
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
// src/utils/port-detect.ts
|
|
3720
|
+
var fs6 = __toESM(require("fs"));
|
|
3721
|
+
var path7 = __toESM(require("path"));
|
|
3722
|
+
var DEFAULT_PORT = 3e3;
|
|
3723
|
+
function detectDevPort(projectPath) {
|
|
3724
|
+
const envPort = getPortFromEnv(projectPath);
|
|
3725
|
+
if (envPort) {
|
|
3726
|
+
console.log(`[PortDetect] Found PORT=${envPort} in .env`);
|
|
3727
|
+
return envPort;
|
|
3728
|
+
}
|
|
3729
|
+
const scriptPort = getPortFromPackageJson(projectPath);
|
|
3730
|
+
if (scriptPort) {
|
|
3731
|
+
console.log(`[PortDetect] Found port ${scriptPort} in package.json dev script`);
|
|
3732
|
+
return scriptPort;
|
|
3733
|
+
}
|
|
3734
|
+
console.log(`[PortDetect] Using default port ${DEFAULT_PORT}`);
|
|
3735
|
+
return DEFAULT_PORT;
|
|
3736
|
+
}
|
|
3737
|
+
function getPortFromEnv(projectPath) {
|
|
3738
|
+
const envPaths = [
|
|
3739
|
+
path7.join(projectPath, ".env"),
|
|
3740
|
+
path7.join(projectPath, ".env.local"),
|
|
3741
|
+
path7.join(projectPath, ".env.development"),
|
|
3742
|
+
path7.join(projectPath, ".env.development.local")
|
|
3743
|
+
];
|
|
3744
|
+
for (const envPath of envPaths) {
|
|
3745
|
+
try {
|
|
3746
|
+
if (!fs6.existsSync(envPath)) continue;
|
|
3747
|
+
const content = fs6.readFileSync(envPath, "utf-8");
|
|
3748
|
+
const lines = content.split("\n");
|
|
3749
|
+
for (const line of lines) {
|
|
3750
|
+
const match = line.match(/^\s*PORT\s*=\s*["']?(\d+)["']?\s*(?:#.*)?$/);
|
|
3751
|
+
if (match) {
|
|
3752
|
+
const port = parseInt(match[1], 10);
|
|
3753
|
+
if (port > 0 && port < 65536) {
|
|
3754
|
+
return port;
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
} catch {
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
return null;
|
|
3762
|
+
}
|
|
3763
|
+
function getPortFromPackageJson(projectPath) {
|
|
3764
|
+
const packageJsonPath = path7.join(projectPath, "package.json");
|
|
3765
|
+
try {
|
|
3766
|
+
if (!fs6.existsSync(packageJsonPath)) return null;
|
|
3767
|
+
const content = fs6.readFileSync(packageJsonPath, "utf-8");
|
|
3768
|
+
const pkg = JSON.parse(content);
|
|
3769
|
+
const devScript = pkg.scripts?.dev;
|
|
3770
|
+
if (!devScript) return null;
|
|
3771
|
+
const portMatch = devScript.match(/(?:--port[=\s]|-p[=\s])(\d+)/);
|
|
3772
|
+
if (portMatch) {
|
|
3773
|
+
const port = parseInt(portMatch[1], 10);
|
|
3774
|
+
if (port > 0 && port < 65536) {
|
|
3775
|
+
return port;
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
const envMatch = devScript.match(/PORT[=\s](\d+)/);
|
|
3779
|
+
if (envMatch) {
|
|
3780
|
+
const port = parseInt(envMatch[1], 10);
|
|
3781
|
+
if (port > 0 && port < 65536) {
|
|
3782
|
+
return port;
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
} catch {
|
|
3786
|
+
}
|
|
3787
|
+
return null;
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
// src/daemon/daemon-process.ts
|
|
3791
|
+
var fs7 = __toESM(require("fs"));
|
|
3792
|
+
var os2 = __toESM(require("os"));
|
|
3793
|
+
var path8 = __toESM(require("path"));
|
|
3159
3794
|
var packageJson = require_package();
|
|
3160
3795
|
var Daemon = class {
|
|
3161
3796
|
constructor() {
|
|
@@ -3235,9 +3870,9 @@ var Daemon = class {
|
|
|
3235
3870
|
machineId: this.machineId,
|
|
3236
3871
|
deviceId: this.deviceId,
|
|
3237
3872
|
// EP726: UUID for unified device identification
|
|
3238
|
-
hostname:
|
|
3239
|
-
platform:
|
|
3240
|
-
arch:
|
|
3873
|
+
hostname: os2.hostname(),
|
|
3874
|
+
platform: os2.platform(),
|
|
3875
|
+
arch: os2.arch(),
|
|
3241
3876
|
projects
|
|
3242
3877
|
};
|
|
3243
3878
|
});
|
|
@@ -3462,6 +4097,150 @@ var Daemon = class {
|
|
|
3462
4097
|
}
|
|
3463
4098
|
}
|
|
3464
4099
|
});
|
|
4100
|
+
client.on("tunnel_command", async (message) => {
|
|
4101
|
+
if (message.type === "tunnel_command" && message.command) {
|
|
4102
|
+
const cmd = message.command;
|
|
4103
|
+
console.log(`[Daemon] Received tunnel command for ${projectId}:`, cmd.action);
|
|
4104
|
+
client.updateActivity();
|
|
4105
|
+
try {
|
|
4106
|
+
const tunnelManager = getTunnelManager();
|
|
4107
|
+
let result;
|
|
4108
|
+
if (cmd.action === "start") {
|
|
4109
|
+
const port = cmd.port || detectDevPort(projectPath);
|
|
4110
|
+
const previewUrl = `https://${cmd.moduleUid.toLowerCase()}-${cmd.projectUid.toLowerCase()}.episoda.site`;
|
|
4111
|
+
const reportTunnelStatus = async (data) => {
|
|
4112
|
+
const config2 = await (0, import_core5.loadConfig)();
|
|
4113
|
+
if (config2?.access_token) {
|
|
4114
|
+
try {
|
|
4115
|
+
const apiUrl = config2.api_url || "https://episoda.dev";
|
|
4116
|
+
const response = await fetch(`${apiUrl}/api/modules/${cmd.moduleUid}/tunnel`, {
|
|
4117
|
+
method: "POST",
|
|
4118
|
+
headers: {
|
|
4119
|
+
"Authorization": `Bearer ${config2.access_token}`,
|
|
4120
|
+
"Content-Type": "application/json"
|
|
4121
|
+
},
|
|
4122
|
+
body: JSON.stringify(data)
|
|
4123
|
+
});
|
|
4124
|
+
if (response.ok) {
|
|
4125
|
+
console.log(`[Daemon] Tunnel status reported for ${cmd.moduleUid}`);
|
|
4126
|
+
} else {
|
|
4127
|
+
console.warn(`[Daemon] Failed to report tunnel status: ${response.statusText}`);
|
|
4128
|
+
}
|
|
4129
|
+
} catch (reportError) {
|
|
4130
|
+
console.warn(`[Daemon] Error reporting tunnel status:`, reportError);
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
};
|
|
4134
|
+
(async () => {
|
|
4135
|
+
const MAX_RETRIES = 3;
|
|
4136
|
+
const RETRY_DELAY_MS = 2e3;
|
|
4137
|
+
await reportTunnelStatus({
|
|
4138
|
+
tunnel_started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4139
|
+
tunnel_error: null
|
|
4140
|
+
// Clear any previous error
|
|
4141
|
+
});
|
|
4142
|
+
try {
|
|
4143
|
+
await tunnelManager.initialize();
|
|
4144
|
+
console.log(`[Daemon] Ensuring dev server is running on port ${port}...`);
|
|
4145
|
+
const devServerResult = await ensureDevServer(projectPath, port, cmd.moduleUid);
|
|
4146
|
+
if (!devServerResult.success) {
|
|
4147
|
+
const errorMsg2 = `Dev server failed to start: ${devServerResult.error}`;
|
|
4148
|
+
console.error(`[Daemon] ${errorMsg2}`);
|
|
4149
|
+
await reportTunnelStatus({ tunnel_error: errorMsg2 });
|
|
4150
|
+
return;
|
|
4151
|
+
}
|
|
4152
|
+
console.log(`[Daemon] Dev server ready on port ${port}`);
|
|
4153
|
+
let lastError;
|
|
4154
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
4155
|
+
console.log(`[Daemon] Starting tunnel (attempt ${attempt}/${MAX_RETRIES})...`);
|
|
4156
|
+
const startResult = await tunnelManager.startTunnel({
|
|
4157
|
+
moduleUid: cmd.moduleUid,
|
|
4158
|
+
port,
|
|
4159
|
+
onUrl: async (url) => {
|
|
4160
|
+
console.log(`[Daemon] Tunnel URL for ${cmd.moduleUid}: ${url}`);
|
|
4161
|
+
await reportTunnelStatus({
|
|
4162
|
+
tunnel_url: url,
|
|
4163
|
+
tunnel_error: null
|
|
4164
|
+
// Clear error on success
|
|
4165
|
+
});
|
|
4166
|
+
},
|
|
4167
|
+
onStatusChange: (status, error) => {
|
|
4168
|
+
if (status === "error") {
|
|
4169
|
+
console.error(`[Daemon] Tunnel error for ${cmd.moduleUid}: ${error}`);
|
|
4170
|
+
reportTunnelStatus({ tunnel_error: error || "Tunnel connection error" });
|
|
4171
|
+
} else if (status === "reconnecting") {
|
|
4172
|
+
console.log(`[Daemon] Tunnel reconnecting for ${cmd.moduleUid}...`);
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
});
|
|
4176
|
+
if (startResult.success) {
|
|
4177
|
+
console.log(`[Daemon] Tunnel started successfully for ${cmd.moduleUid}`);
|
|
4178
|
+
return;
|
|
4179
|
+
}
|
|
4180
|
+
lastError = startResult.error;
|
|
4181
|
+
console.warn(`[Daemon] Tunnel start attempt ${attempt} failed: ${lastError}`);
|
|
4182
|
+
if (attempt < MAX_RETRIES) {
|
|
4183
|
+
console.log(`[Daemon] Retrying in ${RETRY_DELAY_MS}ms...`);
|
|
4184
|
+
await new Promise((resolve2) => setTimeout(resolve2, RETRY_DELAY_MS));
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
const errorMsg = `Tunnel failed after ${MAX_RETRIES} attempts: ${lastError}`;
|
|
4188
|
+
console.error(`[Daemon] ${errorMsg}`);
|
|
4189
|
+
await reportTunnelStatus({ tunnel_error: errorMsg });
|
|
4190
|
+
} catch (error) {
|
|
4191
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
4192
|
+
console.error(`[Daemon] Async tunnel startup error:`, error);
|
|
4193
|
+
await reportTunnelStatus({ tunnel_error: `Unexpected error: ${errorMsg}` });
|
|
4194
|
+
}
|
|
4195
|
+
})();
|
|
4196
|
+
result = {
|
|
4197
|
+
success: true,
|
|
4198
|
+
previewUrl
|
|
4199
|
+
// Note: actual tunnel URL will be reported via API when ready
|
|
4200
|
+
};
|
|
4201
|
+
} else if (cmd.action === "stop") {
|
|
4202
|
+
await tunnelManager.stopTunnel(cmd.moduleUid);
|
|
4203
|
+
await stopDevServer(cmd.moduleUid);
|
|
4204
|
+
const config2 = await (0, import_core5.loadConfig)();
|
|
4205
|
+
if (config2?.access_token) {
|
|
4206
|
+
try {
|
|
4207
|
+
const apiUrl = config2.api_url || "https://episoda.dev";
|
|
4208
|
+
await fetch(`${apiUrl}/api/modules/${cmd.moduleUid}/tunnel`, {
|
|
4209
|
+
method: "DELETE",
|
|
4210
|
+
headers: {
|
|
4211
|
+
"Authorization": `Bearer ${config2.access_token}`
|
|
4212
|
+
}
|
|
4213
|
+
});
|
|
4214
|
+
console.log(`[Daemon] Tunnel URL cleared for ${cmd.moduleUid}`);
|
|
4215
|
+
} catch {
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
result = { success: true };
|
|
4219
|
+
} else {
|
|
4220
|
+
result = {
|
|
4221
|
+
success: false,
|
|
4222
|
+
error: `Unknown tunnel action: ${cmd.action}`
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
await client.send({
|
|
4226
|
+
type: "tunnel_result",
|
|
4227
|
+
commandId: message.id,
|
|
4228
|
+
result
|
|
4229
|
+
});
|
|
4230
|
+
console.log(`[Daemon] Tunnel command ${cmd.action} completed for ${cmd.moduleUid}:`, result.success ? "success" : "failed");
|
|
4231
|
+
} catch (error) {
|
|
4232
|
+
await client.send({
|
|
4233
|
+
type: "tunnel_result",
|
|
4234
|
+
commandId: message.id,
|
|
4235
|
+
result: {
|
|
4236
|
+
success: false,
|
|
4237
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4238
|
+
}
|
|
4239
|
+
});
|
|
4240
|
+
console.error(`[Daemon] Tunnel command execution error:`, error);
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
});
|
|
3465
4244
|
client.on("shutdown", async (message) => {
|
|
3466
4245
|
const shutdownMessage = message;
|
|
3467
4246
|
const reason = shutdownMessage.reason || "unknown";
|
|
@@ -3514,8 +4293,8 @@ var Daemon = class {
|
|
|
3514
4293
|
let daemonPid;
|
|
3515
4294
|
try {
|
|
3516
4295
|
const pidPath = getPidFilePath();
|
|
3517
|
-
if (
|
|
3518
|
-
const pidStr =
|
|
4296
|
+
if (fs7.existsSync(pidPath)) {
|
|
4297
|
+
const pidStr = fs7.readFileSync(pidPath, "utf-8").trim();
|
|
3519
4298
|
daemonPid = parseInt(pidStr, 10);
|
|
3520
4299
|
}
|
|
3521
4300
|
} catch (pidError) {
|
|
@@ -3539,9 +4318,9 @@ var Daemon = class {
|
|
|
3539
4318
|
client.once("auth_error", errorHandler);
|
|
3540
4319
|
});
|
|
3541
4320
|
await client.connect(wsUrl, config.access_token, this.machineId, {
|
|
3542
|
-
hostname:
|
|
3543
|
-
osPlatform:
|
|
3544
|
-
osArch:
|
|
4321
|
+
hostname: os2.hostname(),
|
|
4322
|
+
osPlatform: os2.platform(),
|
|
4323
|
+
osArch: os2.arch(),
|
|
3545
4324
|
daemonPid
|
|
3546
4325
|
});
|
|
3547
4326
|
console.log(`[Daemon] Successfully connected to project ${projectId}`);
|
|
@@ -3594,29 +4373,29 @@ var Daemon = class {
|
|
|
3594
4373
|
*/
|
|
3595
4374
|
async configureGitUser(projectPath, userId, workspaceId, machineId, projectId, deviceId) {
|
|
3596
4375
|
try {
|
|
3597
|
-
const { execSync:
|
|
3598
|
-
|
|
4376
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
4377
|
+
execSync3(`git config episoda.userId ${userId}`, {
|
|
3599
4378
|
cwd: projectPath,
|
|
3600
4379
|
encoding: "utf8",
|
|
3601
4380
|
stdio: "pipe"
|
|
3602
4381
|
});
|
|
3603
|
-
|
|
4382
|
+
execSync3(`git config episoda.workspaceId ${workspaceId}`, {
|
|
3604
4383
|
cwd: projectPath,
|
|
3605
4384
|
encoding: "utf8",
|
|
3606
4385
|
stdio: "pipe"
|
|
3607
4386
|
});
|
|
3608
|
-
|
|
4387
|
+
execSync3(`git config episoda.machineId ${machineId}`, {
|
|
3609
4388
|
cwd: projectPath,
|
|
3610
4389
|
encoding: "utf8",
|
|
3611
4390
|
stdio: "pipe"
|
|
3612
4391
|
});
|
|
3613
|
-
|
|
4392
|
+
execSync3(`git config episoda.projectId ${projectId}`, {
|
|
3614
4393
|
cwd: projectPath,
|
|
3615
4394
|
encoding: "utf8",
|
|
3616
4395
|
stdio: "pipe"
|
|
3617
4396
|
});
|
|
3618
4397
|
if (deviceId) {
|
|
3619
|
-
|
|
4398
|
+
execSync3(`git config episoda.deviceId ${deviceId}`, {
|
|
3620
4399
|
cwd: projectPath,
|
|
3621
4400
|
encoding: "utf8",
|
|
3622
4401
|
stdio: "pipe"
|
|
@@ -3636,27 +4415,27 @@ var Daemon = class {
|
|
|
3636
4415
|
*/
|
|
3637
4416
|
async installGitHooks(projectPath) {
|
|
3638
4417
|
const hooks = ["post-checkout", "pre-commit"];
|
|
3639
|
-
const hooksDir =
|
|
3640
|
-
if (!
|
|
4418
|
+
const hooksDir = path8.join(projectPath, ".git", "hooks");
|
|
4419
|
+
if (!fs7.existsSync(hooksDir)) {
|
|
3641
4420
|
console.warn(`[Daemon] Hooks directory not found: ${hooksDir}`);
|
|
3642
4421
|
return;
|
|
3643
4422
|
}
|
|
3644
4423
|
for (const hookName of hooks) {
|
|
3645
4424
|
try {
|
|
3646
|
-
const hookPath =
|
|
3647
|
-
const bundledHookPath =
|
|
3648
|
-
if (!
|
|
4425
|
+
const hookPath = path8.join(hooksDir, hookName);
|
|
4426
|
+
const bundledHookPath = path8.join(__dirname, "..", "hooks", hookName);
|
|
4427
|
+
if (!fs7.existsSync(bundledHookPath)) {
|
|
3649
4428
|
console.warn(`[Daemon] Bundled hook not found: ${bundledHookPath}`);
|
|
3650
4429
|
continue;
|
|
3651
4430
|
}
|
|
3652
|
-
const hookContent =
|
|
3653
|
-
if (
|
|
3654
|
-
const existingContent =
|
|
4431
|
+
const hookContent = fs7.readFileSync(bundledHookPath, "utf-8");
|
|
4432
|
+
if (fs7.existsSync(hookPath)) {
|
|
4433
|
+
const existingContent = fs7.readFileSync(hookPath, "utf-8");
|
|
3655
4434
|
if (existingContent === hookContent) {
|
|
3656
4435
|
continue;
|
|
3657
4436
|
}
|
|
3658
4437
|
}
|
|
3659
|
-
|
|
4438
|
+
fs7.writeFileSync(hookPath, hookContent, { mode: 493 });
|
|
3660
4439
|
console.log(`[Daemon] Installed git hook: ${hookName}`);
|
|
3661
4440
|
} catch (error) {
|
|
3662
4441
|
console.warn(`[Daemon] Failed to install ${hookName} hook:`, error instanceof Error ? error.message : error);
|
|
@@ -3704,6 +4483,13 @@ var Daemon = class {
|
|
|
3704
4483
|
await connection.client.disconnect();
|
|
3705
4484
|
}
|
|
3706
4485
|
this.connections.clear();
|
|
4486
|
+
try {
|
|
4487
|
+
const tunnelManager = getTunnelManager();
|
|
4488
|
+
await tunnelManager.stopAllTunnels();
|
|
4489
|
+
console.log("[Daemon] All tunnels stopped");
|
|
4490
|
+
} catch (error) {
|
|
4491
|
+
console.error("[Daemon] Failed to stop tunnels:", error);
|
|
4492
|
+
}
|
|
3707
4493
|
await this.ipcServer.stop();
|
|
3708
4494
|
console.log("[Daemon] Shutdown complete");
|
|
3709
4495
|
}
|
|
@@ -3715,8 +4501,8 @@ var Daemon = class {
|
|
|
3715
4501
|
await this.shutdown();
|
|
3716
4502
|
try {
|
|
3717
4503
|
const pidPath = getPidFilePath();
|
|
3718
|
-
if (
|
|
3719
|
-
|
|
4504
|
+
if (fs7.existsSync(pidPath)) {
|
|
4505
|
+
fs7.unlinkSync(pidPath);
|
|
3720
4506
|
console.log("[Daemon] PID file cleaned up");
|
|
3721
4507
|
}
|
|
3722
4508
|
} catch (error) {
|