episoda 0.2.40 → 0.2.42
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 +1470 -474
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +232 -539
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,16 +6,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __esm = (fn, res) => function __init() {
|
|
10
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
-
};
|
|
12
9
|
var __commonJS = (cb, mod) => function __require() {
|
|
13
10
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
14
11
|
};
|
|
15
|
-
var __export = (target, all) => {
|
|
16
|
-
for (var name in all)
|
|
17
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
-
};
|
|
19
12
|
var __copyProps = (to, from, except, desc) => {
|
|
20
13
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
14
|
for (let key of __getOwnPropNames(from))
|
|
@@ -1559,15 +1552,15 @@ var require_git_executor = __commonJS({
|
|
|
1559
1552
|
try {
|
|
1560
1553
|
const { stdout: gitDir } = await execAsync2("git rev-parse --git-dir", { cwd, timeout: 5e3 });
|
|
1561
1554
|
const gitDirPath = gitDir.trim();
|
|
1562
|
-
const
|
|
1555
|
+
const fs18 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1563
1556
|
const rebaseMergePath = `${gitDirPath}/rebase-merge`;
|
|
1564
1557
|
const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
|
|
1565
1558
|
try {
|
|
1566
|
-
await
|
|
1559
|
+
await fs18.access(rebaseMergePath);
|
|
1567
1560
|
inRebase = true;
|
|
1568
1561
|
} catch {
|
|
1569
1562
|
try {
|
|
1570
|
-
await
|
|
1563
|
+
await fs18.access(rebaseApplyPath);
|
|
1571
1564
|
inRebase = true;
|
|
1572
1565
|
} catch {
|
|
1573
1566
|
inRebase = false;
|
|
@@ -1621,9 +1614,9 @@ var require_git_executor = __commonJS({
|
|
|
1621
1614
|
error: validation.error || "UNKNOWN_ERROR"
|
|
1622
1615
|
};
|
|
1623
1616
|
}
|
|
1624
|
-
const
|
|
1617
|
+
const fs18 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1625
1618
|
try {
|
|
1626
|
-
await
|
|
1619
|
+
await fs18.access(command.path);
|
|
1627
1620
|
return {
|
|
1628
1621
|
success: false,
|
|
1629
1622
|
error: "WORKTREE_EXISTS",
|
|
@@ -1677,9 +1670,9 @@ var require_git_executor = __commonJS({
|
|
|
1677
1670
|
*/
|
|
1678
1671
|
async executeWorktreeRemove(command, cwd, options) {
|
|
1679
1672
|
try {
|
|
1680
|
-
const
|
|
1673
|
+
const fs18 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1681
1674
|
try {
|
|
1682
|
-
await
|
|
1675
|
+
await fs18.access(command.path);
|
|
1683
1676
|
} catch {
|
|
1684
1677
|
return {
|
|
1685
1678
|
success: false,
|
|
@@ -1714,7 +1707,7 @@ var require_git_executor = __commonJS({
|
|
|
1714
1707
|
const result = await this.runGitCommand(args, cwd, options);
|
|
1715
1708
|
if (result.success) {
|
|
1716
1709
|
try {
|
|
1717
|
-
await
|
|
1710
|
+
await fs18.rm(command.path, { recursive: true, force: true });
|
|
1718
1711
|
} catch {
|
|
1719
1712
|
}
|
|
1720
1713
|
return {
|
|
@@ -1848,10 +1841,10 @@ var require_git_executor = __commonJS({
|
|
|
1848
1841
|
*/
|
|
1849
1842
|
async executeCloneBare(command, options) {
|
|
1850
1843
|
try {
|
|
1851
|
-
const
|
|
1852
|
-
const
|
|
1844
|
+
const fs18 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1845
|
+
const path19 = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1853
1846
|
try {
|
|
1854
|
-
await
|
|
1847
|
+
await fs18.access(command.path);
|
|
1855
1848
|
return {
|
|
1856
1849
|
success: false,
|
|
1857
1850
|
error: "BRANCH_ALREADY_EXISTS",
|
|
@@ -1860,9 +1853,9 @@ var require_git_executor = __commonJS({
|
|
|
1860
1853
|
};
|
|
1861
1854
|
} catch {
|
|
1862
1855
|
}
|
|
1863
|
-
const parentDir =
|
|
1856
|
+
const parentDir = path19.dirname(command.path);
|
|
1864
1857
|
try {
|
|
1865
|
-
await
|
|
1858
|
+
await fs18.mkdir(parentDir, { recursive: true });
|
|
1866
1859
|
} catch {
|
|
1867
1860
|
}
|
|
1868
1861
|
const { stdout, stderr } = await execAsync2(
|
|
@@ -1910,22 +1903,22 @@ var require_git_executor = __commonJS({
|
|
|
1910
1903
|
*/
|
|
1911
1904
|
async executeProjectInfo(cwd, options) {
|
|
1912
1905
|
try {
|
|
1913
|
-
const
|
|
1914
|
-
const
|
|
1906
|
+
const fs18 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1907
|
+
const path19 = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1915
1908
|
let currentPath = cwd;
|
|
1916
1909
|
let projectPath = cwd;
|
|
1917
1910
|
let bareRepoPath;
|
|
1918
1911
|
for (let i = 0; i < 10; i++) {
|
|
1919
|
-
const bareDir =
|
|
1920
|
-
const episodaDir =
|
|
1912
|
+
const bareDir = path19.join(currentPath, ".bare");
|
|
1913
|
+
const episodaDir = path19.join(currentPath, ".episoda");
|
|
1921
1914
|
try {
|
|
1922
|
-
await
|
|
1923
|
-
await
|
|
1915
|
+
await fs18.access(bareDir);
|
|
1916
|
+
await fs18.access(episodaDir);
|
|
1924
1917
|
projectPath = currentPath;
|
|
1925
1918
|
bareRepoPath = bareDir;
|
|
1926
1919
|
break;
|
|
1927
1920
|
} catch {
|
|
1928
|
-
const parentPath =
|
|
1921
|
+
const parentPath = path19.dirname(currentPath);
|
|
1929
1922
|
if (parentPath === currentPath) {
|
|
1930
1923
|
break;
|
|
1931
1924
|
}
|
|
@@ -2514,36 +2507,36 @@ var require_auth = __commonJS({
|
|
|
2514
2507
|
};
|
|
2515
2508
|
})();
|
|
2516
2509
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
2517
|
-
exports2.getConfigDir =
|
|
2510
|
+
exports2.getConfigDir = getConfigDir7;
|
|
2518
2511
|
exports2.getConfigPath = getConfigPath;
|
|
2519
|
-
exports2.loadConfig =
|
|
2512
|
+
exports2.loadConfig = loadConfig7;
|
|
2520
2513
|
exports2.saveConfig = saveConfig2;
|
|
2521
2514
|
exports2.validateToken = validateToken;
|
|
2522
|
-
var
|
|
2523
|
-
var
|
|
2515
|
+
var fs18 = __importStar(require("fs"));
|
|
2516
|
+
var path19 = __importStar(require("path"));
|
|
2524
2517
|
var os7 = __importStar(require("os"));
|
|
2525
2518
|
var child_process_1 = require("child_process");
|
|
2526
2519
|
var DEFAULT_CONFIG_FILE = "config.json";
|
|
2527
|
-
function
|
|
2528
|
-
return process.env.EPISODA_CONFIG_DIR ||
|
|
2520
|
+
function getConfigDir7() {
|
|
2521
|
+
return process.env.EPISODA_CONFIG_DIR || path19.join(os7.homedir(), ".episoda");
|
|
2529
2522
|
}
|
|
2530
2523
|
function getConfigPath(configPath) {
|
|
2531
2524
|
if (configPath) {
|
|
2532
2525
|
return configPath;
|
|
2533
2526
|
}
|
|
2534
|
-
return
|
|
2527
|
+
return path19.join(getConfigDir7(), DEFAULT_CONFIG_FILE);
|
|
2535
2528
|
}
|
|
2536
2529
|
function ensureConfigDir(configPath) {
|
|
2537
|
-
const dir =
|
|
2538
|
-
const isNew = !
|
|
2530
|
+
const dir = path19.dirname(configPath);
|
|
2531
|
+
const isNew = !fs18.existsSync(dir);
|
|
2539
2532
|
if (isNew) {
|
|
2540
|
-
|
|
2533
|
+
fs18.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2541
2534
|
}
|
|
2542
2535
|
if (process.platform === "darwin") {
|
|
2543
|
-
const nosyncPath =
|
|
2544
|
-
if (isNew || !
|
|
2536
|
+
const nosyncPath = path19.join(dir, ".nosync");
|
|
2537
|
+
if (isNew || !fs18.existsSync(nosyncPath)) {
|
|
2545
2538
|
try {
|
|
2546
|
-
|
|
2539
|
+
fs18.writeFileSync(nosyncPath, "", { mode: 384 });
|
|
2547
2540
|
(0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
|
|
2548
2541
|
stdio: "ignore",
|
|
2549
2542
|
timeout: 5e3
|
|
@@ -2553,13 +2546,13 @@ var require_auth = __commonJS({
|
|
|
2553
2546
|
}
|
|
2554
2547
|
}
|
|
2555
2548
|
}
|
|
2556
|
-
async function
|
|
2549
|
+
async function loadConfig7(configPath) {
|
|
2557
2550
|
const fullPath = getConfigPath(configPath);
|
|
2558
|
-
if (!
|
|
2551
|
+
if (!fs18.existsSync(fullPath)) {
|
|
2559
2552
|
return null;
|
|
2560
2553
|
}
|
|
2561
2554
|
try {
|
|
2562
|
-
const content =
|
|
2555
|
+
const content = fs18.readFileSync(fullPath, "utf8");
|
|
2563
2556
|
const config = JSON.parse(content);
|
|
2564
2557
|
return config;
|
|
2565
2558
|
} catch (error) {
|
|
@@ -2572,7 +2565,7 @@ var require_auth = __commonJS({
|
|
|
2572
2565
|
ensureConfigDir(fullPath);
|
|
2573
2566
|
try {
|
|
2574
2567
|
const content = JSON.stringify(config, null, 2);
|
|
2575
|
-
|
|
2568
|
+
fs18.writeFileSync(fullPath, content, { mode: 384 });
|
|
2576
2569
|
} catch (error) {
|
|
2577
2570
|
throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
|
|
2578
2571
|
}
|
|
@@ -2684,49 +2677,12 @@ var require_dist = __commonJS({
|
|
|
2684
2677
|
}
|
|
2685
2678
|
});
|
|
2686
2679
|
|
|
2687
|
-
// src/utils/port-check.ts
|
|
2688
|
-
var port_check_exports = {};
|
|
2689
|
-
__export(port_check_exports, {
|
|
2690
|
-
getServerPort: () => getServerPort,
|
|
2691
|
-
isPortInUse: () => isPortInUse
|
|
2692
|
-
});
|
|
2693
|
-
async function isPortInUse(port) {
|
|
2694
|
-
return new Promise((resolve3) => {
|
|
2695
|
-
const server = net2.createServer();
|
|
2696
|
-
server.once("error", (err) => {
|
|
2697
|
-
if (err.code === "EADDRINUSE") {
|
|
2698
|
-
resolve3(true);
|
|
2699
|
-
} else {
|
|
2700
|
-
resolve3(false);
|
|
2701
|
-
}
|
|
2702
|
-
});
|
|
2703
|
-
server.once("listening", () => {
|
|
2704
|
-
server.close();
|
|
2705
|
-
resolve3(false);
|
|
2706
|
-
});
|
|
2707
|
-
server.listen(port);
|
|
2708
|
-
});
|
|
2709
|
-
}
|
|
2710
|
-
function getServerPort() {
|
|
2711
|
-
if (process.env.PORT) {
|
|
2712
|
-
return parseInt(process.env.PORT, 10);
|
|
2713
|
-
}
|
|
2714
|
-
return 3e3;
|
|
2715
|
-
}
|
|
2716
|
-
var net2;
|
|
2717
|
-
var init_port_check = __esm({
|
|
2718
|
-
"src/utils/port-check.ts"() {
|
|
2719
|
-
"use strict";
|
|
2720
|
-
net2 = __toESM(require("net"));
|
|
2721
|
-
}
|
|
2722
|
-
});
|
|
2723
|
-
|
|
2724
2680
|
// package.json
|
|
2725
2681
|
var require_package = __commonJS({
|
|
2726
2682
|
"package.json"(exports2, module2) {
|
|
2727
2683
|
module2.exports = {
|
|
2728
2684
|
name: "episoda",
|
|
2729
|
-
version: "0.2.
|
|
2685
|
+
version: "0.2.42",
|
|
2730
2686
|
description: "CLI tool for Episoda local development workflow orchestration",
|
|
2731
2687
|
main: "dist/index.js",
|
|
2732
2688
|
types: "dist/index.d.ts",
|
|
@@ -3056,8 +3012,8 @@ var IPCServer = class {
|
|
|
3056
3012
|
const message = buffer.slice(0, newlineIndex);
|
|
3057
3013
|
buffer = buffer.slice(newlineIndex + 1);
|
|
3058
3014
|
try {
|
|
3059
|
-
const
|
|
3060
|
-
const response = await this.handleRequest(
|
|
3015
|
+
const request2 = JSON.parse(message);
|
|
3016
|
+
const response = await this.handleRequest(request2);
|
|
3061
3017
|
socket.write(JSON.stringify(response) + "\n");
|
|
3062
3018
|
} catch (error) {
|
|
3063
3019
|
const errorResponse = {
|
|
@@ -3075,25 +3031,25 @@ var IPCServer = class {
|
|
|
3075
3031
|
/**
|
|
3076
3032
|
* Handle IPC request
|
|
3077
3033
|
*/
|
|
3078
|
-
async handleRequest(
|
|
3079
|
-
const handler = this.handlers.get(
|
|
3034
|
+
async handleRequest(request2) {
|
|
3035
|
+
const handler = this.handlers.get(request2.command);
|
|
3080
3036
|
if (!handler) {
|
|
3081
3037
|
return {
|
|
3082
|
-
id:
|
|
3038
|
+
id: request2.id,
|
|
3083
3039
|
success: false,
|
|
3084
|
-
error: `Unknown command: ${
|
|
3040
|
+
error: `Unknown command: ${request2.command}`
|
|
3085
3041
|
};
|
|
3086
3042
|
}
|
|
3087
3043
|
try {
|
|
3088
|
-
const data = await handler(
|
|
3044
|
+
const data = await handler(request2.params);
|
|
3089
3045
|
return {
|
|
3090
|
-
id:
|
|
3046
|
+
id: request2.id,
|
|
3091
3047
|
success: true,
|
|
3092
3048
|
data
|
|
3093
3049
|
};
|
|
3094
3050
|
} catch (error) {
|
|
3095
3051
|
return {
|
|
3096
|
-
id:
|
|
3052
|
+
id: request2.id,
|
|
3097
3053
|
success: false,
|
|
3098
3054
|
error: error instanceof Error ? error.message : "Unknown error"
|
|
3099
3055
|
};
|
|
@@ -3102,7 +3058,7 @@ var IPCServer = class {
|
|
|
3102
3058
|
};
|
|
3103
3059
|
|
|
3104
3060
|
// src/daemon/daemon-process.ts
|
|
3105
|
-
var
|
|
3061
|
+
var import_core11 = __toESM(require_dist());
|
|
3106
3062
|
|
|
3107
3063
|
// src/utils/update-checker.ts
|
|
3108
3064
|
var import_child_process2 = require("child_process");
|
|
@@ -4018,7 +3974,134 @@ var import_events = require("events");
|
|
|
4018
3974
|
var fs6 = __toESM(require("fs"));
|
|
4019
3975
|
var path7 = __toESM(require("path"));
|
|
4020
3976
|
var os2 = __toESM(require("os"));
|
|
3977
|
+
|
|
3978
|
+
// src/tunnel/tunnel-api.ts
|
|
3979
|
+
var import_core6 = __toESM(require_dist());
|
|
3980
|
+
async function provisionNamedTunnel(moduleId) {
|
|
3981
|
+
const config = await (0, import_core6.loadConfig)();
|
|
3982
|
+
if (!config?.access_token) {
|
|
3983
|
+
return { success: false, error: "Not authenticated" };
|
|
3984
|
+
}
|
|
3985
|
+
try {
|
|
3986
|
+
const apiUrl = config.api_url || "https://episoda.dev";
|
|
3987
|
+
const response = await fetch(`${apiUrl}/api/tunnels`, {
|
|
3988
|
+
method: "POST",
|
|
3989
|
+
headers: {
|
|
3990
|
+
"Authorization": `Bearer ${config.access_token}`,
|
|
3991
|
+
"Content-Type": "application/json"
|
|
3992
|
+
},
|
|
3993
|
+
body: JSON.stringify({ module_id: moduleId })
|
|
3994
|
+
});
|
|
3995
|
+
const data = await response.json();
|
|
3996
|
+
if (!response.ok) {
|
|
3997
|
+
return {
|
|
3998
|
+
success: false,
|
|
3999
|
+
error: data.error?.message || data.message || `HTTP ${response.status}`
|
|
4000
|
+
};
|
|
4001
|
+
}
|
|
4002
|
+
return {
|
|
4003
|
+
success: true,
|
|
4004
|
+
tunnel: data.tunnel,
|
|
4005
|
+
message: data.message
|
|
4006
|
+
};
|
|
4007
|
+
} catch (error) {
|
|
4008
|
+
return {
|
|
4009
|
+
success: false,
|
|
4010
|
+
error: error instanceof Error ? error.message : "Failed to provision tunnel"
|
|
4011
|
+
};
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
async function provisionNamedTunnelByUid(moduleUid) {
|
|
4015
|
+
const config = await (0, import_core6.loadConfig)();
|
|
4016
|
+
if (!config?.access_token) {
|
|
4017
|
+
return { success: false, error: "Not authenticated" };
|
|
4018
|
+
}
|
|
4019
|
+
try {
|
|
4020
|
+
const apiUrl = config.api_url || "https://episoda.dev";
|
|
4021
|
+
const moduleResponse = await fetch(`${apiUrl}/api/modules/${moduleUid}`, {
|
|
4022
|
+
headers: {
|
|
4023
|
+
"Authorization": `Bearer ${config.access_token}`
|
|
4024
|
+
}
|
|
4025
|
+
});
|
|
4026
|
+
if (!moduleResponse.ok) {
|
|
4027
|
+
return {
|
|
4028
|
+
success: false,
|
|
4029
|
+
error: `Failed to find module ${moduleUid}`
|
|
4030
|
+
};
|
|
4031
|
+
}
|
|
4032
|
+
const moduleData = await moduleResponse.json();
|
|
4033
|
+
const moduleId = moduleData.moduleRecord?.id;
|
|
4034
|
+
if (!moduleId) {
|
|
4035
|
+
return {
|
|
4036
|
+
success: false,
|
|
4037
|
+
error: `Module ${moduleUid} has no ID (response keys: ${JSON.stringify(Object.keys(moduleData))})`
|
|
4038
|
+
};
|
|
4039
|
+
}
|
|
4040
|
+
return provisionNamedTunnel(moduleId);
|
|
4041
|
+
} catch (error) {
|
|
4042
|
+
return {
|
|
4043
|
+
success: false,
|
|
4044
|
+
error: error instanceof Error ? error.message : "Failed to provision tunnel"
|
|
4045
|
+
};
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
async function updateTunnelStatus(moduleUid, status, error) {
|
|
4049
|
+
if (!moduleUid || moduleUid === "LOCAL") {
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
const config = await (0, import_core6.loadConfig)();
|
|
4053
|
+
if (!config?.access_token) {
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
try {
|
|
4057
|
+
const apiUrl = config.api_url || "https://episoda.dev";
|
|
4058
|
+
await fetch(`${apiUrl}/api/modules/${moduleUid}/tunnel`, {
|
|
4059
|
+
method: "PATCH",
|
|
4060
|
+
headers: {
|
|
4061
|
+
"Authorization": `Bearer ${config.access_token}`,
|
|
4062
|
+
"Content-Type": "application/json"
|
|
4063
|
+
},
|
|
4064
|
+
body: JSON.stringify({
|
|
4065
|
+
tunnel_health_status: status,
|
|
4066
|
+
tunnel_last_health_check: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4067
|
+
...error && { tunnel_error: error }
|
|
4068
|
+
})
|
|
4069
|
+
});
|
|
4070
|
+
} catch {
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
async function clearTunnelUrl(moduleUid) {
|
|
4074
|
+
if (!moduleUid || moduleUid === "LOCAL") {
|
|
4075
|
+
return;
|
|
4076
|
+
}
|
|
4077
|
+
const config = await (0, import_core6.loadConfig)();
|
|
4078
|
+
if (!config?.access_token) {
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
4081
|
+
try {
|
|
4082
|
+
const apiUrl = config.api_url || "https://episoda.dev";
|
|
4083
|
+
await fetch(`${apiUrl}/api/modules/${moduleUid}/tunnel`, {
|
|
4084
|
+
method: "DELETE",
|
|
4085
|
+
headers: {
|
|
4086
|
+
"Authorization": `Bearer ${config.access_token}`
|
|
4087
|
+
}
|
|
4088
|
+
});
|
|
4089
|
+
} catch {
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
// src/tunnel/tunnel-manager.ts
|
|
4021
4094
|
var TUNNEL_PID_DIR = path7.join(os2.homedir(), ".episoda", "tunnels");
|
|
4095
|
+
var TUNNEL_TIMEOUTS = {
|
|
4096
|
+
/** Time to wait for Named Tunnel connection (includes API token fetch + connect) */
|
|
4097
|
+
NAMED_TUNNEL_CONNECT: 6e4,
|
|
4098
|
+
/** Time to wait for Quick Tunnel connection (simpler, faster connection) */
|
|
4099
|
+
QUICK_TUNNEL_CONNECT: 3e4,
|
|
4100
|
+
/** Time to wait for cloudflared process to start before giving up */
|
|
4101
|
+
PROCESS_START: 1e4,
|
|
4102
|
+
/** Grace period after starting cloudflared before checking status */
|
|
4103
|
+
STARTUP_GRACE: 2e3
|
|
4104
|
+
};
|
|
4022
4105
|
var TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
4023
4106
|
var DEFAULT_RECONNECT_CONFIG = {
|
|
4024
4107
|
maxRetries: 5,
|
|
@@ -4075,6 +4158,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4075
4158
|
}
|
|
4076
4159
|
/**
|
|
4077
4160
|
* EP877: Read PID from file
|
|
4161
|
+
* EP948: Enhanced with validation and stale file cleanup
|
|
4078
4162
|
*/
|
|
4079
4163
|
readPidFile(moduleUid) {
|
|
4080
4164
|
try {
|
|
@@ -4082,9 +4166,25 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4082
4166
|
if (!fs6.existsSync(pidPath)) {
|
|
4083
4167
|
return null;
|
|
4084
4168
|
}
|
|
4085
|
-
const
|
|
4086
|
-
|
|
4169
|
+
const content = fs6.readFileSync(pidPath, "utf8").trim();
|
|
4170
|
+
const pid = parseInt(content, 10);
|
|
4171
|
+
if (isNaN(pid) || pid <= 0) {
|
|
4172
|
+
console.warn(`[Tunnel] EP948: Invalid PID file content for ${moduleUid}: "${content}", removing stale file`);
|
|
4173
|
+
this.removePidFile(moduleUid);
|
|
4174
|
+
return null;
|
|
4175
|
+
}
|
|
4176
|
+
if (!this.isProcessRunning(pid)) {
|
|
4177
|
+
console.log(`[Tunnel] EP948: PID ${pid} for ${moduleUid} is not running, removing stale file`);
|
|
4178
|
+
this.removePidFile(moduleUid);
|
|
4179
|
+
return null;
|
|
4180
|
+
}
|
|
4181
|
+
return pid;
|
|
4087
4182
|
} catch (error) {
|
|
4183
|
+
console.warn(`[Tunnel] EP948: Failed to read PID file for ${moduleUid}: ${error.message}`);
|
|
4184
|
+
try {
|
|
4185
|
+
this.removePidFile(moduleUid);
|
|
4186
|
+
} catch {
|
|
4187
|
+
}
|
|
4088
4188
|
return null;
|
|
4089
4189
|
}
|
|
4090
4190
|
}
|
|
@@ -4288,10 +4388,190 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4288
4388
|
}, delay);
|
|
4289
4389
|
}
|
|
4290
4390
|
/**
|
|
4291
|
-
*
|
|
4292
|
-
*
|
|
4391
|
+
* EP948: Start a Named Tunnel using a pre-provisioned token
|
|
4392
|
+
* Named Tunnels connect to a persistent tunnel created via Cloudflare API
|
|
4393
|
+
*/
|
|
4394
|
+
async startNamedTunnelProcess(options, existingState) {
|
|
4395
|
+
const { moduleUid, port = 3e3, onUrl, onStatusChange, tunnelToken, previewUrl } = options;
|
|
4396
|
+
if (!tunnelToken) {
|
|
4397
|
+
return { success: false, error: "Named tunnel requires a token" };
|
|
4398
|
+
}
|
|
4399
|
+
if (!this.cloudflaredPath) {
|
|
4400
|
+
try {
|
|
4401
|
+
this.cloudflaredPath = await ensureCloudflared();
|
|
4402
|
+
} catch (error) {
|
|
4403
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4404
|
+
return { success: false, error: `Failed to get cloudflared: ${errorMessage}` };
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
return new Promise((resolve3) => {
|
|
4408
|
+
const tunnelInfo = {
|
|
4409
|
+
moduleUid,
|
|
4410
|
+
url: previewUrl || "",
|
|
4411
|
+
// Named tunnels have a known URL
|
|
4412
|
+
port,
|
|
4413
|
+
status: "starting",
|
|
4414
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
4415
|
+
process: null
|
|
4416
|
+
};
|
|
4417
|
+
console.log(`[Tunnel] EP948: Starting Named Tunnel for ${moduleUid} with preview URL ${previewUrl}`);
|
|
4418
|
+
const process2 = (0, import_child_process6.spawn)(this.cloudflaredPath, [
|
|
4419
|
+
"tunnel",
|
|
4420
|
+
"run",
|
|
4421
|
+
"--token",
|
|
4422
|
+
tunnelToken
|
|
4423
|
+
], {
|
|
4424
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4425
|
+
});
|
|
4426
|
+
tunnelInfo.process = process2;
|
|
4427
|
+
tunnelInfo.pid = process2.pid;
|
|
4428
|
+
if (process2.pid) {
|
|
4429
|
+
this.writePidFile(moduleUid, process2.pid);
|
|
4430
|
+
}
|
|
4431
|
+
const state = existingState || {
|
|
4432
|
+
info: tunnelInfo,
|
|
4433
|
+
options,
|
|
4434
|
+
intentionallyStopped: false,
|
|
4435
|
+
retryCount: 0,
|
|
4436
|
+
retryTimeoutId: null
|
|
4437
|
+
};
|
|
4438
|
+
state.info = tunnelInfo;
|
|
4439
|
+
this.tunnelStates.set(moduleUid, state);
|
|
4440
|
+
let connected = false;
|
|
4441
|
+
let stderrBuffer = "";
|
|
4442
|
+
const connectionPatterns = [
|
|
4443
|
+
/Connection.*registered/i,
|
|
4444
|
+
// "Connection [id] registered"
|
|
4445
|
+
/Registered tunnel connection/i,
|
|
4446
|
+
// Alternative format
|
|
4447
|
+
/connected to.*connector/i,
|
|
4448
|
+
// "connected to [colo] connector ID"
|
|
4449
|
+
/Tunnel is ready/i,
|
|
4450
|
+
// Some versions use this
|
|
4451
|
+
/ingress.*rules.*applied/i,
|
|
4452
|
+
// Indicates ingress rules are active
|
|
4453
|
+
/Initial.*connection.*established/i
|
|
4454
|
+
// Initial connection message
|
|
4455
|
+
];
|
|
4456
|
+
const checkConnection = (data) => {
|
|
4457
|
+
if (connected) return;
|
|
4458
|
+
const isConnected = connectionPatterns.some((pattern) => pattern.test(data));
|
|
4459
|
+
if (isConnected) {
|
|
4460
|
+
connected = true;
|
|
4461
|
+
tunnelInfo.status = "connected";
|
|
4462
|
+
tunnelInfo.url = previewUrl || "";
|
|
4463
|
+
console.log(`[Tunnel] EP948: Named Tunnel connected for ${moduleUid}: ${previewUrl}`);
|
|
4464
|
+
updateTunnelStatus(moduleUid, "healthy").catch(() => {
|
|
4465
|
+
});
|
|
4466
|
+
onStatusChange?.("connected");
|
|
4467
|
+
onUrl?.(tunnelInfo.url);
|
|
4468
|
+
this.emitEvent({
|
|
4469
|
+
type: "started",
|
|
4470
|
+
moduleUid,
|
|
4471
|
+
url: tunnelInfo.url
|
|
4472
|
+
});
|
|
4473
|
+
resolve3({ success: true, url: tunnelInfo.url });
|
|
4474
|
+
}
|
|
4475
|
+
};
|
|
4476
|
+
process2.stderr?.on("data", (data) => {
|
|
4477
|
+
stderrBuffer += data.toString();
|
|
4478
|
+
checkConnection(stderrBuffer);
|
|
4479
|
+
});
|
|
4480
|
+
process2.stdout?.on("data", (data) => {
|
|
4481
|
+
checkConnection(data.toString());
|
|
4482
|
+
});
|
|
4483
|
+
process2.on("exit", (code, signal) => {
|
|
4484
|
+
const wasConnected = tunnelInfo.status === "connected";
|
|
4485
|
+
tunnelInfo.status = "disconnected";
|
|
4486
|
+
const currentState = this.tunnelStates.get(moduleUid);
|
|
4487
|
+
if (!connected) {
|
|
4488
|
+
const errorMsg = `Named tunnel process exited with code ${code}`;
|
|
4489
|
+
tunnelInfo.status = "error";
|
|
4490
|
+
tunnelInfo.error = errorMsg;
|
|
4491
|
+
updateTunnelStatus(moduleUid, "error", errorMsg).catch(() => {
|
|
4492
|
+
});
|
|
4493
|
+
if (currentState && !currentState.intentionallyStopped) {
|
|
4494
|
+
this.attemptReconnect(moduleUid);
|
|
4495
|
+
} else {
|
|
4496
|
+
this.tunnelStates.delete(moduleUid);
|
|
4497
|
+
onStatusChange?.("error", errorMsg);
|
|
4498
|
+
this.emitEvent({ type: "error", moduleUid, error: errorMsg });
|
|
4499
|
+
}
|
|
4500
|
+
resolve3({ success: false, error: errorMsg });
|
|
4501
|
+
} else if (wasConnected) {
|
|
4502
|
+
if (currentState && !currentState.intentionallyStopped) {
|
|
4503
|
+
console.log(`[Tunnel] EP948: Named tunnel ${moduleUid} crashed unexpectedly, attempting reconnect...`);
|
|
4504
|
+
updateTunnelStatus(moduleUid, "disconnected").catch(() => {
|
|
4505
|
+
});
|
|
4506
|
+
onStatusChange?.("reconnecting");
|
|
4507
|
+
this.attemptReconnect(moduleUid);
|
|
4508
|
+
} else {
|
|
4509
|
+
updateTunnelStatus(moduleUid, "disconnected").catch(() => {
|
|
4510
|
+
});
|
|
4511
|
+
this.tunnelStates.delete(moduleUid);
|
|
4512
|
+
onStatusChange?.("disconnected");
|
|
4513
|
+
this.emitEvent({ type: "stopped", moduleUid });
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
});
|
|
4517
|
+
process2.on("error", (error) => {
|
|
4518
|
+
tunnelInfo.status = "error";
|
|
4519
|
+
tunnelInfo.error = error.message;
|
|
4520
|
+
updateTunnelStatus(moduleUid, "error", error.message).catch(() => {
|
|
4521
|
+
});
|
|
4522
|
+
const currentState = this.tunnelStates.get(moduleUid);
|
|
4523
|
+
if (currentState && !currentState.intentionallyStopped) {
|
|
4524
|
+
this.attemptReconnect(moduleUid);
|
|
4525
|
+
} else {
|
|
4526
|
+
this.tunnelStates.delete(moduleUid);
|
|
4527
|
+
onStatusChange?.("error", error.message);
|
|
4528
|
+
this.emitEvent({ type: "error", moduleUid, error: error.message });
|
|
4529
|
+
}
|
|
4530
|
+
if (!connected) {
|
|
4531
|
+
resolve3({ success: false, error: error.message });
|
|
4532
|
+
}
|
|
4533
|
+
});
|
|
4534
|
+
setTimeout(() => {
|
|
4535
|
+
if (!connected) {
|
|
4536
|
+
process2.kill();
|
|
4537
|
+
if (stderrBuffer) {
|
|
4538
|
+
console.error(`[Tunnel] EP948: Named tunnel ${moduleUid} stderr before timeout:`);
|
|
4539
|
+
console.error(stderrBuffer.slice(-2e3));
|
|
4540
|
+
}
|
|
4541
|
+
const errorMsg = "Named tunnel connection timed out after 60 seconds. Check logs for cloudflared output.";
|
|
4542
|
+
tunnelInfo.status = "error";
|
|
4543
|
+
tunnelInfo.error = errorMsg;
|
|
4544
|
+
updateTunnelStatus(moduleUid, "error", errorMsg).catch(() => {
|
|
4545
|
+
});
|
|
4546
|
+
const currentState = this.tunnelStates.get(moduleUid);
|
|
4547
|
+
if (currentState && !currentState.intentionallyStopped) {
|
|
4548
|
+
this.attemptReconnect(moduleUid);
|
|
4549
|
+
} else {
|
|
4550
|
+
this.tunnelStates.delete(moduleUid);
|
|
4551
|
+
onStatusChange?.("error", errorMsg);
|
|
4552
|
+
this.emitEvent({ type: "error", moduleUid, error: errorMsg });
|
|
4553
|
+
}
|
|
4554
|
+
resolve3({ success: false, error: errorMsg });
|
|
4555
|
+
}
|
|
4556
|
+
}, TUNNEL_TIMEOUTS.NAMED_TUNNEL_CONNECT);
|
|
4557
|
+
});
|
|
4558
|
+
}
|
|
4559
|
+
/**
|
|
4560
|
+
* EP948: Route to the appropriate tunnel process method based on mode
|
|
4293
4561
|
*/
|
|
4294
4562
|
async startTunnelProcess(options, existingState) {
|
|
4563
|
+
const mode = options.mode || "named";
|
|
4564
|
+
if (mode === "named" && options.tunnelToken) {
|
|
4565
|
+
return this.startNamedTunnelProcess(options, existingState);
|
|
4566
|
+
}
|
|
4567
|
+
console.log(`[Tunnel] EP948: Using Quick Tunnel mode for ${options.moduleUid}`);
|
|
4568
|
+
return this.startQuickTunnelProcess(options, existingState);
|
|
4569
|
+
}
|
|
4570
|
+
/**
|
|
4571
|
+
* EP672-9: Internal method to start the tunnel process (Quick Tunnel mode)
|
|
4572
|
+
* Separated from startTunnel to support reconnection
|
|
4573
|
+
*/
|
|
4574
|
+
async startQuickTunnelProcess(options, existingState) {
|
|
4295
4575
|
const { moduleUid, port = 3e3, onUrl, onStatusChange } = options;
|
|
4296
4576
|
if (!this.cloudflaredPath) {
|
|
4297
4577
|
try {
|
|
@@ -4419,7 +4699,7 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4419
4699
|
}
|
|
4420
4700
|
resolve3({ success: false, error: errorMsg });
|
|
4421
4701
|
}
|
|
4422
|
-
},
|
|
4702
|
+
}, TUNNEL_TIMEOUTS.QUICK_TUNNEL_CONNECT);
|
|
4423
4703
|
});
|
|
4424
4704
|
}
|
|
4425
4705
|
/**
|
|
@@ -4446,9 +4726,11 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4446
4726
|
* EP877: Internal start implementation with lock already held
|
|
4447
4727
|
* EP901: Enhanced to clean up ALL orphaned cloudflared processes before starting
|
|
4448
4728
|
* EP904: Added port-based deduplication to prevent multiple tunnels on same port
|
|
4729
|
+
* EP948: Added Named Tunnel provisioning via platform API
|
|
4449
4730
|
*/
|
|
4450
4731
|
async startTunnelWithLock(options) {
|
|
4451
4732
|
const { moduleUid, port = 3e3 } = options;
|
|
4733
|
+
let resolvedOptions = { ...options };
|
|
4452
4734
|
const existingState = this.tunnelStates.get(moduleUid);
|
|
4453
4735
|
if (existingState) {
|
|
4454
4736
|
if (existingState.info.status === "connected") {
|
|
@@ -4475,7 +4757,23 @@ var TunnelManager = class extends import_events.EventEmitter {
|
|
|
4475
4757
|
if (cleanup.cleaned > 0) {
|
|
4476
4758
|
console.log(`[Tunnel] EP901: Pre-start cleanup removed ${cleanup.cleaned} orphaned processes`);
|
|
4477
4759
|
}
|
|
4478
|
-
|
|
4760
|
+
const mode = resolvedOptions.mode || "named";
|
|
4761
|
+
if (mode === "named" && !resolvedOptions.tunnelToken && moduleUid !== "LOCAL") {
|
|
4762
|
+
console.log(`[Tunnel] EP948: Provisioning Named Tunnel for ${moduleUid}...`);
|
|
4763
|
+
const provisionResult = await provisionNamedTunnelByUid(moduleUid);
|
|
4764
|
+
if (provisionResult.success && provisionResult.tunnel) {
|
|
4765
|
+
console.log(`[Tunnel] EP948: Named Tunnel provisioned: ${provisionResult.tunnel.preview_url}`);
|
|
4766
|
+
resolvedOptions = {
|
|
4767
|
+
...resolvedOptions,
|
|
4768
|
+
tunnelToken: provisionResult.tunnel.token,
|
|
4769
|
+
previewUrl: provisionResult.tunnel.preview_url
|
|
4770
|
+
};
|
|
4771
|
+
} else {
|
|
4772
|
+
console.warn(`[Tunnel] EP948: Named Tunnel provisioning failed: ${provisionResult.error}. Falling back to Quick Tunnel.`);
|
|
4773
|
+
resolvedOptions = { ...resolvedOptions, mode: "quick" };
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
return this.startTunnelProcess(resolvedOptions);
|
|
4479
4777
|
}
|
|
4480
4778
|
/**
|
|
4481
4779
|
* Stop a tunnel for a module
|
|
@@ -4574,28 +4872,6 @@ function getTunnelManager() {
|
|
|
4574
4872
|
return tunnelManagerInstance;
|
|
4575
4873
|
}
|
|
4576
4874
|
|
|
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
4875
|
// src/agent/claude-binary.ts
|
|
4600
4876
|
var import_child_process7 = require("child_process");
|
|
4601
4877
|
var path8 = __toESM(require("path"));
|
|
@@ -5055,11 +5331,57 @@ var AgentManager = class {
|
|
|
5055
5331
|
}
|
|
5056
5332
|
};
|
|
5057
5333
|
|
|
5058
|
-
// src/
|
|
5334
|
+
// src/preview/types.ts
|
|
5335
|
+
var DEV_SERVER_CONSTANTS = {
|
|
5336
|
+
/** Maximum restart attempts before giving up */
|
|
5337
|
+
MAX_RESTART_ATTEMPTS: 5,
|
|
5338
|
+
/** Initial delay before first restart (ms) */
|
|
5339
|
+
INITIAL_RESTART_DELAY_MS: 2e3,
|
|
5340
|
+
/** Maximum delay between restarts (ms) */
|
|
5341
|
+
MAX_RESTART_DELAY_MS: 3e4,
|
|
5342
|
+
/** Maximum log file size before rotation (bytes) */
|
|
5343
|
+
MAX_LOG_SIZE_BYTES: 5 * 1024 * 1024,
|
|
5344
|
+
// 5MB
|
|
5345
|
+
/** Node.js memory limit (MB) */
|
|
5346
|
+
NODE_MEMORY_LIMIT_MB: 2048,
|
|
5347
|
+
/** Timeout waiting for server to start (ms) */
|
|
5348
|
+
STARTUP_TIMEOUT_MS: 6e4,
|
|
5349
|
+
/** Timeout for health check requests (ms) */
|
|
5350
|
+
HEALTH_CHECK_TIMEOUT_MS: 5e3
|
|
5351
|
+
};
|
|
5352
|
+
|
|
5353
|
+
// src/preview/preview-manager.ts
|
|
5354
|
+
var import_events3 = require("events");
|
|
5355
|
+
var import_fs = require("fs");
|
|
5356
|
+
|
|
5357
|
+
// src/preview/dev-server-runner.ts
|
|
5059
5358
|
var import_child_process9 = require("child_process");
|
|
5060
|
-
|
|
5359
|
+
var http = __toESM(require("http"));
|
|
5360
|
+
var fs11 = __toESM(require("fs"));
|
|
5361
|
+
var path12 = __toESM(require("path"));
|
|
5362
|
+
var import_events2 = require("events");
|
|
5061
5363
|
var import_core7 = __toESM(require_dist());
|
|
5062
5364
|
|
|
5365
|
+
// src/utils/port-check.ts
|
|
5366
|
+
var net2 = __toESM(require("net"));
|
|
5367
|
+
async function isPortInUse(port) {
|
|
5368
|
+
return new Promise((resolve3) => {
|
|
5369
|
+
const server = net2.createServer();
|
|
5370
|
+
server.once("error", (err) => {
|
|
5371
|
+
if (err.code === "EADDRINUSE") {
|
|
5372
|
+
resolve3(true);
|
|
5373
|
+
} else {
|
|
5374
|
+
resolve3(false);
|
|
5375
|
+
}
|
|
5376
|
+
});
|
|
5377
|
+
server.once("listening", () => {
|
|
5378
|
+
server.close();
|
|
5379
|
+
resolve3(false);
|
|
5380
|
+
});
|
|
5381
|
+
server.listen(port);
|
|
5382
|
+
});
|
|
5383
|
+
}
|
|
5384
|
+
|
|
5063
5385
|
// src/utils/env-cache.ts
|
|
5064
5386
|
var fs10 = __toESM(require("fs"));
|
|
5065
5387
|
var path11 = __toESM(require("path"));
|
|
@@ -5211,10 +5533,809 @@ No cached values available as fallback.`
|
|
|
5211
5533
|
}
|
|
5212
5534
|
}
|
|
5213
5535
|
|
|
5536
|
+
// src/preview/dev-server-runner.ts
|
|
5537
|
+
var DevServerRunner = class extends import_events2.EventEmitter {
|
|
5538
|
+
constructor() {
|
|
5539
|
+
super();
|
|
5540
|
+
this.servers = /* @__PURE__ */ new Map();
|
|
5541
|
+
}
|
|
5542
|
+
/**
|
|
5543
|
+
* Start a dev server for a module
|
|
5544
|
+
*/
|
|
5545
|
+
async start(config) {
|
|
5546
|
+
const {
|
|
5547
|
+
projectPath,
|
|
5548
|
+
port,
|
|
5549
|
+
moduleUid,
|
|
5550
|
+
customCommand,
|
|
5551
|
+
autoRestart = true
|
|
5552
|
+
} = config;
|
|
5553
|
+
if (await isPortInUse(port)) {
|
|
5554
|
+
console.log(`[DevServerRunner] Server already running on port ${port}`);
|
|
5555
|
+
return { success: true, alreadyRunning: true };
|
|
5556
|
+
}
|
|
5557
|
+
const existing = this.servers.get(moduleUid);
|
|
5558
|
+
if (existing && !existing.process.killed) {
|
|
5559
|
+
console.log(`[DevServerRunner] Process already exists for ${moduleUid}`);
|
|
5560
|
+
return { success: true, alreadyRunning: true };
|
|
5561
|
+
}
|
|
5562
|
+
console.log(`[DevServerRunner] Starting dev server for ${moduleUid} on port ${port}...`);
|
|
5563
|
+
const injectedEnvVars = await this.fetchEnvVars(projectPath);
|
|
5564
|
+
try {
|
|
5565
|
+
const logPath = this.getLogFilePath(moduleUid);
|
|
5566
|
+
const process2 = this.spawnProcess(projectPath, port, moduleUid, logPath, customCommand, injectedEnvVars);
|
|
5567
|
+
const state = {
|
|
5568
|
+
process: process2,
|
|
5569
|
+
moduleUid,
|
|
5570
|
+
projectPath,
|
|
5571
|
+
port,
|
|
5572
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
5573
|
+
restartCount: 0,
|
|
5574
|
+
lastRestartAt: null,
|
|
5575
|
+
autoRestartEnabled: autoRestart,
|
|
5576
|
+
logFile: logPath,
|
|
5577
|
+
customCommand,
|
|
5578
|
+
injectedEnvVars
|
|
5579
|
+
};
|
|
5580
|
+
this.servers.set(moduleUid, state);
|
|
5581
|
+
this.writeToLog(logPath, `Starting dev server on port ${port}`);
|
|
5582
|
+
this.setupProcessHandlers(moduleUid, process2, logPath);
|
|
5583
|
+
console.log(`[DevServerRunner] Waiting for server on port ${port}...`);
|
|
5584
|
+
const ready = await this.waitForPort(port, DEV_SERVER_CONSTANTS.STARTUP_TIMEOUT_MS);
|
|
5585
|
+
if (!ready) {
|
|
5586
|
+
process2.kill();
|
|
5587
|
+
this.servers.delete(moduleUid);
|
|
5588
|
+
this.writeToLog(logPath, "Failed to start within timeout", true);
|
|
5589
|
+
return { success: false, error: "Dev server failed to start within timeout" };
|
|
5590
|
+
}
|
|
5591
|
+
console.log(`[DevServerRunner] Server started successfully on port ${port}`);
|
|
5592
|
+
this.writeToLog(logPath, "Server started successfully");
|
|
5593
|
+
this.emit("started", moduleUid, port);
|
|
5594
|
+
return { success: true };
|
|
5595
|
+
} catch (error) {
|
|
5596
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
5597
|
+
console.error(`[DevServerRunner] Failed to start:`, error);
|
|
5598
|
+
return { success: false, error: errorMsg };
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
/**
|
|
5602
|
+
* Stop a dev server
|
|
5603
|
+
*/
|
|
5604
|
+
async stop(moduleUid) {
|
|
5605
|
+
const state = this.servers.get(moduleUid);
|
|
5606
|
+
if (!state) {
|
|
5607
|
+
return;
|
|
5608
|
+
}
|
|
5609
|
+
state.autoRestartEnabled = false;
|
|
5610
|
+
if (!state.process.killed) {
|
|
5611
|
+
console.log(`[DevServerRunner] Stopping server for ${moduleUid}`);
|
|
5612
|
+
this.writeToLog(state.logFile, "Stopping server (manual stop)");
|
|
5613
|
+
state.process.kill("SIGTERM");
|
|
5614
|
+
await this.wait(2e3);
|
|
5615
|
+
if (!state.process.killed) {
|
|
5616
|
+
state.process.kill("SIGKILL");
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5619
|
+
this.servers.delete(moduleUid);
|
|
5620
|
+
this.emit("stopped", moduleUid);
|
|
5621
|
+
}
|
|
5622
|
+
/**
|
|
5623
|
+
* Restart a dev server
|
|
5624
|
+
*/
|
|
5625
|
+
async restart(moduleUid) {
|
|
5626
|
+
const state = this.servers.get(moduleUid);
|
|
5627
|
+
if (!state) {
|
|
5628
|
+
return { success: false, error: `No dev server found for ${moduleUid}` };
|
|
5629
|
+
}
|
|
5630
|
+
const { projectPath, port, autoRestartEnabled, customCommand, logFile } = state;
|
|
5631
|
+
console.log(`[DevServerRunner] Restarting server for ${moduleUid}...`);
|
|
5632
|
+
this.writeToLog(logFile, "Manual restart requested");
|
|
5633
|
+
await this.stop(moduleUid);
|
|
5634
|
+
await this.wait(1e3);
|
|
5635
|
+
if (await isPortInUse(port)) {
|
|
5636
|
+
await this.killProcessOnPort(port);
|
|
5637
|
+
}
|
|
5638
|
+
return this.start({
|
|
5639
|
+
projectPath,
|
|
5640
|
+
port,
|
|
5641
|
+
moduleUid,
|
|
5642
|
+
customCommand,
|
|
5643
|
+
autoRestart: autoRestartEnabled
|
|
5644
|
+
});
|
|
5645
|
+
}
|
|
5646
|
+
/**
|
|
5647
|
+
* Check if a dev server is healthy (responding to HTTP requests)
|
|
5648
|
+
*/
|
|
5649
|
+
async isHealthy(moduleUid) {
|
|
5650
|
+
const state = this.servers.get(moduleUid);
|
|
5651
|
+
if (!state) {
|
|
5652
|
+
return false;
|
|
5653
|
+
}
|
|
5654
|
+
return this.checkHealth(state.port);
|
|
5655
|
+
}
|
|
5656
|
+
/**
|
|
5657
|
+
* Check if a dev server is running
|
|
5658
|
+
*/
|
|
5659
|
+
isRunning(moduleUid) {
|
|
5660
|
+
const state = this.servers.get(moduleUid);
|
|
5661
|
+
return !!state && !state.process.killed;
|
|
5662
|
+
}
|
|
5663
|
+
/**
|
|
5664
|
+
* Get status of a specific dev server
|
|
5665
|
+
*/
|
|
5666
|
+
getStatus(moduleUid) {
|
|
5667
|
+
const state = this.servers.get(moduleUid);
|
|
5668
|
+
if (!state) {
|
|
5669
|
+
return void 0;
|
|
5670
|
+
}
|
|
5671
|
+
return this.stateToStatus(state);
|
|
5672
|
+
}
|
|
5673
|
+
/**
|
|
5674
|
+
* Get status of all dev servers
|
|
5675
|
+
*/
|
|
5676
|
+
getAllStatus() {
|
|
5677
|
+
return Array.from(this.servers.values()).map((s) => this.stateToStatus(s));
|
|
5678
|
+
}
|
|
5679
|
+
/**
|
|
5680
|
+
* Stop all dev servers
|
|
5681
|
+
*/
|
|
5682
|
+
async stopAll() {
|
|
5683
|
+
const uids = Array.from(this.servers.keys());
|
|
5684
|
+
await Promise.all(uids.map((uid) => this.stop(uid)));
|
|
5685
|
+
}
|
|
5686
|
+
/**
|
|
5687
|
+
* Ensure a dev server is running, starting if needed
|
|
5688
|
+
*
|
|
5689
|
+
* Note: start() already handles the case where the port is in use,
|
|
5690
|
+
* returning { success: true, alreadyRunning: true }.
|
|
5691
|
+
*/
|
|
5692
|
+
async ensure(config) {
|
|
5693
|
+
return this.start({ ...config, autoRestart: true });
|
|
5694
|
+
}
|
|
5695
|
+
/**
|
|
5696
|
+
* Kill any process on a specific port
|
|
5697
|
+
*/
|
|
5698
|
+
async killProcessOnPort(port) {
|
|
5699
|
+
try {
|
|
5700
|
+
const result = (0, import_child_process9.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
|
|
5701
|
+
if (!result) {
|
|
5702
|
+
return true;
|
|
5703
|
+
}
|
|
5704
|
+
const pids = result.split("\n").filter(Boolean);
|
|
5705
|
+
console.log(`[DevServerRunner] Found ${pids.length} process(es) on port ${port}`);
|
|
5706
|
+
for (const pid of pids) {
|
|
5707
|
+
try {
|
|
5708
|
+
(0, import_child_process9.execSync)(`kill -15 ${pid} 2>/dev/null || true`);
|
|
5709
|
+
} catch {
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
await this.wait(1e3);
|
|
5713
|
+
for (const pid of pids) {
|
|
5714
|
+
try {
|
|
5715
|
+
(0, import_child_process9.execSync)(`kill -0 ${pid} 2>/dev/null`);
|
|
5716
|
+
(0, import_child_process9.execSync)(`kill -9 ${pid} 2>/dev/null || true`);
|
|
5717
|
+
} catch {
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
await this.wait(500);
|
|
5721
|
+
return !await isPortInUse(port);
|
|
5722
|
+
} catch (error) {
|
|
5723
|
+
console.error(`[DevServerRunner] Error killing process on port ${port}:`, error);
|
|
5724
|
+
return false;
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
// ============ Private Methods ============
|
|
5728
|
+
async fetchEnvVars(projectPath) {
|
|
5729
|
+
try {
|
|
5730
|
+
const config = await (0, import_core7.loadConfig)();
|
|
5731
|
+
if (!config?.access_token || !config?.project_id) {
|
|
5732
|
+
return {};
|
|
5733
|
+
}
|
|
5734
|
+
const apiUrl = config.api_url || "https://episoda.dev";
|
|
5735
|
+
const result = await fetchEnvVarsWithCache(apiUrl, config.access_token, {
|
|
5736
|
+
projectId: config.project_id,
|
|
5737
|
+
cacheTtl: 300
|
|
5738
|
+
});
|
|
5739
|
+
console.log(`[DevServerRunner] Loaded ${Object.keys(result.envVars).length} env vars`);
|
|
5740
|
+
const envFilePath = path12.join(projectPath, ".env");
|
|
5741
|
+
if (!fs11.existsSync(envFilePath) && Object.keys(result.envVars).length > 0) {
|
|
5742
|
+
console.log(`[DevServerRunner] Writing .env file`);
|
|
5743
|
+
writeEnvFile(projectPath, result.envVars);
|
|
5744
|
+
}
|
|
5745
|
+
return result.envVars;
|
|
5746
|
+
} catch (error) {
|
|
5747
|
+
console.warn(`[DevServerRunner] Failed to fetch env vars:`, error);
|
|
5748
|
+
return {};
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5751
|
+
spawnProcess(projectPath, port, moduleUid, logPath, customCommand, envVars) {
|
|
5752
|
+
this.rotateLogIfNeeded(logPath);
|
|
5753
|
+
const nodeOptions = process.env.NODE_OPTIONS || "";
|
|
5754
|
+
const memoryFlag = `--max-old-space-size=${DEV_SERVER_CONSTANTS.NODE_MEMORY_LIMIT_MB}`;
|
|
5755
|
+
const enhancedNodeOptions = nodeOptions.includes("max-old-space-size") ? nodeOptions : `${nodeOptions} ${memoryFlag}`.trim();
|
|
5756
|
+
const command = customCommand || "npm run dev";
|
|
5757
|
+
const [cmd, ...args] = command.split(" ");
|
|
5758
|
+
console.log(`[DevServerRunner] Running: ${command}`);
|
|
5759
|
+
const mergedEnv = {
|
|
5760
|
+
...process.env,
|
|
5761
|
+
...envVars,
|
|
5762
|
+
PORT: String(port),
|
|
5763
|
+
NODE_OPTIONS: enhancedNodeOptions
|
|
5764
|
+
};
|
|
5765
|
+
const proc = (0, import_child_process9.spawn)(cmd, args, {
|
|
5766
|
+
cwd: projectPath,
|
|
5767
|
+
env: mergedEnv,
|
|
5768
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5769
|
+
detached: false,
|
|
5770
|
+
shell: true
|
|
5771
|
+
});
|
|
5772
|
+
proc.stdout?.on("data", (data) => {
|
|
5773
|
+
const line = data.toString().trim();
|
|
5774
|
+
if (line) {
|
|
5775
|
+
console.log(`[DevServer:${moduleUid}] ${line}`);
|
|
5776
|
+
this.writeToLog(logPath, line);
|
|
5777
|
+
}
|
|
5778
|
+
});
|
|
5779
|
+
proc.stderr?.on("data", (data) => {
|
|
5780
|
+
const line = data.toString().trim();
|
|
5781
|
+
if (line) {
|
|
5782
|
+
console.error(`[DevServer:${moduleUid}] ${line}`);
|
|
5783
|
+
this.writeToLog(logPath, line, true);
|
|
5784
|
+
}
|
|
5785
|
+
});
|
|
5786
|
+
return proc;
|
|
5787
|
+
}
|
|
5788
|
+
setupProcessHandlers(moduleUid, proc, logPath) {
|
|
5789
|
+
proc.on("exit", (code, signal) => {
|
|
5790
|
+
this.handleProcessExit(moduleUid, code, signal);
|
|
5791
|
+
});
|
|
5792
|
+
proc.on("error", (error) => {
|
|
5793
|
+
console.error(`[DevServerRunner] Process error for ${moduleUid}:`, error);
|
|
5794
|
+
this.writeToLog(logPath, `Process error: ${error.message}`, true);
|
|
5795
|
+
this.emit("error", moduleUid, error);
|
|
5796
|
+
});
|
|
5797
|
+
}
|
|
5798
|
+
async handleProcessExit(moduleUid, code, signal) {
|
|
5799
|
+
const state = this.servers.get(moduleUid);
|
|
5800
|
+
if (!state) {
|
|
5801
|
+
return;
|
|
5802
|
+
}
|
|
5803
|
+
const exitReason = signal ? `signal ${signal}` : `code ${code}`;
|
|
5804
|
+
console.log(`[DevServerRunner] Process for ${moduleUid} exited with ${exitReason}`);
|
|
5805
|
+
this.writeToLog(state.logFile, `Process exited with ${exitReason}`, true);
|
|
5806
|
+
if (!state.autoRestartEnabled) {
|
|
5807
|
+
this.servers.delete(moduleUid);
|
|
5808
|
+
return;
|
|
5809
|
+
}
|
|
5810
|
+
if (state.restartCount >= DEV_SERVER_CONSTANTS.MAX_RESTART_ATTEMPTS) {
|
|
5811
|
+
console.error(`[DevServerRunner] Max restart attempts reached for ${moduleUid}`);
|
|
5812
|
+
this.writeToLog(state.logFile, "Max restart attempts reached", true);
|
|
5813
|
+
this.servers.delete(moduleUid);
|
|
5814
|
+
return;
|
|
5815
|
+
}
|
|
5816
|
+
const delay = this.calculateRestartDelay(state.restartCount);
|
|
5817
|
+
console.log(`[DevServerRunner] Restarting ${moduleUid} in ${delay}ms (attempt ${state.restartCount + 1})`);
|
|
5818
|
+
await this.wait(delay);
|
|
5819
|
+
if (!this.servers.has(moduleUid)) {
|
|
5820
|
+
return;
|
|
5821
|
+
}
|
|
5822
|
+
const logPath = state.logFile || this.getLogFilePath(moduleUid);
|
|
5823
|
+
const newProcess = this.spawnProcess(
|
|
5824
|
+
state.projectPath,
|
|
5825
|
+
state.port,
|
|
5826
|
+
moduleUid,
|
|
5827
|
+
logPath,
|
|
5828
|
+
state.customCommand,
|
|
5829
|
+
state.injectedEnvVars
|
|
5830
|
+
);
|
|
5831
|
+
state.process = newProcess;
|
|
5832
|
+
state.restartCount++;
|
|
5833
|
+
state.lastRestartAt = /* @__PURE__ */ new Date();
|
|
5834
|
+
this.setupProcessHandlers(moduleUid, newProcess, logPath);
|
|
5835
|
+
const ready = await this.waitForPort(state.port, DEV_SERVER_CONSTANTS.STARTUP_TIMEOUT_MS);
|
|
5836
|
+
if (ready) {
|
|
5837
|
+
console.log(`[DevServerRunner] Server ${moduleUid} restarted successfully`);
|
|
5838
|
+
state.restartCount = 0;
|
|
5839
|
+
this.emit("restarted", moduleUid, state.restartCount);
|
|
5840
|
+
} else {
|
|
5841
|
+
console.error(`[DevServerRunner] Server ${moduleUid} failed to restart after attempt ${state.restartCount}`);
|
|
5842
|
+
this.writeToLog(logPath, `Failed to restart (attempt ${state.restartCount})`, true);
|
|
5843
|
+
if (state.restartCount >= DEV_SERVER_CONSTANTS.MAX_RESTART_ATTEMPTS) {
|
|
5844
|
+
console.error(`[DevServerRunner] Max restart attempts reached for ${moduleUid}, cleaning up`);
|
|
5845
|
+
this.writeToLog(logPath, "Max restart attempts reached, giving up", true);
|
|
5846
|
+
this.servers.delete(moduleUid);
|
|
5847
|
+
this.emit("permanent_failure", moduleUid, new Error("Max restart attempts reached"));
|
|
5848
|
+
}
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
calculateRestartDelay(restartCount) {
|
|
5852
|
+
const delay = DEV_SERVER_CONSTANTS.INITIAL_RESTART_DELAY_MS * Math.pow(2, restartCount);
|
|
5853
|
+
return Math.min(delay, DEV_SERVER_CONSTANTS.MAX_RESTART_DELAY_MS);
|
|
5854
|
+
}
|
|
5855
|
+
async checkHealth(port) {
|
|
5856
|
+
return new Promise((resolve3) => {
|
|
5857
|
+
const req = http.request(
|
|
5858
|
+
{
|
|
5859
|
+
hostname: "localhost",
|
|
5860
|
+
port,
|
|
5861
|
+
path: "/",
|
|
5862
|
+
method: "HEAD",
|
|
5863
|
+
timeout: DEV_SERVER_CONSTANTS.HEALTH_CHECK_TIMEOUT_MS
|
|
5864
|
+
},
|
|
5865
|
+
() => resolve3(true)
|
|
5866
|
+
);
|
|
5867
|
+
req.on("error", () => resolve3(false));
|
|
5868
|
+
req.on("timeout", () => {
|
|
5869
|
+
req.destroy();
|
|
5870
|
+
resolve3(false);
|
|
5871
|
+
});
|
|
5872
|
+
req.end();
|
|
5873
|
+
});
|
|
5874
|
+
}
|
|
5875
|
+
async waitForPort(port, timeoutMs) {
|
|
5876
|
+
const startTime = Date.now();
|
|
5877
|
+
const checkInterval = 500;
|
|
5878
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
5879
|
+
if (await isPortInUse(port)) {
|
|
5880
|
+
return true;
|
|
5881
|
+
}
|
|
5882
|
+
await this.wait(checkInterval);
|
|
5883
|
+
}
|
|
5884
|
+
return false;
|
|
5885
|
+
}
|
|
5886
|
+
wait(ms) {
|
|
5887
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5888
|
+
}
|
|
5889
|
+
getLogsDir() {
|
|
5890
|
+
const logsDir = path12.join((0, import_core7.getConfigDir)(), "logs");
|
|
5891
|
+
if (!fs11.existsSync(logsDir)) {
|
|
5892
|
+
fs11.mkdirSync(logsDir, { recursive: true });
|
|
5893
|
+
}
|
|
5894
|
+
return logsDir;
|
|
5895
|
+
}
|
|
5896
|
+
getLogFilePath(moduleUid) {
|
|
5897
|
+
return path12.join(this.getLogsDir(), `dev-${moduleUid}.log`);
|
|
5898
|
+
}
|
|
5899
|
+
rotateLogIfNeeded(logPath) {
|
|
5900
|
+
try {
|
|
5901
|
+
if (fs11.existsSync(logPath)) {
|
|
5902
|
+
const stats = fs11.statSync(logPath);
|
|
5903
|
+
if (stats.size > DEV_SERVER_CONSTANTS.MAX_LOG_SIZE_BYTES) {
|
|
5904
|
+
const backupPath = `${logPath}.1`;
|
|
5905
|
+
if (fs11.existsSync(backupPath)) {
|
|
5906
|
+
fs11.unlinkSync(backupPath);
|
|
5907
|
+
}
|
|
5908
|
+
fs11.renameSync(logPath, backupPath);
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
} catch {
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
writeToLog(logPath, line, isError = false) {
|
|
5915
|
+
if (!logPath) return;
|
|
5916
|
+
try {
|
|
5917
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
5918
|
+
const prefix = isError ? "ERR" : "OUT";
|
|
5919
|
+
fs11.appendFileSync(logPath, `[${timestamp}] [${prefix}] ${line}
|
|
5920
|
+
`);
|
|
5921
|
+
} catch {
|
|
5922
|
+
}
|
|
5923
|
+
}
|
|
5924
|
+
stateToStatus(state) {
|
|
5925
|
+
return {
|
|
5926
|
+
moduleUid: state.moduleUid,
|
|
5927
|
+
port: state.port,
|
|
5928
|
+
pid: state.process.pid,
|
|
5929
|
+
startedAt: state.startedAt,
|
|
5930
|
+
uptime: Math.floor((Date.now() - state.startedAt.getTime()) / 1e3),
|
|
5931
|
+
restartCount: state.restartCount,
|
|
5932
|
+
lastRestartAt: state.lastRestartAt,
|
|
5933
|
+
autoRestartEnabled: state.autoRestartEnabled,
|
|
5934
|
+
logFile: state.logFile
|
|
5935
|
+
};
|
|
5936
|
+
}
|
|
5937
|
+
};
|
|
5938
|
+
var instance2 = null;
|
|
5939
|
+
function getDevServerRunner() {
|
|
5940
|
+
if (!instance2) {
|
|
5941
|
+
instance2 = new DevServerRunner();
|
|
5942
|
+
}
|
|
5943
|
+
return instance2;
|
|
5944
|
+
}
|
|
5945
|
+
|
|
5946
|
+
// src/preview/preview-manager.ts
|
|
5947
|
+
var DEFAULT_PORT = 3e3;
|
|
5948
|
+
var PreviewManager = class extends import_events3.EventEmitter {
|
|
5949
|
+
constructor() {
|
|
5950
|
+
super();
|
|
5951
|
+
this.previews = /* @__PURE__ */ new Map();
|
|
5952
|
+
this.startingModules = /* @__PURE__ */ new Set();
|
|
5953
|
+
// Prevents concurrent startPreview() race conditions
|
|
5954
|
+
this.initialized = false;
|
|
5955
|
+
this.devServer = getDevServerRunner();
|
|
5956
|
+
this.tunnel = getTunnelManager();
|
|
5957
|
+
this.setupEventForwarding();
|
|
5958
|
+
}
|
|
5959
|
+
/**
|
|
5960
|
+
* Initialize the preview manager
|
|
5961
|
+
*
|
|
5962
|
+
* Must be called before starting any previews.
|
|
5963
|
+
* Initializes the tunnel manager (ensures cloudflared, cleans orphans).
|
|
5964
|
+
*/
|
|
5965
|
+
async initialize() {
|
|
5966
|
+
if (this.initialized) {
|
|
5967
|
+
return;
|
|
5968
|
+
}
|
|
5969
|
+
console.log("[PreviewManager] Initializing...");
|
|
5970
|
+
await this.tunnel.initialize();
|
|
5971
|
+
this.initialized = true;
|
|
5972
|
+
console.log("[PreviewManager] Initialized");
|
|
5973
|
+
}
|
|
5974
|
+
/**
|
|
5975
|
+
* Start a preview for a module
|
|
5976
|
+
*
|
|
5977
|
+
* This will:
|
|
5978
|
+
* 1. Start the dev server in the worktree
|
|
5979
|
+
* 2. Provision a Named Tunnel via the platform API
|
|
5980
|
+
* 3. Connect the tunnel to the dev server
|
|
5981
|
+
* 4. Return the preview URL
|
|
5982
|
+
*
|
|
5983
|
+
* @param config - Preview configuration
|
|
5984
|
+
* @returns Result with success status and preview URL
|
|
5985
|
+
*/
|
|
5986
|
+
async startPreview(config) {
|
|
5987
|
+
const { moduleUid, worktreePath, port = DEFAULT_PORT, customCommand } = config;
|
|
5988
|
+
if (!worktreePath) {
|
|
5989
|
+
return { success: false, error: "Worktree path is required" };
|
|
5990
|
+
}
|
|
5991
|
+
if (!(0, import_fs.existsSync)(worktreePath)) {
|
|
5992
|
+
console.error(`[PreviewManager] Worktree path does not exist: ${worktreePath}`);
|
|
5993
|
+
return { success: false, error: `Worktree path does not exist: ${worktreePath}` };
|
|
5994
|
+
}
|
|
5995
|
+
try {
|
|
5996
|
+
const stats = (0, import_fs.statSync)(worktreePath);
|
|
5997
|
+
if (!stats.isDirectory()) {
|
|
5998
|
+
console.error(`[PreviewManager] Worktree path is not a directory: ${worktreePath}`);
|
|
5999
|
+
return { success: false, error: `Worktree path is not a directory: ${worktreePath}` };
|
|
6000
|
+
}
|
|
6001
|
+
} catch (error) {
|
|
6002
|
+
console.error(`[PreviewManager] Cannot access worktree path: ${worktreePath}`, error);
|
|
6003
|
+
return { success: false, error: `Cannot access worktree path: ${worktreePath}` };
|
|
6004
|
+
}
|
|
6005
|
+
if (!this.initialized) {
|
|
6006
|
+
await this.initialize();
|
|
6007
|
+
}
|
|
6008
|
+
if (this.startingModules.has(moduleUid)) {
|
|
6009
|
+
console.log(`[PreviewManager] Preview startup already in progress for ${moduleUid}`);
|
|
6010
|
+
return { success: false, error: "Preview startup already in progress" };
|
|
6011
|
+
}
|
|
6012
|
+
const existing = this.previews.get(moduleUid);
|
|
6013
|
+
if (existing && (existing.state === "live" || existing.state === "running")) {
|
|
6014
|
+
console.log(`[PreviewManager] Preview already running for ${moduleUid}`);
|
|
6015
|
+
return {
|
|
6016
|
+
success: true,
|
|
6017
|
+
previewUrl: existing.tunnelUrl,
|
|
6018
|
+
alreadyRunning: true
|
|
6019
|
+
};
|
|
6020
|
+
}
|
|
6021
|
+
this.startingModules.add(moduleUid);
|
|
6022
|
+
console.log(`[PreviewManager] Starting preview for ${moduleUid} at ${worktreePath}:${port}`);
|
|
6023
|
+
const state = {
|
|
6024
|
+
moduleUid,
|
|
6025
|
+
worktreePath,
|
|
6026
|
+
port,
|
|
6027
|
+
state: "starting",
|
|
6028
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
6029
|
+
};
|
|
6030
|
+
this.previews.set(moduleUid, state);
|
|
6031
|
+
this.emitStateChange(moduleUid, "starting");
|
|
6032
|
+
try {
|
|
6033
|
+
console.log(`[PreviewManager] Starting dev server for ${moduleUid}...`);
|
|
6034
|
+
const devResult = await this.devServer.start({
|
|
6035
|
+
projectPath: worktreePath,
|
|
6036
|
+
port,
|
|
6037
|
+
moduleUid,
|
|
6038
|
+
customCommand,
|
|
6039
|
+
autoRestart: true
|
|
6040
|
+
});
|
|
6041
|
+
if (!devResult.success) {
|
|
6042
|
+
state.state = "error";
|
|
6043
|
+
state.error = devResult.error || "Failed to start dev server";
|
|
6044
|
+
this.emitStateChange(moduleUid, "error");
|
|
6045
|
+
this.emit("error", moduleUid, new Error(state.error));
|
|
6046
|
+
return { success: false, error: state.error };
|
|
6047
|
+
}
|
|
6048
|
+
state.state = "running";
|
|
6049
|
+
this.emitStateChange(moduleUid, "running");
|
|
6050
|
+
console.log(`[PreviewManager] Dev server running on port ${port}`);
|
|
6051
|
+
console.log(`[PreviewManager] Starting Named Tunnel for ${moduleUid}...`);
|
|
6052
|
+
state.state = "tunneling";
|
|
6053
|
+
this.emitStateChange(moduleUid, "tunneling");
|
|
6054
|
+
const MAX_TUNNEL_RETRIES = 2;
|
|
6055
|
+
let tunnelResult = { success: false };
|
|
6056
|
+
let lastError = "";
|
|
6057
|
+
for (let attempt = 1; attempt <= MAX_TUNNEL_RETRIES; attempt++) {
|
|
6058
|
+
if (attempt > 1) {
|
|
6059
|
+
console.log(`[PreviewManager] Retrying tunnel for ${moduleUid} (attempt ${attempt}/${MAX_TUNNEL_RETRIES})...`);
|
|
6060
|
+
await new Promise((resolve3) => setTimeout(resolve3, 2e3));
|
|
6061
|
+
}
|
|
6062
|
+
tunnelResult = await this.tunnel.startTunnel({
|
|
6063
|
+
moduleUid,
|
|
6064
|
+
port,
|
|
6065
|
+
mode: "named",
|
|
6066
|
+
// Named Tunnels only
|
|
6067
|
+
onStatusChange: (status, error) => {
|
|
6068
|
+
console.log(`[PreviewManager] Tunnel status for ${moduleUid}: ${status}${error ? ` - ${error}` : ""}`);
|
|
6069
|
+
if (status === "error") {
|
|
6070
|
+
state.state = "error";
|
|
6071
|
+
state.error = error || "Tunnel error";
|
|
6072
|
+
this.emitStateChange(moduleUid, "error");
|
|
6073
|
+
} else if (status === "disconnected") {
|
|
6074
|
+
state.state = "running";
|
|
6075
|
+
state.tunnelUrl = void 0;
|
|
6076
|
+
this.emitStateChange(moduleUid, "running");
|
|
6077
|
+
} else if (status === "reconnecting") {
|
|
6078
|
+
state.state = "tunneling";
|
|
6079
|
+
this.emitStateChange(moduleUid, "tunneling");
|
|
6080
|
+
}
|
|
6081
|
+
},
|
|
6082
|
+
onUrl: (url) => {
|
|
6083
|
+
state.tunnelUrl = url;
|
|
6084
|
+
}
|
|
6085
|
+
});
|
|
6086
|
+
if (tunnelResult.success) {
|
|
6087
|
+
break;
|
|
6088
|
+
}
|
|
6089
|
+
lastError = tunnelResult.error || "Unknown tunnel error";
|
|
6090
|
+
console.warn(`[PreviewManager] Tunnel attempt ${attempt} failed for ${moduleUid}: ${lastError}`);
|
|
6091
|
+
}
|
|
6092
|
+
if (!tunnelResult.success) {
|
|
6093
|
+
console.error(`[PreviewManager] Tunnel failed after ${MAX_TUNNEL_RETRIES} attempts for ${moduleUid}, stopping dev server`);
|
|
6094
|
+
try {
|
|
6095
|
+
await this.devServer.stop(moduleUid);
|
|
6096
|
+
} catch (cleanupError) {
|
|
6097
|
+
console.warn(`[PreviewManager] Error cleaning up dev server after tunnel failure:`, cleanupError);
|
|
6098
|
+
}
|
|
6099
|
+
state.state = "error";
|
|
6100
|
+
state.error = lastError;
|
|
6101
|
+
this.previews.delete(moduleUid);
|
|
6102
|
+
this.emitStateChange(moduleUid, "error");
|
|
6103
|
+
return {
|
|
6104
|
+
success: false,
|
|
6105
|
+
error: `Tunnel failed after ${MAX_TUNNEL_RETRIES} attempts: ${lastError}`
|
|
6106
|
+
};
|
|
6107
|
+
}
|
|
6108
|
+
state.state = "live";
|
|
6109
|
+
state.tunnelUrl = tunnelResult.url;
|
|
6110
|
+
state.error = void 0;
|
|
6111
|
+
this.emitStateChange(moduleUid, "live");
|
|
6112
|
+
this.emit("live", moduleUid, tunnelResult.url);
|
|
6113
|
+
console.log(`[PreviewManager] Preview live for ${moduleUid}: ${tunnelResult.url}`);
|
|
6114
|
+
return {
|
|
6115
|
+
success: true,
|
|
6116
|
+
previewUrl: tunnelResult.url
|
|
6117
|
+
};
|
|
6118
|
+
} catch (error) {
|
|
6119
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
6120
|
+
console.error(`[PreviewManager] Error starting preview for ${moduleUid}:`, error);
|
|
6121
|
+
state.state = "error";
|
|
6122
|
+
state.error = errorMsg;
|
|
6123
|
+
this.emitStateChange(moduleUid, "error");
|
|
6124
|
+
this.emit("error", moduleUid, error instanceof Error ? error : new Error(errorMsg));
|
|
6125
|
+
return { success: false, error: errorMsg };
|
|
6126
|
+
} finally {
|
|
6127
|
+
this.startingModules.delete(moduleUid);
|
|
6128
|
+
}
|
|
6129
|
+
}
|
|
6130
|
+
/**
|
|
6131
|
+
* Stop a preview for a module
|
|
6132
|
+
*
|
|
6133
|
+
* This will:
|
|
6134
|
+
* 1. Stop the tunnel
|
|
6135
|
+
* 2. Stop the dev server
|
|
6136
|
+
* 3. Clear the tunnel URL from the platform API
|
|
6137
|
+
*
|
|
6138
|
+
* @param moduleUid - Module identifier
|
|
6139
|
+
*/
|
|
6140
|
+
async stopPreview(moduleUid) {
|
|
6141
|
+
console.log(`[PreviewManager] Stopping preview for ${moduleUid}`);
|
|
6142
|
+
const state = this.previews.get(moduleUid);
|
|
6143
|
+
try {
|
|
6144
|
+
await this.tunnel.stopTunnel(moduleUid);
|
|
6145
|
+
} catch (error) {
|
|
6146
|
+
console.warn(`[PreviewManager] Error stopping tunnel for ${moduleUid}:`, error);
|
|
6147
|
+
}
|
|
6148
|
+
try {
|
|
6149
|
+
await this.devServer.stop(moduleUid);
|
|
6150
|
+
} catch (error) {
|
|
6151
|
+
console.warn(`[PreviewManager] Error stopping dev server for ${moduleUid}:`, error);
|
|
6152
|
+
}
|
|
6153
|
+
try {
|
|
6154
|
+
await clearTunnelUrl(moduleUid);
|
|
6155
|
+
} catch (error) {
|
|
6156
|
+
console.warn(`[PreviewManager] Error clearing tunnel URL for ${moduleUid}:`, error);
|
|
6157
|
+
}
|
|
6158
|
+
if (state) {
|
|
6159
|
+
state.state = "stopped";
|
|
6160
|
+
state.tunnelUrl = void 0;
|
|
6161
|
+
}
|
|
6162
|
+
this.previews.delete(moduleUid);
|
|
6163
|
+
this.emitStateChange(moduleUid, "stopped");
|
|
6164
|
+
this.emit("stopped", moduleUid);
|
|
6165
|
+
console.log(`[PreviewManager] Preview stopped for ${moduleUid}`);
|
|
6166
|
+
}
|
|
6167
|
+
/**
|
|
6168
|
+
* Restart a preview for a module
|
|
6169
|
+
*
|
|
6170
|
+
* @param moduleUid - Module identifier
|
|
6171
|
+
* @returns Result with success status and new preview URL
|
|
6172
|
+
*/
|
|
6173
|
+
async restartPreview(moduleUid) {
|
|
6174
|
+
const state = this.previews.get(moduleUid);
|
|
6175
|
+
if (!state) {
|
|
6176
|
+
return { success: false, error: `No preview found for ${moduleUid}` };
|
|
6177
|
+
}
|
|
6178
|
+
console.log(`[PreviewManager] Restarting preview for ${moduleUid}`);
|
|
6179
|
+
await this.stopPreview(moduleUid);
|
|
6180
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
6181
|
+
return this.startPreview({
|
|
6182
|
+
moduleUid,
|
|
6183
|
+
worktreePath: state.worktreePath,
|
|
6184
|
+
port: state.port
|
|
6185
|
+
});
|
|
6186
|
+
}
|
|
6187
|
+
/**
|
|
6188
|
+
* Get the status of a preview
|
|
6189
|
+
*
|
|
6190
|
+
* @param moduleUid - Module identifier
|
|
6191
|
+
* @returns Preview status or undefined if not found
|
|
6192
|
+
*/
|
|
6193
|
+
getStatus(moduleUid) {
|
|
6194
|
+
const state = this.previews.get(moduleUid);
|
|
6195
|
+
if (!state) {
|
|
6196
|
+
return void 0;
|
|
6197
|
+
}
|
|
6198
|
+
const devServerStatus = this.devServer.getStatus(moduleUid);
|
|
6199
|
+
const tunnelInfo = this.tunnel.getTunnel(moduleUid);
|
|
6200
|
+
return {
|
|
6201
|
+
moduleUid: state.moduleUid,
|
|
6202
|
+
state: state.state,
|
|
6203
|
+
devServer: devServerStatus,
|
|
6204
|
+
tunnelUrl: state.tunnelUrl,
|
|
6205
|
+
tunnelState: tunnelInfo?.status === "connected" ? "connected" : tunnelInfo?.status === "starting" ? "starting" : tunnelInfo?.status === "error" ? "error" : tunnelInfo?.status === "disconnected" ? "disconnected" : void 0,
|
|
6206
|
+
port: state.port,
|
|
6207
|
+
error: state.error,
|
|
6208
|
+
startedAt: state.startedAt
|
|
6209
|
+
};
|
|
6210
|
+
}
|
|
6211
|
+
/**
|
|
6212
|
+
* Get the status of all previews
|
|
6213
|
+
*
|
|
6214
|
+
* @returns Array of preview statuses
|
|
6215
|
+
*/
|
|
6216
|
+
getAllStatus() {
|
|
6217
|
+
return Array.from(this.previews.keys()).map((uid) => this.getStatus(uid));
|
|
6218
|
+
}
|
|
6219
|
+
/**
|
|
6220
|
+
* Check if a preview is running
|
|
6221
|
+
*
|
|
6222
|
+
* @param moduleUid - Module identifier
|
|
6223
|
+
* @returns True if preview is running (dev server active)
|
|
6224
|
+
*/
|
|
6225
|
+
isRunning(moduleUid) {
|
|
6226
|
+
const state = this.previews.get(moduleUid);
|
|
6227
|
+
return !!state && (state.state === "running" || state.state === "live" || state.state === "tunneling");
|
|
6228
|
+
}
|
|
6229
|
+
/**
|
|
6230
|
+
* Check if a preview is fully live (dev server + tunnel)
|
|
6231
|
+
*
|
|
6232
|
+
* @param moduleUid - Module identifier
|
|
6233
|
+
* @returns True if preview is fully live with tunnel connected
|
|
6234
|
+
*/
|
|
6235
|
+
isLive(moduleUid) {
|
|
6236
|
+
const state = this.previews.get(moduleUid);
|
|
6237
|
+
return !!state && state.state === "live";
|
|
6238
|
+
}
|
|
6239
|
+
/**
|
|
6240
|
+
* Get the preview URL for a module
|
|
6241
|
+
*
|
|
6242
|
+
* @param moduleUid - Module identifier
|
|
6243
|
+
* @returns Preview URL or undefined if not available
|
|
6244
|
+
*/
|
|
6245
|
+
getPreviewUrl(moduleUid) {
|
|
6246
|
+
return this.previews.get(moduleUid)?.tunnelUrl;
|
|
6247
|
+
}
|
|
6248
|
+
/**
|
|
6249
|
+
* Stop all previews
|
|
6250
|
+
*/
|
|
6251
|
+
async stopAll() {
|
|
6252
|
+
console.log("[PreviewManager] Stopping all previews...");
|
|
6253
|
+
const moduleUids = Array.from(this.previews.keys());
|
|
6254
|
+
await Promise.all(moduleUids.map((uid) => this.stopPreview(uid)));
|
|
6255
|
+
console.log("[PreviewManager] All previews stopped");
|
|
6256
|
+
}
|
|
6257
|
+
/**
|
|
6258
|
+
* Get all module UIDs with active previews
|
|
6259
|
+
*/
|
|
6260
|
+
getActiveModuleUids() {
|
|
6261
|
+
return Array.from(this.previews.keys()).filter((uid) => this.isRunning(uid));
|
|
6262
|
+
}
|
|
6263
|
+
// ============ Private Methods ============
|
|
6264
|
+
setupEventForwarding() {
|
|
6265
|
+
this.devServer.on("started", (moduleUid, port) => {
|
|
6266
|
+
console.log(`[PreviewManager] Dev server started: ${moduleUid} on port ${port}`);
|
|
6267
|
+
});
|
|
6268
|
+
this.devServer.on("stopped", (moduleUid) => {
|
|
6269
|
+
console.log(`[PreviewManager] Dev server stopped: ${moduleUid}`);
|
|
6270
|
+
const state = this.previews.get(moduleUid);
|
|
6271
|
+
if (state && state.state !== "stopped") {
|
|
6272
|
+
state.state = "error";
|
|
6273
|
+
state.error = "Dev server stopped unexpectedly";
|
|
6274
|
+
this.emitStateChange(moduleUid, "error");
|
|
6275
|
+
}
|
|
6276
|
+
});
|
|
6277
|
+
this.devServer.on("error", (moduleUid, error) => {
|
|
6278
|
+
console.error(`[PreviewManager] Dev server error: ${moduleUid}`, error);
|
|
6279
|
+
const state = this.previews.get(moduleUid);
|
|
6280
|
+
if (state) {
|
|
6281
|
+
state.state = "error";
|
|
6282
|
+
state.error = error.message;
|
|
6283
|
+
this.emitStateChange(moduleUid, "error");
|
|
6284
|
+
}
|
|
6285
|
+
});
|
|
6286
|
+
this.devServer.on("permanent_failure", (moduleUid, error) => {
|
|
6287
|
+
console.error(`[PreviewManager] Dev server permanent failure: ${moduleUid}`, error);
|
|
6288
|
+
const state = this.previews.get(moduleUid);
|
|
6289
|
+
if (state) {
|
|
6290
|
+
state.state = "error";
|
|
6291
|
+
state.error = `Dev server failed permanently: ${error.message}`;
|
|
6292
|
+
this.emitStateChange(moduleUid, "error");
|
|
6293
|
+
this.emit("error", moduleUid, error);
|
|
6294
|
+
this.tunnel.stopTunnel(moduleUid).catch(() => {
|
|
6295
|
+
});
|
|
6296
|
+
this.previews.delete(moduleUid);
|
|
6297
|
+
}
|
|
6298
|
+
});
|
|
6299
|
+
this.tunnel.on("tunnel", (event) => {
|
|
6300
|
+
const moduleUid = event.moduleUid;
|
|
6301
|
+
const state = this.previews.get(moduleUid);
|
|
6302
|
+
if (!state) return;
|
|
6303
|
+
if (event.type === "started") {
|
|
6304
|
+
state.tunnelUrl = event.url;
|
|
6305
|
+
state.state = "live";
|
|
6306
|
+
this.emitStateChange(moduleUid, "live");
|
|
6307
|
+
this.emit("live", moduleUid, event.url);
|
|
6308
|
+
} else if (event.type === "stopped") {
|
|
6309
|
+
state.tunnelUrl = void 0;
|
|
6310
|
+
if (state.state === "live") {
|
|
6311
|
+
state.state = "running";
|
|
6312
|
+
this.emitStateChange(moduleUid, "running");
|
|
6313
|
+
}
|
|
6314
|
+
} else if (event.type === "error") {
|
|
6315
|
+
console.error(`[PreviewManager] Tunnel error for ${moduleUid}:`, event.error);
|
|
6316
|
+
} else if (event.type === "reconnecting") {
|
|
6317
|
+
state.state = "tunneling";
|
|
6318
|
+
this.emitStateChange(moduleUid, "tunneling");
|
|
6319
|
+
}
|
|
6320
|
+
});
|
|
6321
|
+
}
|
|
6322
|
+
emitStateChange(moduleUid, state) {
|
|
6323
|
+
this.emit("stateChange", moduleUid, state);
|
|
6324
|
+
}
|
|
6325
|
+
};
|
|
6326
|
+
var instance3 = null;
|
|
6327
|
+
function getPreviewManager() {
|
|
6328
|
+
if (!instance3) {
|
|
6329
|
+
instance3 = new PreviewManager();
|
|
6330
|
+
}
|
|
6331
|
+
return instance3;
|
|
6332
|
+
}
|
|
6333
|
+
|
|
5214
6334
|
// src/utils/dev-server.ts
|
|
5215
|
-
var
|
|
5216
|
-
var
|
|
5217
|
-
var
|
|
6335
|
+
var import_child_process10 = require("child_process");
|
|
6336
|
+
var import_core8 = __toESM(require_dist());
|
|
6337
|
+
var fs12 = __toESM(require("fs"));
|
|
6338
|
+
var path13 = __toESM(require("path"));
|
|
5218
6339
|
var MAX_RESTART_ATTEMPTS = 5;
|
|
5219
6340
|
var INITIAL_RESTART_DELAY_MS = 2e3;
|
|
5220
6341
|
var MAX_RESTART_DELAY_MS = 3e4;
|
|
@@ -5222,26 +6343,26 @@ var MAX_LOG_SIZE_BYTES = 5 * 1024 * 1024;
|
|
|
5222
6343
|
var NODE_MEMORY_LIMIT_MB = 2048;
|
|
5223
6344
|
var activeServers = /* @__PURE__ */ new Map();
|
|
5224
6345
|
function getLogsDir() {
|
|
5225
|
-
const logsDir =
|
|
5226
|
-
if (!
|
|
5227
|
-
|
|
6346
|
+
const logsDir = path13.join((0, import_core8.getConfigDir)(), "logs");
|
|
6347
|
+
if (!fs12.existsSync(logsDir)) {
|
|
6348
|
+
fs12.mkdirSync(logsDir, { recursive: true });
|
|
5228
6349
|
}
|
|
5229
6350
|
return logsDir;
|
|
5230
6351
|
}
|
|
5231
6352
|
function getLogFilePath(moduleUid) {
|
|
5232
|
-
return
|
|
6353
|
+
return path13.join(getLogsDir(), `dev-${moduleUid}.log`);
|
|
5233
6354
|
}
|
|
5234
6355
|
function rotateLogIfNeeded(logPath) {
|
|
5235
6356
|
try {
|
|
5236
|
-
if (
|
|
5237
|
-
const stats =
|
|
6357
|
+
if (fs12.existsSync(logPath)) {
|
|
6358
|
+
const stats = fs12.statSync(logPath);
|
|
5238
6359
|
if (stats.size > MAX_LOG_SIZE_BYTES) {
|
|
5239
6360
|
const backupPath = `${logPath}.1`;
|
|
5240
|
-
if (
|
|
5241
|
-
|
|
6361
|
+
if (fs12.existsSync(backupPath)) {
|
|
6362
|
+
fs12.unlinkSync(backupPath);
|
|
5242
6363
|
}
|
|
5243
|
-
|
|
5244
|
-
console.log(`[DevServer] EP932: Rotated log file for ${
|
|
6364
|
+
fs12.renameSync(logPath, backupPath);
|
|
6365
|
+
console.log(`[DevServer] EP932: Rotated log file for ${path13.basename(logPath)}`);
|
|
5245
6366
|
}
|
|
5246
6367
|
}
|
|
5247
6368
|
} catch (error) {
|
|
@@ -5254,37 +6375,13 @@ function writeToLog(logPath, line, isError = false) {
|
|
|
5254
6375
|
const prefix = isError ? "ERR" : "OUT";
|
|
5255
6376
|
const logLine = `[${timestamp}] [${prefix}] ${line}
|
|
5256
6377
|
`;
|
|
5257
|
-
|
|
6378
|
+
fs12.appendFileSync(logPath, logLine);
|
|
5258
6379
|
} catch {
|
|
5259
6380
|
}
|
|
5260
6381
|
}
|
|
5261
|
-
async function isDevServerHealthy(port, timeoutMs = 5e3) {
|
|
5262
|
-
return new Promise((resolve3) => {
|
|
5263
|
-
const req = import_http.default.request(
|
|
5264
|
-
{
|
|
5265
|
-
hostname: "localhost",
|
|
5266
|
-
port,
|
|
5267
|
-
path: "/",
|
|
5268
|
-
method: "HEAD",
|
|
5269
|
-
timeout: timeoutMs
|
|
5270
|
-
},
|
|
5271
|
-
(res) => {
|
|
5272
|
-
resolve3(true);
|
|
5273
|
-
}
|
|
5274
|
-
);
|
|
5275
|
-
req.on("error", () => {
|
|
5276
|
-
resolve3(false);
|
|
5277
|
-
});
|
|
5278
|
-
req.on("timeout", () => {
|
|
5279
|
-
req.destroy();
|
|
5280
|
-
resolve3(false);
|
|
5281
|
-
});
|
|
5282
|
-
req.end();
|
|
5283
|
-
});
|
|
5284
|
-
}
|
|
5285
6382
|
async function killProcessOnPort(port) {
|
|
5286
6383
|
try {
|
|
5287
|
-
const result = (0,
|
|
6384
|
+
const result = (0, import_child_process10.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
|
|
5288
6385
|
if (!result) {
|
|
5289
6386
|
console.log(`[DevServer] EP929: No process found on port ${port}`);
|
|
5290
6387
|
return true;
|
|
@@ -5293,7 +6390,7 @@ async function killProcessOnPort(port) {
|
|
|
5293
6390
|
console.log(`[DevServer] EP929: Found ${pids.length} process(es) on port ${port}: ${pids.join(", ")}`);
|
|
5294
6391
|
for (const pid of pids) {
|
|
5295
6392
|
try {
|
|
5296
|
-
(0,
|
|
6393
|
+
(0, import_child_process10.execSync)(`kill -15 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
|
|
5297
6394
|
console.log(`[DevServer] EP929: Sent SIGTERM to PID ${pid}`);
|
|
5298
6395
|
} catch {
|
|
5299
6396
|
}
|
|
@@ -5301,8 +6398,8 @@ async function killProcessOnPort(port) {
|
|
|
5301
6398
|
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
5302
6399
|
for (const pid of pids) {
|
|
5303
6400
|
try {
|
|
5304
|
-
(0,
|
|
5305
|
-
(0,
|
|
6401
|
+
(0, import_child_process10.execSync)(`kill -0 ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
6402
|
+
(0, import_child_process10.execSync)(`kill -9 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
|
|
5306
6403
|
console.log(`[DevServer] EP929: Force killed PID ${pid}`);
|
|
5307
6404
|
} catch {
|
|
5308
6405
|
}
|
|
@@ -5353,7 +6450,7 @@ function spawnDevServerProcess(projectPath, port, moduleUid, logPath, customComm
|
|
|
5353
6450
|
if (injectedCount > 0) {
|
|
5354
6451
|
console.log(`[DevServer] EP998: Injecting ${injectedCount} env vars from database`);
|
|
5355
6452
|
}
|
|
5356
|
-
const devProcess = (0,
|
|
6453
|
+
const devProcess = (0, import_child_process10.spawn)(cmd, args, {
|
|
5357
6454
|
cwd: projectPath,
|
|
5358
6455
|
env: mergedEnv,
|
|
5359
6456
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -5447,7 +6544,7 @@ async function startDevServer(projectPath, port = 3e3, moduleUid = "default", op
|
|
|
5447
6544
|
console.log(`[DevServer] EP932: Starting dev server for ${moduleUid} on port ${port} (auto-restart: ${autoRestart})...`);
|
|
5448
6545
|
let injectedEnvVars = {};
|
|
5449
6546
|
try {
|
|
5450
|
-
const config = await (0,
|
|
6547
|
+
const config = await (0, import_core8.loadConfig)();
|
|
5451
6548
|
if (config?.access_token && config?.project_id) {
|
|
5452
6549
|
const apiUrl = config.api_url || "https://episoda.dev";
|
|
5453
6550
|
const result = await fetchEnvVarsWithCache(apiUrl, config.access_token, {
|
|
@@ -5457,8 +6554,8 @@ async function startDevServer(projectPath, port = 3e3, moduleUid = "default", op
|
|
|
5457
6554
|
});
|
|
5458
6555
|
injectedEnvVars = result.envVars;
|
|
5459
6556
|
console.log(`[DevServer] EP998: Loaded ${Object.keys(injectedEnvVars).length} env vars (from ${result.fromCache ? "cache" : "server"})`);
|
|
5460
|
-
const envFilePath =
|
|
5461
|
-
if (!
|
|
6557
|
+
const envFilePath = path13.join(projectPath, ".env");
|
|
6558
|
+
if (!fs12.existsSync(envFilePath) && Object.keys(injectedEnvVars).length > 0) {
|
|
5462
6559
|
console.log(`[DevServer] EP1004: .env file missing, writing ${Object.keys(injectedEnvVars).length} vars to ${envFilePath}`);
|
|
5463
6560
|
writeEnvFile(projectPath, injectedEnvVars);
|
|
5464
6561
|
}
|
|
@@ -5562,17 +6659,11 @@ function getDevServerStatus() {
|
|
|
5562
6659
|
logFile: info.logFile
|
|
5563
6660
|
}));
|
|
5564
6661
|
}
|
|
5565
|
-
async function ensureDevServer(projectPath, port = 3e3, moduleUid = "default", customCommand) {
|
|
5566
|
-
if (await isPortInUse(port)) {
|
|
5567
|
-
return { success: true };
|
|
5568
|
-
}
|
|
5569
|
-
return startDevServer(projectPath, port, moduleUid, { autoRestart: true, customCommand });
|
|
5570
|
-
}
|
|
5571
6662
|
|
|
5572
6663
|
// src/utils/port-detect.ts
|
|
5573
|
-
var
|
|
5574
|
-
var
|
|
5575
|
-
var
|
|
6664
|
+
var fs13 = __toESM(require("fs"));
|
|
6665
|
+
var path14 = __toESM(require("path"));
|
|
6666
|
+
var DEFAULT_PORT2 = 3e3;
|
|
5576
6667
|
function detectDevPort(projectPath) {
|
|
5577
6668
|
const envPort = getPortFromEnv(projectPath);
|
|
5578
6669
|
if (envPort) {
|
|
@@ -5584,20 +6675,20 @@ function detectDevPort(projectPath) {
|
|
|
5584
6675
|
console.log(`[PortDetect] Found port ${scriptPort} in package.json dev script`);
|
|
5585
6676
|
return scriptPort;
|
|
5586
6677
|
}
|
|
5587
|
-
console.log(`[PortDetect] Using default port ${
|
|
5588
|
-
return
|
|
6678
|
+
console.log(`[PortDetect] Using default port ${DEFAULT_PORT2}`);
|
|
6679
|
+
return DEFAULT_PORT2;
|
|
5589
6680
|
}
|
|
5590
6681
|
function getPortFromEnv(projectPath) {
|
|
5591
6682
|
const envPaths = [
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
6683
|
+
path14.join(projectPath, ".env"),
|
|
6684
|
+
path14.join(projectPath, ".env.local"),
|
|
6685
|
+
path14.join(projectPath, ".env.development"),
|
|
6686
|
+
path14.join(projectPath, ".env.development.local")
|
|
5596
6687
|
];
|
|
5597
6688
|
for (const envPath of envPaths) {
|
|
5598
6689
|
try {
|
|
5599
|
-
if (!
|
|
5600
|
-
const content =
|
|
6690
|
+
if (!fs13.existsSync(envPath)) continue;
|
|
6691
|
+
const content = fs13.readFileSync(envPath, "utf-8");
|
|
5601
6692
|
const lines = content.split("\n");
|
|
5602
6693
|
for (const line of lines) {
|
|
5603
6694
|
const match = line.match(/^\s*PORT\s*=\s*["']?(\d+)["']?\s*(?:#.*)?$/);
|
|
@@ -5614,10 +6705,10 @@ function getPortFromEnv(projectPath) {
|
|
|
5614
6705
|
return null;
|
|
5615
6706
|
}
|
|
5616
6707
|
function getPortFromPackageJson(projectPath) {
|
|
5617
|
-
const packageJsonPath =
|
|
6708
|
+
const packageJsonPath = path14.join(projectPath, "package.json");
|
|
5618
6709
|
try {
|
|
5619
|
-
if (!
|
|
5620
|
-
const content =
|
|
6710
|
+
if (!fs13.existsSync(packageJsonPath)) return null;
|
|
6711
|
+
const content = fs13.readFileSync(packageJsonPath, "utf-8");
|
|
5621
6712
|
const pkg = JSON.parse(content);
|
|
5622
6713
|
const devScript = pkg.scripts?.dev;
|
|
5623
6714
|
if (!devScript) return null;
|
|
@@ -5641,9 +6732,9 @@ function getPortFromPackageJson(projectPath) {
|
|
|
5641
6732
|
}
|
|
5642
6733
|
|
|
5643
6734
|
// src/daemon/worktree-manager.ts
|
|
5644
|
-
var
|
|
5645
|
-
var
|
|
5646
|
-
var
|
|
6735
|
+
var fs14 = __toESM(require("fs"));
|
|
6736
|
+
var path15 = __toESM(require("path"));
|
|
6737
|
+
var import_core9 = __toESM(require_dist());
|
|
5647
6738
|
function validateModuleUid(moduleUid) {
|
|
5648
6739
|
if (!moduleUid || typeof moduleUid !== "string" || !moduleUid.trim()) {
|
|
5649
6740
|
return false;
|
|
@@ -5666,9 +6757,9 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
5666
6757
|
// ============================================================
|
|
5667
6758
|
this.lockPath = "";
|
|
5668
6759
|
this.projectRoot = projectRoot;
|
|
5669
|
-
this.bareRepoPath =
|
|
5670
|
-
this.configPath =
|
|
5671
|
-
this.gitExecutor = new
|
|
6760
|
+
this.bareRepoPath = path15.join(projectRoot, ".bare");
|
|
6761
|
+
this.configPath = path15.join(projectRoot, ".episoda", "config.json");
|
|
6762
|
+
this.gitExecutor = new import_core9.GitExecutor();
|
|
5672
6763
|
}
|
|
5673
6764
|
/**
|
|
5674
6765
|
* Initialize worktree manager from existing project root
|
|
@@ -5676,10 +6767,10 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
5676
6767
|
* @returns true if valid project, false otherwise
|
|
5677
6768
|
*/
|
|
5678
6769
|
async initialize() {
|
|
5679
|
-
if (!
|
|
6770
|
+
if (!fs14.existsSync(this.bareRepoPath)) {
|
|
5680
6771
|
return false;
|
|
5681
6772
|
}
|
|
5682
|
-
if (!
|
|
6773
|
+
if (!fs14.existsSync(this.configPath)) {
|
|
5683
6774
|
return false;
|
|
5684
6775
|
}
|
|
5685
6776
|
try {
|
|
@@ -5699,10 +6790,10 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
5699
6790
|
*/
|
|
5700
6791
|
async ensureFetchRefspecConfigured() {
|
|
5701
6792
|
try {
|
|
5702
|
-
const { execSync:
|
|
6793
|
+
const { execSync: execSync8 } = require("child_process");
|
|
5703
6794
|
let fetchRefspec = null;
|
|
5704
6795
|
try {
|
|
5705
|
-
fetchRefspec =
|
|
6796
|
+
fetchRefspec = execSync8("git config --get remote.origin.fetch", {
|
|
5706
6797
|
cwd: this.bareRepoPath,
|
|
5707
6798
|
encoding: "utf-8",
|
|
5708
6799
|
timeout: 5e3
|
|
@@ -5711,7 +6802,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
5711
6802
|
}
|
|
5712
6803
|
if (!fetchRefspec) {
|
|
5713
6804
|
console.log("[WorktreeManager] EP1014: Configuring missing fetch refspec for bare repo");
|
|
5714
|
-
|
|
6805
|
+
execSync8('git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"', {
|
|
5715
6806
|
cwd: this.bareRepoPath,
|
|
5716
6807
|
timeout: 5e3
|
|
5717
6808
|
});
|
|
@@ -5726,8 +6817,8 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
5726
6817
|
*/
|
|
5727
6818
|
static async createProject(projectRoot, repoUrl, projectId, workspaceSlug, projectSlug) {
|
|
5728
6819
|
const manager = new _WorktreeManager(projectRoot);
|
|
5729
|
-
const episodaDir =
|
|
5730
|
-
|
|
6820
|
+
const episodaDir = path15.join(projectRoot, ".episoda");
|
|
6821
|
+
fs14.mkdirSync(episodaDir, { recursive: true });
|
|
5731
6822
|
const cloneResult = await manager.gitExecutor.execute({
|
|
5732
6823
|
action: "clone_bare",
|
|
5733
6824
|
url: repoUrl,
|
|
@@ -5758,7 +6849,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
5758
6849
|
error: `Invalid module UID: "${moduleUid}" - contains disallowed characters`
|
|
5759
6850
|
};
|
|
5760
6851
|
}
|
|
5761
|
-
const worktreePath =
|
|
6852
|
+
const worktreePath = path15.join(this.projectRoot, moduleUid);
|
|
5762
6853
|
const lockAcquired = await this.acquireLock();
|
|
5763
6854
|
if (!lockAcquired) {
|
|
5764
6855
|
return {
|
|
@@ -5940,7 +7031,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
5940
7031
|
let prunedCount = 0;
|
|
5941
7032
|
await this.updateConfigSafe((config) => {
|
|
5942
7033
|
const initialCount = config.worktrees.length;
|
|
5943
|
-
config.worktrees = config.worktrees.filter((w) =>
|
|
7034
|
+
config.worktrees = config.worktrees.filter((w) => fs14.existsSync(w.worktreePath));
|
|
5944
7035
|
prunedCount = initialCount - config.worktrees.length;
|
|
5945
7036
|
return config;
|
|
5946
7037
|
});
|
|
@@ -6021,16 +7112,16 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6021
7112
|
const retryInterval = 50;
|
|
6022
7113
|
while (Date.now() - startTime < timeoutMs) {
|
|
6023
7114
|
try {
|
|
6024
|
-
|
|
7115
|
+
fs14.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
6025
7116
|
return true;
|
|
6026
7117
|
} catch (err) {
|
|
6027
7118
|
if (err.code === "EEXIST") {
|
|
6028
7119
|
try {
|
|
6029
|
-
const stats =
|
|
7120
|
+
const stats = fs14.statSync(lockPath);
|
|
6030
7121
|
const lockAge = Date.now() - stats.mtimeMs;
|
|
6031
7122
|
if (lockAge > 3e4) {
|
|
6032
7123
|
try {
|
|
6033
|
-
const lockContent =
|
|
7124
|
+
const lockContent = fs14.readFileSync(lockPath, "utf-8").trim();
|
|
6034
7125
|
const lockPid = parseInt(lockContent, 10);
|
|
6035
7126
|
if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
|
|
6036
7127
|
await new Promise((resolve3) => setTimeout(resolve3, retryInterval));
|
|
@@ -6039,7 +7130,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6039
7130
|
} catch {
|
|
6040
7131
|
}
|
|
6041
7132
|
try {
|
|
6042
|
-
|
|
7133
|
+
fs14.unlinkSync(lockPath);
|
|
6043
7134
|
} catch {
|
|
6044
7135
|
}
|
|
6045
7136
|
continue;
|
|
@@ -6060,16 +7151,16 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6060
7151
|
*/
|
|
6061
7152
|
releaseLock() {
|
|
6062
7153
|
try {
|
|
6063
|
-
|
|
7154
|
+
fs14.unlinkSync(this.getLockPath());
|
|
6064
7155
|
} catch {
|
|
6065
7156
|
}
|
|
6066
7157
|
}
|
|
6067
7158
|
readConfig() {
|
|
6068
7159
|
try {
|
|
6069
|
-
if (!
|
|
7160
|
+
if (!fs14.existsSync(this.configPath)) {
|
|
6070
7161
|
return null;
|
|
6071
7162
|
}
|
|
6072
|
-
const content =
|
|
7163
|
+
const content = fs14.readFileSync(this.configPath, "utf-8");
|
|
6073
7164
|
return JSON.parse(content);
|
|
6074
7165
|
} catch (error) {
|
|
6075
7166
|
console.error("[WorktreeManager] Failed to read config:", error);
|
|
@@ -6078,11 +7169,11 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6078
7169
|
}
|
|
6079
7170
|
writeConfig(config) {
|
|
6080
7171
|
try {
|
|
6081
|
-
const dir =
|
|
6082
|
-
if (!
|
|
6083
|
-
|
|
7172
|
+
const dir = path15.dirname(this.configPath);
|
|
7173
|
+
if (!fs14.existsSync(dir)) {
|
|
7174
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
6084
7175
|
}
|
|
6085
|
-
|
|
7176
|
+
fs14.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
6086
7177
|
} catch (error) {
|
|
6087
7178
|
console.error("[WorktreeManager] Failed to write config:", error);
|
|
6088
7179
|
throw error;
|
|
@@ -6163,14 +7254,14 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6163
7254
|
}
|
|
6164
7255
|
try {
|
|
6165
7256
|
for (const file of files) {
|
|
6166
|
-
const srcPath =
|
|
6167
|
-
const destPath =
|
|
6168
|
-
if (
|
|
6169
|
-
const destDir =
|
|
6170
|
-
if (!
|
|
6171
|
-
|
|
6172
|
-
}
|
|
6173
|
-
|
|
7257
|
+
const srcPath = path15.join(mainWorktree.worktreePath, file);
|
|
7258
|
+
const destPath = path15.join(worktree.worktreePath, file);
|
|
7259
|
+
if (fs14.existsSync(srcPath)) {
|
|
7260
|
+
const destDir = path15.dirname(destPath);
|
|
7261
|
+
if (!fs14.existsSync(destDir)) {
|
|
7262
|
+
fs14.mkdirSync(destDir, { recursive: true });
|
|
7263
|
+
}
|
|
7264
|
+
fs14.copyFileSync(srcPath, destPath);
|
|
6174
7265
|
console.log(`[WorktreeManager] EP964: Copied ${file} to ${moduleUid} (deprecated)`);
|
|
6175
7266
|
} else {
|
|
6176
7267
|
console.log(`[WorktreeManager] EP964: Skipped ${file} (not found in main)`);
|
|
@@ -6201,8 +7292,8 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6201
7292
|
console.log(`[WorktreeManager] EP959: Timeout: ${TIMEOUT_MINUTES} minutes`);
|
|
6202
7293
|
console.log(`[WorktreeManager] EP959: Script: ${scriptPreview}`);
|
|
6203
7294
|
try {
|
|
6204
|
-
const { execSync:
|
|
6205
|
-
|
|
7295
|
+
const { execSync: execSync8 } = require("child_process");
|
|
7296
|
+
execSync8(script, {
|
|
6206
7297
|
cwd: worktree.worktreePath,
|
|
6207
7298
|
stdio: "inherit",
|
|
6208
7299
|
timeout: TIMEOUT_MINUTES * 60 * 1e3,
|
|
@@ -6236,8 +7327,8 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6236
7327
|
console.log(`[WorktreeManager] EP959: Timeout: ${TIMEOUT_MINUTES} minutes`);
|
|
6237
7328
|
console.log(`[WorktreeManager] EP959: Script: ${scriptPreview}`);
|
|
6238
7329
|
try {
|
|
6239
|
-
const { execSync:
|
|
6240
|
-
|
|
7330
|
+
const { execSync: execSync8 } = require("child_process");
|
|
7331
|
+
execSync8(script, {
|
|
6241
7332
|
cwd: worktree.worktreePath,
|
|
6242
7333
|
stdio: "inherit",
|
|
6243
7334
|
timeout: TIMEOUT_MINUTES * 60 * 1e3,
|
|
@@ -6253,27 +7344,27 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6253
7344
|
}
|
|
6254
7345
|
};
|
|
6255
7346
|
function getEpisodaRoot() {
|
|
6256
|
-
return process.env.EPISODA_ROOT ||
|
|
7347
|
+
return process.env.EPISODA_ROOT || path15.join(require("os").homedir(), "episoda");
|
|
6257
7348
|
}
|
|
6258
7349
|
async function isWorktreeProject(projectRoot) {
|
|
6259
7350
|
const manager = new WorktreeManager(projectRoot);
|
|
6260
7351
|
return manager.initialize();
|
|
6261
7352
|
}
|
|
6262
7353
|
async function findProjectRoot(startPath) {
|
|
6263
|
-
let current =
|
|
7354
|
+
let current = path15.resolve(startPath);
|
|
6264
7355
|
const episodaRoot = getEpisodaRoot();
|
|
6265
7356
|
if (!current.startsWith(episodaRoot)) {
|
|
6266
7357
|
return null;
|
|
6267
7358
|
}
|
|
6268
7359
|
for (let i = 0; i < 10; i++) {
|
|
6269
|
-
const bareDir =
|
|
6270
|
-
const episodaDir =
|
|
6271
|
-
if (
|
|
7360
|
+
const bareDir = path15.join(current, ".bare");
|
|
7361
|
+
const episodaDir = path15.join(current, ".episoda");
|
|
7362
|
+
if (fs14.existsSync(bareDir) && fs14.existsSync(episodaDir)) {
|
|
6272
7363
|
if (await isWorktreeProject(current)) {
|
|
6273
7364
|
return current;
|
|
6274
7365
|
}
|
|
6275
7366
|
}
|
|
6276
|
-
const parent =
|
|
7367
|
+
const parent = path15.dirname(current);
|
|
6277
7368
|
if (parent === current) {
|
|
6278
7369
|
break;
|
|
6279
7370
|
}
|
|
@@ -6283,24 +7374,24 @@ async function findProjectRoot(startPath) {
|
|
|
6283
7374
|
}
|
|
6284
7375
|
|
|
6285
7376
|
// src/utils/worktree.ts
|
|
6286
|
-
var
|
|
6287
|
-
var
|
|
7377
|
+
var path16 = __toESM(require("path"));
|
|
7378
|
+
var fs15 = __toESM(require("fs"));
|
|
6288
7379
|
var os5 = __toESM(require("os"));
|
|
6289
|
-
var
|
|
7380
|
+
var import_core10 = __toESM(require_dist());
|
|
6290
7381
|
function getEpisodaRoot2() {
|
|
6291
|
-
return process.env.EPISODA_ROOT ||
|
|
7382
|
+
return process.env.EPISODA_ROOT || path16.join(os5.homedir(), "episoda");
|
|
6292
7383
|
}
|
|
6293
7384
|
function getWorktreeInfo(moduleUid, workspaceSlug, projectSlug) {
|
|
6294
7385
|
const root = getEpisodaRoot2();
|
|
6295
|
-
const worktreePath =
|
|
7386
|
+
const worktreePath = path16.join(root, workspaceSlug, projectSlug, moduleUid);
|
|
6296
7387
|
return {
|
|
6297
7388
|
path: worktreePath,
|
|
6298
|
-
exists:
|
|
7389
|
+
exists: fs15.existsSync(worktreePath),
|
|
6299
7390
|
moduleUid
|
|
6300
7391
|
};
|
|
6301
7392
|
}
|
|
6302
7393
|
async function getWorktreeInfoForModule(moduleUid) {
|
|
6303
|
-
const config = await (0,
|
|
7394
|
+
const config = await (0, import_core10.loadConfig)();
|
|
6304
7395
|
if (!config?.workspace_slug || !config?.project_slug) {
|
|
6305
7396
|
console.warn("[Worktree] Missing workspace_slug or project_slug in config");
|
|
6306
7397
|
return null;
|
|
@@ -6319,61 +7410,61 @@ function clearAllPorts() {
|
|
|
6319
7410
|
}
|
|
6320
7411
|
|
|
6321
7412
|
// src/framework-detector.ts
|
|
6322
|
-
var
|
|
6323
|
-
var
|
|
7413
|
+
var fs16 = __toESM(require("fs"));
|
|
7414
|
+
var path17 = __toESM(require("path"));
|
|
6324
7415
|
function getInstallCommand(cwd) {
|
|
6325
|
-
if (
|
|
7416
|
+
if (fs16.existsSync(path17.join(cwd, "bun.lockb"))) {
|
|
6326
7417
|
return {
|
|
6327
7418
|
command: ["bun", "install"],
|
|
6328
7419
|
description: "Installing dependencies with bun",
|
|
6329
7420
|
detectedFrom: "bun.lockb"
|
|
6330
7421
|
};
|
|
6331
7422
|
}
|
|
6332
|
-
if (
|
|
7423
|
+
if (fs16.existsSync(path17.join(cwd, "pnpm-lock.yaml"))) {
|
|
6333
7424
|
return {
|
|
6334
7425
|
command: ["pnpm", "install"],
|
|
6335
7426
|
description: "Installing dependencies with pnpm",
|
|
6336
7427
|
detectedFrom: "pnpm-lock.yaml"
|
|
6337
7428
|
};
|
|
6338
7429
|
}
|
|
6339
|
-
if (
|
|
7430
|
+
if (fs16.existsSync(path17.join(cwd, "yarn.lock"))) {
|
|
6340
7431
|
return {
|
|
6341
7432
|
command: ["yarn", "install"],
|
|
6342
7433
|
description: "Installing dependencies with yarn",
|
|
6343
7434
|
detectedFrom: "yarn.lock"
|
|
6344
7435
|
};
|
|
6345
7436
|
}
|
|
6346
|
-
if (
|
|
7437
|
+
if (fs16.existsSync(path17.join(cwd, "package-lock.json"))) {
|
|
6347
7438
|
return {
|
|
6348
7439
|
command: ["npm", "ci"],
|
|
6349
7440
|
description: "Installing dependencies with npm ci",
|
|
6350
7441
|
detectedFrom: "package-lock.json"
|
|
6351
7442
|
};
|
|
6352
7443
|
}
|
|
6353
|
-
if (
|
|
7444
|
+
if (fs16.existsSync(path17.join(cwd, "package.json"))) {
|
|
6354
7445
|
return {
|
|
6355
7446
|
command: ["npm", "install"],
|
|
6356
7447
|
description: "Installing dependencies with npm",
|
|
6357
7448
|
detectedFrom: "package.json"
|
|
6358
7449
|
};
|
|
6359
7450
|
}
|
|
6360
|
-
if (
|
|
7451
|
+
if (fs16.existsSync(path17.join(cwd, "Pipfile.lock")) || fs16.existsSync(path17.join(cwd, "Pipfile"))) {
|
|
6361
7452
|
return {
|
|
6362
7453
|
command: ["pipenv", "install"],
|
|
6363
7454
|
description: "Installing dependencies with pipenv",
|
|
6364
|
-
detectedFrom:
|
|
7455
|
+
detectedFrom: fs16.existsSync(path17.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
|
|
6365
7456
|
};
|
|
6366
7457
|
}
|
|
6367
|
-
if (
|
|
7458
|
+
if (fs16.existsSync(path17.join(cwd, "poetry.lock"))) {
|
|
6368
7459
|
return {
|
|
6369
7460
|
command: ["poetry", "install"],
|
|
6370
7461
|
description: "Installing dependencies with poetry",
|
|
6371
7462
|
detectedFrom: "poetry.lock"
|
|
6372
7463
|
};
|
|
6373
7464
|
}
|
|
6374
|
-
if (
|
|
6375
|
-
const pyprojectPath =
|
|
6376
|
-
const content =
|
|
7465
|
+
if (fs16.existsSync(path17.join(cwd, "pyproject.toml"))) {
|
|
7466
|
+
const pyprojectPath = path17.join(cwd, "pyproject.toml");
|
|
7467
|
+
const content = fs16.readFileSync(pyprojectPath, "utf-8");
|
|
6377
7468
|
if (content.includes("[tool.poetry]")) {
|
|
6378
7469
|
return {
|
|
6379
7470
|
command: ["poetry", "install"],
|
|
@@ -6382,41 +7473,41 @@ function getInstallCommand(cwd) {
|
|
|
6382
7473
|
};
|
|
6383
7474
|
}
|
|
6384
7475
|
}
|
|
6385
|
-
if (
|
|
7476
|
+
if (fs16.existsSync(path17.join(cwd, "requirements.txt"))) {
|
|
6386
7477
|
return {
|
|
6387
7478
|
command: ["pip", "install", "-r", "requirements.txt"],
|
|
6388
7479
|
description: "Installing dependencies with pip",
|
|
6389
7480
|
detectedFrom: "requirements.txt"
|
|
6390
7481
|
};
|
|
6391
7482
|
}
|
|
6392
|
-
if (
|
|
7483
|
+
if (fs16.existsSync(path17.join(cwd, "Gemfile.lock")) || fs16.existsSync(path17.join(cwd, "Gemfile"))) {
|
|
6393
7484
|
return {
|
|
6394
7485
|
command: ["bundle", "install"],
|
|
6395
7486
|
description: "Installing dependencies with bundler",
|
|
6396
|
-
detectedFrom:
|
|
7487
|
+
detectedFrom: fs16.existsSync(path17.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
|
|
6397
7488
|
};
|
|
6398
7489
|
}
|
|
6399
|
-
if (
|
|
7490
|
+
if (fs16.existsSync(path17.join(cwd, "go.sum")) || fs16.existsSync(path17.join(cwd, "go.mod"))) {
|
|
6400
7491
|
return {
|
|
6401
7492
|
command: ["go", "mod", "download"],
|
|
6402
7493
|
description: "Downloading Go modules",
|
|
6403
|
-
detectedFrom:
|
|
7494
|
+
detectedFrom: fs16.existsSync(path17.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
|
|
6404
7495
|
};
|
|
6405
7496
|
}
|
|
6406
|
-
if (
|
|
7497
|
+
if (fs16.existsSync(path17.join(cwd, "Cargo.lock")) || fs16.existsSync(path17.join(cwd, "Cargo.toml"))) {
|
|
6407
7498
|
return {
|
|
6408
7499
|
command: ["cargo", "build"],
|
|
6409
7500
|
description: "Building Rust project (downloads dependencies)",
|
|
6410
|
-
detectedFrom:
|
|
7501
|
+
detectedFrom: fs16.existsSync(path17.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
|
|
6411
7502
|
};
|
|
6412
7503
|
}
|
|
6413
7504
|
return null;
|
|
6414
7505
|
}
|
|
6415
7506
|
|
|
6416
7507
|
// src/daemon/daemon-process.ts
|
|
6417
|
-
var
|
|
7508
|
+
var fs17 = __toESM(require("fs"));
|
|
6418
7509
|
var os6 = __toESM(require("os"));
|
|
6419
|
-
var
|
|
7510
|
+
var path18 = __toESM(require("path"));
|
|
6420
7511
|
var packageJson = require_package();
|
|
6421
7512
|
async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
|
|
6422
7513
|
const now = Date.now();
|
|
@@ -6451,7 +7542,7 @@ async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
|
|
|
6451
7542
|
refresh_token: tokenResponse.refresh_token || config.refresh_token,
|
|
6452
7543
|
expires_at: now + tokenResponse.expires_in * 1e3
|
|
6453
7544
|
};
|
|
6454
|
-
await (0,
|
|
7545
|
+
await (0, import_core11.saveConfig)(updatedConfig);
|
|
6455
7546
|
console.log("[Daemon] EP904: Access token refreshed successfully");
|
|
6456
7547
|
return updatedConfig;
|
|
6457
7548
|
} catch (error) {
|
|
@@ -6460,7 +7551,7 @@ async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
|
|
|
6460
7551
|
}
|
|
6461
7552
|
}
|
|
6462
7553
|
async function fetchWithAuth(url, options = {}, retryOnUnauthorized = true) {
|
|
6463
|
-
let config = await (0,
|
|
7554
|
+
let config = await (0, import_core11.loadConfig)();
|
|
6464
7555
|
if (!config?.access_token) {
|
|
6465
7556
|
throw new Error("No access token configured");
|
|
6466
7557
|
}
|
|
@@ -6487,7 +7578,7 @@ async function fetchWithAuth(url, options = {}, retryOnUnauthorized = true) {
|
|
|
6487
7578
|
}
|
|
6488
7579
|
async function fetchEnvVars2() {
|
|
6489
7580
|
try {
|
|
6490
|
-
const config = await (0,
|
|
7581
|
+
const config = await (0, import_core11.loadConfig)();
|
|
6491
7582
|
if (!config?.project_id) {
|
|
6492
7583
|
console.warn("[Daemon] EP973: No project_id in config, cannot fetch env vars");
|
|
6493
7584
|
return {};
|
|
@@ -6572,7 +7663,7 @@ var Daemon = class _Daemon {
|
|
|
6572
7663
|
console.log("[Daemon] Starting Episoda daemon...");
|
|
6573
7664
|
this.machineId = await getMachineId();
|
|
6574
7665
|
console.log(`[Daemon] Machine ID: ${this.machineId}`);
|
|
6575
|
-
const config = await (0,
|
|
7666
|
+
const config = await (0, import_core11.loadConfig)();
|
|
6576
7667
|
if (config?.device_id) {
|
|
6577
7668
|
this.deviceId = config.device_id;
|
|
6578
7669
|
console.log(`[Daemon] Loaded cached Device ID (UUID): ${this.deviceId}`);
|
|
@@ -6709,7 +7800,7 @@ var Daemon = class _Daemon {
|
|
|
6709
7800
|
};
|
|
6710
7801
|
});
|
|
6711
7802
|
this.ipcServer.on("verify-server-connection", async () => {
|
|
6712
|
-
const config = await (0,
|
|
7803
|
+
const config = await (0, import_core11.loadConfig)();
|
|
6713
7804
|
if (!config?.access_token || !config?.api_url) {
|
|
6714
7805
|
return {
|
|
6715
7806
|
verified: false,
|
|
@@ -6883,7 +7974,7 @@ var Daemon = class _Daemon {
|
|
|
6883
7974
|
console.warn(`[Daemon] Stale connection detected for ${projectPath}, forcing reconnection`);
|
|
6884
7975
|
await this.disconnectProject(projectPath);
|
|
6885
7976
|
}
|
|
6886
|
-
const config = await (0,
|
|
7977
|
+
const config = await (0, import_core11.loadConfig)();
|
|
6887
7978
|
if (!config || !config.access_token) {
|
|
6888
7979
|
throw new Error("No access token found. Please run: episoda auth");
|
|
6889
7980
|
}
|
|
@@ -6904,8 +7995,8 @@ var Daemon = class _Daemon {
|
|
|
6904
7995
|
wsUrl = `${wsProtocol}//${wsHostname}:${wsPort}`;
|
|
6905
7996
|
}
|
|
6906
7997
|
console.log(`[Daemon] Connecting to ${wsUrl} for project ${projectId}...`);
|
|
6907
|
-
const client = new
|
|
6908
|
-
const gitExecutor = new
|
|
7998
|
+
const client = new import_core11.EpisodaClient();
|
|
7999
|
+
const gitExecutor = new import_core11.GitExecutor();
|
|
6909
8000
|
const connection = {
|
|
6910
8001
|
projectId,
|
|
6911
8002
|
projectPath,
|
|
@@ -6920,7 +8011,7 @@ var Daemon = class _Daemon {
|
|
|
6920
8011
|
client.updateActivity();
|
|
6921
8012
|
try {
|
|
6922
8013
|
const gitCmd = message.command;
|
|
6923
|
-
const bareRepoPath =
|
|
8014
|
+
const bareRepoPath = path18.join(projectPath, ".bare");
|
|
6924
8015
|
const cwd = gitCmd.worktreePath || bareRepoPath;
|
|
6925
8016
|
if (gitCmd.worktreePath) {
|
|
6926
8017
|
console.log(`[Daemon] Routing command to worktree: ${gitCmd.worktreePath}`);
|
|
@@ -7048,15 +8139,15 @@ var Daemon = class _Daemon {
|
|
|
7048
8139
|
client.on("tunnel_command", async (message) => {
|
|
7049
8140
|
if (message.type === "tunnel_command" && message.command) {
|
|
7050
8141
|
const cmd = message.command;
|
|
7051
|
-
console.log(`[Daemon] Received tunnel command for ${projectId}:`, cmd.action);
|
|
8142
|
+
console.log(`[Daemon] EP1024: Received tunnel command for ${projectId}:`, cmd.action);
|
|
7052
8143
|
client.updateActivity();
|
|
7053
8144
|
try {
|
|
7054
|
-
const
|
|
8145
|
+
const previewManager = getPreviewManager();
|
|
7055
8146
|
let result;
|
|
7056
8147
|
if (cmd.action === "start") {
|
|
7057
8148
|
const worktree = await getWorktreeInfoForModule(cmd.moduleUid);
|
|
7058
8149
|
if (!worktree) {
|
|
7059
|
-
console.error(`[Daemon]
|
|
8150
|
+
console.error(`[Daemon] EP1024: Cannot resolve worktree path for ${cmd.moduleUid}`);
|
|
7060
8151
|
await client.send({
|
|
7061
8152
|
type: "tunnel_result",
|
|
7062
8153
|
commandId: message.id,
|
|
@@ -7065,7 +8156,7 @@ var Daemon = class _Daemon {
|
|
|
7065
8156
|
return;
|
|
7066
8157
|
}
|
|
7067
8158
|
if (!worktree.exists) {
|
|
7068
|
-
console.error(`[Daemon]
|
|
8159
|
+
console.error(`[Daemon] EP1024: Worktree not found at ${worktree.path}`);
|
|
7069
8160
|
await client.send({
|
|
7070
8161
|
type: "tunnel_result",
|
|
7071
8162
|
commandId: message.id,
|
|
@@ -7073,118 +8164,31 @@ var Daemon = class _Daemon {
|
|
|
7073
8164
|
});
|
|
7074
8165
|
return;
|
|
7075
8166
|
}
|
|
7076
|
-
console.log(`[Daemon]
|
|
8167
|
+
console.log(`[Daemon] EP1024: Using worktree path ${worktree.path} for ${cmd.moduleUid}`);
|
|
7077
8168
|
const port = cmd.port || detectDevPort(worktree.path);
|
|
7078
|
-
const
|
|
7079
|
-
const
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
console.warn(`[Daemon] Error reporting tunnel status:`, reportError);
|
|
7099
|
-
}
|
|
7100
|
-
}
|
|
7101
|
-
};
|
|
7102
|
-
(async () => {
|
|
7103
|
-
const MAX_RETRIES = 3;
|
|
7104
|
-
const RETRY_DELAY_MS = 3e3;
|
|
7105
|
-
await reportTunnelStatus({
|
|
7106
|
-
tunnel_started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7107
|
-
tunnel_error: null
|
|
7108
|
-
// Clear any previous error
|
|
7109
|
-
});
|
|
7110
|
-
try {
|
|
7111
|
-
await tunnelManager.initialize();
|
|
7112
|
-
const devConfig = await (0, import_core10.loadConfig)();
|
|
7113
|
-
const devServerScript = devConfig?.project_settings?.worktree_dev_server_script;
|
|
7114
|
-
console.log(`[Daemon] EP973: Ensuring dev server is running in ${worktree.path} on port ${port}...`);
|
|
7115
|
-
const devServerResult = await ensureDevServer(worktree.path, port, cmd.moduleUid, devServerScript);
|
|
7116
|
-
if (!devServerResult.success) {
|
|
7117
|
-
const errorMsg2 = `Dev server failed to start: ${devServerResult.error}`;
|
|
7118
|
-
console.error(`[Daemon] ${errorMsg2}`);
|
|
7119
|
-
await reportTunnelStatus({ tunnel_error: errorMsg2 });
|
|
7120
|
-
return;
|
|
7121
|
-
}
|
|
7122
|
-
console.log(`[Daemon] Dev server ready on port ${port}`);
|
|
7123
|
-
let lastError;
|
|
7124
|
-
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
7125
|
-
console.log(`[Daemon] Starting tunnel (attempt ${attempt}/${MAX_RETRIES})...`);
|
|
7126
|
-
const startResult = await tunnelManager.startTunnel({
|
|
7127
|
-
moduleUid: cmd.moduleUid,
|
|
7128
|
-
port,
|
|
7129
|
-
onUrl: async (url) => {
|
|
7130
|
-
console.log(`[Daemon] Tunnel URL for ${cmd.moduleUid}: ${url}`);
|
|
7131
|
-
await reportTunnelStatus({
|
|
7132
|
-
tunnel_url: url,
|
|
7133
|
-
tunnel_error: null
|
|
7134
|
-
// Clear error on success
|
|
7135
|
-
});
|
|
7136
|
-
},
|
|
7137
|
-
onStatusChange: (status, error) => {
|
|
7138
|
-
if (status === "error") {
|
|
7139
|
-
console.error(`[Daemon] Tunnel error for ${cmd.moduleUid}: ${error}`);
|
|
7140
|
-
reportTunnelStatus({ tunnel_error: error || "Tunnel connection error" });
|
|
7141
|
-
} else if (status === "reconnecting") {
|
|
7142
|
-
console.log(`[Daemon] Tunnel reconnecting for ${cmd.moduleUid}...`);
|
|
7143
|
-
}
|
|
7144
|
-
}
|
|
7145
|
-
});
|
|
7146
|
-
if (startResult.success) {
|
|
7147
|
-
console.log(`[Daemon] Tunnel started successfully for ${cmd.moduleUid}`);
|
|
7148
|
-
return;
|
|
7149
|
-
}
|
|
7150
|
-
lastError = startResult.error;
|
|
7151
|
-
console.warn(`[Daemon] Tunnel start attempt ${attempt} failed: ${lastError}`);
|
|
7152
|
-
if (attempt < MAX_RETRIES) {
|
|
7153
|
-
console.log(`[Daemon] Retrying in ${RETRY_DELAY_MS}ms...`);
|
|
7154
|
-
await new Promise((resolve3) => setTimeout(resolve3, RETRY_DELAY_MS));
|
|
7155
|
-
}
|
|
7156
|
-
}
|
|
7157
|
-
const errorMsg = `Tunnel failed after ${MAX_RETRIES} attempts: ${lastError}`;
|
|
7158
|
-
console.error(`[Daemon] ${errorMsg}`);
|
|
7159
|
-
await reportTunnelStatus({ tunnel_error: errorMsg });
|
|
7160
|
-
} catch (error) {
|
|
7161
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
7162
|
-
console.error(`[Daemon] Async tunnel startup error:`, error);
|
|
7163
|
-
await reportTunnelStatus({ tunnel_error: `Unexpected error: ${errorMsg}` });
|
|
7164
|
-
}
|
|
7165
|
-
})();
|
|
7166
|
-
result = {
|
|
7167
|
-
success: true,
|
|
7168
|
-
previewUrl
|
|
7169
|
-
// Note: actual tunnel URL will be reported via API when ready
|
|
7170
|
-
};
|
|
7171
|
-
} else if (cmd.action === "stop") {
|
|
7172
|
-
await tunnelManager.stopTunnel(cmd.moduleUid);
|
|
7173
|
-
await stopDevServer(cmd.moduleUid);
|
|
7174
|
-
const config2 = await (0, import_core10.loadConfig)();
|
|
7175
|
-
if (config2?.access_token) {
|
|
7176
|
-
try {
|
|
7177
|
-
const apiUrl = config2.api_url || "https://episoda.dev";
|
|
7178
|
-
await fetch(`${apiUrl}/api/modules/${cmd.moduleUid}/tunnel`, {
|
|
7179
|
-
method: "DELETE",
|
|
7180
|
-
headers: {
|
|
7181
|
-
"Authorization": `Bearer ${config2.access_token}`
|
|
7182
|
-
}
|
|
7183
|
-
});
|
|
7184
|
-
console.log(`[Daemon] Tunnel URL cleared for ${cmd.moduleUid}`);
|
|
7185
|
-
} catch {
|
|
7186
|
-
}
|
|
8169
|
+
const devConfig = await (0, import_core11.loadConfig)();
|
|
8170
|
+
const customCommand = devConfig?.project_settings?.worktree_dev_server_script;
|
|
8171
|
+
const startResult = await previewManager.startPreview({
|
|
8172
|
+
moduleUid: cmd.moduleUid,
|
|
8173
|
+
worktreePath: worktree.path,
|
|
8174
|
+
port,
|
|
8175
|
+
customCommand
|
|
8176
|
+
});
|
|
8177
|
+
if (startResult.success) {
|
|
8178
|
+
console.log(`[Daemon] EP1024: Preview started for ${cmd.moduleUid}: ${startResult.previewUrl}`);
|
|
8179
|
+
result = {
|
|
8180
|
+
success: true,
|
|
8181
|
+
previewUrl: startResult.previewUrl
|
|
8182
|
+
};
|
|
8183
|
+
} else {
|
|
8184
|
+
console.error(`[Daemon] EP1024: Preview failed for ${cmd.moduleUid}: ${startResult.error}`);
|
|
8185
|
+
result = {
|
|
8186
|
+
success: false,
|
|
8187
|
+
error: startResult.error || "Failed to start preview"
|
|
8188
|
+
};
|
|
7187
8189
|
}
|
|
8190
|
+
} else if (cmd.action === "stop") {
|
|
8191
|
+
await previewManager.stopPreview(cmd.moduleUid);
|
|
7188
8192
|
result = { success: true };
|
|
7189
8193
|
} else {
|
|
7190
8194
|
result = {
|
|
@@ -7197,7 +8201,7 @@ var Daemon = class _Daemon {
|
|
|
7197
8201
|
commandId: message.id,
|
|
7198
8202
|
result
|
|
7199
8203
|
});
|
|
7200
|
-
console.log(`[Daemon] Tunnel command ${cmd.action} completed for ${cmd.moduleUid}:`, result.success ? "success" : "failed");
|
|
8204
|
+
console.log(`[Daemon] EP1024: Tunnel command ${cmd.action} completed for ${cmd.moduleUid}:`, result.success ? "success" : "failed");
|
|
7201
8205
|
} catch (error) {
|
|
7202
8206
|
await client.send({
|
|
7203
8207
|
type: "tunnel_result",
|
|
@@ -7207,7 +8211,7 @@ var Daemon = class _Daemon {
|
|
|
7207
8211
|
error: error instanceof Error ? error.message : String(error)
|
|
7208
8212
|
}
|
|
7209
8213
|
});
|
|
7210
|
-
console.error(`[Daemon] Tunnel command execution error:`, error);
|
|
8214
|
+
console.error(`[Daemon] EP1024: Tunnel command execution error:`, error);
|
|
7211
8215
|
}
|
|
7212
8216
|
}
|
|
7213
8217
|
});
|
|
@@ -7422,8 +8426,8 @@ var Daemon = class _Daemon {
|
|
|
7422
8426
|
let daemonPid;
|
|
7423
8427
|
try {
|
|
7424
8428
|
const pidPath = getPidFilePath();
|
|
7425
|
-
if (
|
|
7426
|
-
const pidStr =
|
|
8429
|
+
if (fs17.existsSync(pidPath)) {
|
|
8430
|
+
const pidStr = fs17.readFileSync(pidPath, "utf-8").trim();
|
|
7427
8431
|
daemonPid = parseInt(pidStr, 10);
|
|
7428
8432
|
}
|
|
7429
8433
|
} catch (pidError) {
|
|
@@ -7502,29 +8506,29 @@ var Daemon = class _Daemon {
|
|
|
7502
8506
|
*/
|
|
7503
8507
|
async configureGitUser(projectPath, userId, workspaceId, machineId, projectId, deviceId) {
|
|
7504
8508
|
try {
|
|
7505
|
-
const { execSync:
|
|
7506
|
-
|
|
8509
|
+
const { execSync: execSync8 } = await import("child_process");
|
|
8510
|
+
execSync8(`git config episoda.userId ${userId}`, {
|
|
7507
8511
|
cwd: projectPath,
|
|
7508
8512
|
encoding: "utf8",
|
|
7509
8513
|
stdio: "pipe"
|
|
7510
8514
|
});
|
|
7511
|
-
|
|
8515
|
+
execSync8(`git config episoda.workspaceId ${workspaceId}`, {
|
|
7512
8516
|
cwd: projectPath,
|
|
7513
8517
|
encoding: "utf8",
|
|
7514
8518
|
stdio: "pipe"
|
|
7515
8519
|
});
|
|
7516
|
-
|
|
8520
|
+
execSync8(`git config episoda.machineId ${machineId}`, {
|
|
7517
8521
|
cwd: projectPath,
|
|
7518
8522
|
encoding: "utf8",
|
|
7519
8523
|
stdio: "pipe"
|
|
7520
8524
|
});
|
|
7521
|
-
|
|
8525
|
+
execSync8(`git config episoda.projectId ${projectId}`, {
|
|
7522
8526
|
cwd: projectPath,
|
|
7523
8527
|
encoding: "utf8",
|
|
7524
8528
|
stdio: "pipe"
|
|
7525
8529
|
});
|
|
7526
8530
|
if (deviceId) {
|
|
7527
|
-
|
|
8531
|
+
execSync8(`git config episoda.deviceId ${deviceId}`, {
|
|
7528
8532
|
cwd: projectPath,
|
|
7529
8533
|
encoding: "utf8",
|
|
7530
8534
|
stdio: "pipe"
|
|
@@ -7544,27 +8548,27 @@ var Daemon = class _Daemon {
|
|
|
7544
8548
|
*/
|
|
7545
8549
|
async installGitHooks(projectPath) {
|
|
7546
8550
|
const hooks = ["post-checkout", "pre-commit", "post-commit"];
|
|
7547
|
-
const hooksDir =
|
|
7548
|
-
if (!
|
|
8551
|
+
const hooksDir = path18.join(projectPath, ".git", "hooks");
|
|
8552
|
+
if (!fs17.existsSync(hooksDir)) {
|
|
7549
8553
|
console.warn(`[Daemon] Hooks directory not found: ${hooksDir}`);
|
|
7550
8554
|
return;
|
|
7551
8555
|
}
|
|
7552
8556
|
for (const hookName of hooks) {
|
|
7553
8557
|
try {
|
|
7554
|
-
const hookPath =
|
|
7555
|
-
const bundledHookPath =
|
|
7556
|
-
if (!
|
|
8558
|
+
const hookPath = path18.join(hooksDir, hookName);
|
|
8559
|
+
const bundledHookPath = path18.join(__dirname, "..", "hooks", hookName);
|
|
8560
|
+
if (!fs17.existsSync(bundledHookPath)) {
|
|
7557
8561
|
console.warn(`[Daemon] Bundled hook not found: ${bundledHookPath}`);
|
|
7558
8562
|
continue;
|
|
7559
8563
|
}
|
|
7560
|
-
const hookContent =
|
|
7561
|
-
if (
|
|
7562
|
-
const existingContent =
|
|
8564
|
+
const hookContent = fs17.readFileSync(bundledHookPath, "utf-8");
|
|
8565
|
+
if (fs17.existsSync(hookPath)) {
|
|
8566
|
+
const existingContent = fs17.readFileSync(hookPath, "utf-8");
|
|
7563
8567
|
if (existingContent === hookContent) {
|
|
7564
8568
|
continue;
|
|
7565
8569
|
}
|
|
7566
8570
|
}
|
|
7567
|
-
|
|
8571
|
+
fs17.writeFileSync(hookPath, hookContent, { mode: 493 });
|
|
7568
8572
|
console.log(`[Daemon] Installed git hook: ${hookName}`);
|
|
7569
8573
|
} catch (error) {
|
|
7570
8574
|
console.warn(`[Daemon] Failed to install ${hookName} hook:`, error instanceof Error ? error.message : error);
|
|
@@ -7579,7 +8583,7 @@ var Daemon = class _Daemon {
|
|
|
7579
8583
|
*/
|
|
7580
8584
|
async cacheDeviceId(deviceId) {
|
|
7581
8585
|
try {
|
|
7582
|
-
const config = await (0,
|
|
8586
|
+
const config = await (0, import_core11.loadConfig)();
|
|
7583
8587
|
if (!config) {
|
|
7584
8588
|
console.warn("[Daemon] Cannot cache device ID - no config found");
|
|
7585
8589
|
return;
|
|
@@ -7592,7 +8596,7 @@ var Daemon = class _Daemon {
|
|
|
7592
8596
|
device_id: deviceId,
|
|
7593
8597
|
machine_id: this.machineId
|
|
7594
8598
|
};
|
|
7595
|
-
await (0,
|
|
8599
|
+
await (0, import_core11.saveConfig)(updatedConfig);
|
|
7596
8600
|
console.log(`[Daemon] Cached device ID to config: ${deviceId}`);
|
|
7597
8601
|
} catch (error) {
|
|
7598
8602
|
console.warn("[Daemon] Failed to cache device ID:", error instanceof Error ? error.message : error);
|
|
@@ -7606,7 +8610,7 @@ var Daemon = class _Daemon {
|
|
|
7606
8610
|
*/
|
|
7607
8611
|
async syncProjectSettings(projectId) {
|
|
7608
8612
|
try {
|
|
7609
|
-
const config = await (0,
|
|
8613
|
+
const config = await (0, import_core11.loadConfig)();
|
|
7610
8614
|
if (!config) return;
|
|
7611
8615
|
const apiUrl = config.api_url || "https://episoda.dev";
|
|
7612
8616
|
const response = await fetchWithAuth(`${apiUrl}/api/projects/${projectId}/settings`);
|
|
@@ -7640,7 +8644,7 @@ var Daemon = class _Daemon {
|
|
|
7640
8644
|
cached_at: Date.now()
|
|
7641
8645
|
}
|
|
7642
8646
|
};
|
|
7643
|
-
await (0,
|
|
8647
|
+
await (0, import_core11.saveConfig)(updatedConfig);
|
|
7644
8648
|
console.log(`[Daemon] EP973: Project settings synced (slugs: ${projectSlug}/${workspaceSlug})`);
|
|
7645
8649
|
}
|
|
7646
8650
|
} catch (error) {
|
|
@@ -7660,7 +8664,7 @@ var Daemon = class _Daemon {
|
|
|
7660
8664
|
console.warn("[Daemon] EP995: Cannot sync project path - deviceId not available");
|
|
7661
8665
|
return;
|
|
7662
8666
|
}
|
|
7663
|
-
const config = await (0,
|
|
8667
|
+
const config = await (0, import_core11.loadConfig)();
|
|
7664
8668
|
if (!config) return;
|
|
7665
8669
|
const apiUrl = config.api_url || "https://episoda.dev";
|
|
7666
8670
|
const response = await fetchWithAuth(`${apiUrl}/api/account/machines/${this.deviceId}`, {
|
|
@@ -7693,7 +8697,7 @@ var Daemon = class _Daemon {
|
|
|
7693
8697
|
*/
|
|
7694
8698
|
async updateModuleWorktreeStatus(moduleUid, status, worktreePath, errorMessage) {
|
|
7695
8699
|
try {
|
|
7696
|
-
const config = await (0,
|
|
8700
|
+
const config = await (0, import_core11.loadConfig)();
|
|
7697
8701
|
if (!config) return;
|
|
7698
8702
|
const apiUrl = config.api_url || "https://episoda.dev";
|
|
7699
8703
|
const body = {
|
|
@@ -7748,7 +8752,7 @@ var Daemon = class _Daemon {
|
|
|
7748
8752
|
console.log("[Daemon] EP1003: Cannot reconcile - deviceId not available yet");
|
|
7749
8753
|
return;
|
|
7750
8754
|
}
|
|
7751
|
-
const config = await (0,
|
|
8755
|
+
const config = await (0, import_core11.loadConfig)();
|
|
7752
8756
|
if (!config) return;
|
|
7753
8757
|
const apiUrl = config.api_url || "https://episoda.dev";
|
|
7754
8758
|
const controller = new AbortController();
|
|
@@ -7860,7 +8864,7 @@ var Daemon = class _Daemon {
|
|
|
7860
8864
|
console.log(`[Daemon] EP994: No worktree to remove for ${moduleUid}`);
|
|
7861
8865
|
}
|
|
7862
8866
|
try {
|
|
7863
|
-
const cleanupConfig = await (0,
|
|
8867
|
+
const cleanupConfig = await (0, import_core11.loadConfig)();
|
|
7864
8868
|
const cleanupApiUrl = cleanupConfig?.api_url || "https://episoda.dev";
|
|
7865
8869
|
await fetchWithAuth(`${cleanupApiUrl}/api/modules/${moduleUid}`, {
|
|
7866
8870
|
method: "PATCH",
|
|
@@ -7892,7 +8896,7 @@ var Daemon = class _Daemon {
|
|
|
7892
8896
|
try {
|
|
7893
8897
|
const envVars = await fetchEnvVars2();
|
|
7894
8898
|
console.log(`[Daemon] EP1002: Fetched ${Object.keys(envVars).length} env vars for ${moduleUid}`);
|
|
7895
|
-
const config = await (0,
|
|
8899
|
+
const config = await (0, import_core11.loadConfig)();
|
|
7896
8900
|
const setupConfig = config?.project_settings;
|
|
7897
8901
|
await this.runWorktreeSetupSync(
|
|
7898
8902
|
moduleUid,
|
|
@@ -7938,8 +8942,8 @@ var Daemon = class _Daemon {
|
|
|
7938
8942
|
console.log(`[Daemon] EP1002: ${installCmd.description} (detected from ${installCmd.detectedFrom})`);
|
|
7939
8943
|
console.log(`[Daemon] EP1002: Running: ${installCmd.command.join(" ")}`);
|
|
7940
8944
|
try {
|
|
7941
|
-
const { execSync:
|
|
7942
|
-
|
|
8945
|
+
const { execSync: execSync8 } = await import("child_process");
|
|
8946
|
+
execSync8(installCmd.command.join(" "), {
|
|
7943
8947
|
cwd: worktreePath,
|
|
7944
8948
|
stdio: "inherit",
|
|
7945
8949
|
timeout: 10 * 60 * 1e3,
|
|
@@ -7992,8 +8996,8 @@ var Daemon = class _Daemon {
|
|
|
7992
8996
|
console.log(`[Daemon] EP986: ${installCmd.description} (detected from ${installCmd.detectedFrom})`);
|
|
7993
8997
|
console.log(`[Daemon] EP986: Running: ${installCmd.command.join(" ")}`);
|
|
7994
8998
|
try {
|
|
7995
|
-
const { execSync:
|
|
7996
|
-
|
|
8999
|
+
const { execSync: execSync8 } = await import("child_process");
|
|
9000
|
+
execSync8(installCmd.command.join(" "), {
|
|
7997
9001
|
cwd: worktreePath,
|
|
7998
9002
|
stdio: "inherit",
|
|
7999
9003
|
timeout: 10 * 60 * 1e3,
|
|
@@ -8070,7 +9074,7 @@ var Daemon = class _Daemon {
|
|
|
8070
9074
|
}
|
|
8071
9075
|
this.healthCheckInProgress = true;
|
|
8072
9076
|
try {
|
|
8073
|
-
const config = await (0,
|
|
9077
|
+
const config = await (0, import_core11.loadConfig)();
|
|
8074
9078
|
if (config?.access_token) {
|
|
8075
9079
|
await this.performHealthChecks(config);
|
|
8076
9080
|
}
|
|
@@ -8189,7 +9193,7 @@ var Daemon = class _Daemon {
|
|
|
8189
9193
|
*/
|
|
8190
9194
|
async fetchActiveModuleUids(projectId) {
|
|
8191
9195
|
try {
|
|
8192
|
-
const config = await (0,
|
|
9196
|
+
const config = await (0, import_core11.loadConfig)();
|
|
8193
9197
|
if (!config?.access_token || !config?.api_url) {
|
|
8194
9198
|
return null;
|
|
8195
9199
|
}
|
|
@@ -8289,84 +9293,76 @@ var Daemon = class _Daemon {
|
|
|
8289
9293
|
}
|
|
8290
9294
|
/**
|
|
8291
9295
|
* EP833: Restart a failed tunnel
|
|
8292
|
-
*
|
|
9296
|
+
* EP1024: Refactored to use PreviewManager for unified preview lifecycle
|
|
8293
9297
|
*/
|
|
8294
9298
|
async restartTunnel(moduleUid, port) {
|
|
8295
|
-
const
|
|
9299
|
+
const previewManager = getPreviewManager();
|
|
8296
9300
|
try {
|
|
8297
|
-
await
|
|
8298
|
-
const config = await (0, import_core10.loadConfig)();
|
|
9301
|
+
const config = await (0, import_core11.loadConfig)();
|
|
8299
9302
|
if (!config?.access_token) {
|
|
8300
9303
|
console.error(`[Daemon] EP833: No access token for tunnel restart`);
|
|
8301
9304
|
return;
|
|
8302
9305
|
}
|
|
8303
9306
|
const apiUrl = config.api_url || "https://episoda.dev";
|
|
8304
|
-
const
|
|
8305
|
-
if (
|
|
8306
|
-
console.log(`[Daemon]
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
if (moduleResponse.ok) {
|
|
8311
|
-
const moduleData = await moduleResponse.json();
|
|
8312
|
-
projectId = moduleData.moduleRecord?.project_id ?? null;
|
|
8313
|
-
}
|
|
8314
|
-
} catch (e) {
|
|
8315
|
-
console.warn(`[Daemon] EP833: Failed to fetch module details for project lookup`);
|
|
8316
|
-
}
|
|
8317
|
-
const worktree = await getWorktreeInfoForModule(moduleUid);
|
|
8318
|
-
if (!worktree) {
|
|
8319
|
-
console.error(`[Daemon] EP973: Cannot resolve worktree path for ${moduleUid} - missing config slugs`);
|
|
8320
|
-
return;
|
|
8321
|
-
}
|
|
8322
|
-
if (!worktree.exists) {
|
|
8323
|
-
console.error(`[Daemon] EP973: Worktree not found at ${worktree.path}`);
|
|
8324
|
-
return;
|
|
8325
|
-
}
|
|
8326
|
-
const { isPortInUse: isPortInUse2 } = await Promise.resolve().then(() => (init_port_check(), port_check_exports));
|
|
8327
|
-
if (await isPortInUse2(port)) {
|
|
8328
|
-
console.log(`[Daemon] EP932: Port ${port} in use, checking health...`);
|
|
8329
|
-
const healthy = await isDevServerHealthy(port);
|
|
8330
|
-
if (!healthy) {
|
|
8331
|
-
console.log(`[Daemon] EP932: Dev server on port ${port} is not responding, killing process...`);
|
|
8332
|
-
await killProcessOnPort(port);
|
|
8333
|
-
}
|
|
8334
|
-
}
|
|
8335
|
-
const devServerScript = config.project_settings?.worktree_dev_server_script;
|
|
8336
|
-
const startResult2 = await ensureDevServer(worktree.path, port, moduleUid, devServerScript);
|
|
8337
|
-
if (!startResult2.success) {
|
|
8338
|
-
console.error(`[Daemon] EP932: Failed to start dev server: ${startResult2.error}`);
|
|
8339
|
-
return;
|
|
8340
|
-
}
|
|
8341
|
-
}
|
|
8342
|
-
console.log(`[Daemon] EP932: Dev server ready, restarting tunnel for ${moduleUid}...`);
|
|
8343
|
-
const startResult = await tunnelManager.startTunnel({
|
|
8344
|
-
moduleUid,
|
|
8345
|
-
port,
|
|
8346
|
-
onUrl: async (url) => {
|
|
8347
|
-
console.log(`[Daemon] EP833: Tunnel restarted for ${moduleUid}: ${url}`);
|
|
9307
|
+
const status = previewManager.getStatus(moduleUid);
|
|
9308
|
+
if (status) {
|
|
9309
|
+
console.log(`[Daemon] EP1024: Restarting tracked preview for ${moduleUid}...`);
|
|
9310
|
+
const result2 = await previewManager.restartPreview(moduleUid);
|
|
9311
|
+
if (result2.success && result2.previewUrl) {
|
|
9312
|
+
console.log(`[Daemon] EP833: Preview restarted for ${moduleUid}: ${result2.previewUrl}`);
|
|
8348
9313
|
try {
|
|
8349
9314
|
await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}/tunnel`, {
|
|
8350
9315
|
method: "POST",
|
|
8351
9316
|
body: JSON.stringify({
|
|
8352
|
-
tunnel_url:
|
|
9317
|
+
tunnel_url: result2.previewUrl,
|
|
8353
9318
|
tunnel_error: null,
|
|
8354
9319
|
restart_reason: "health_check_failure"
|
|
8355
|
-
// EP1003: Server can track restart causes
|
|
8356
9320
|
})
|
|
8357
9321
|
});
|
|
8358
9322
|
} catch (e) {
|
|
8359
9323
|
console.warn(`[Daemon] EP833: Failed to report restarted tunnel URL`);
|
|
8360
9324
|
}
|
|
9325
|
+
} else {
|
|
9326
|
+
console.error(`[Daemon] EP833: Preview restart failed for ${moduleUid}: ${result2.error}`);
|
|
8361
9327
|
}
|
|
9328
|
+
return;
|
|
9329
|
+
}
|
|
9330
|
+
console.log(`[Daemon] EP1024: No tracked preview for ${moduleUid}, starting fresh...`);
|
|
9331
|
+
const worktree = await getWorktreeInfoForModule(moduleUid);
|
|
9332
|
+
if (!worktree) {
|
|
9333
|
+
console.error(`[Daemon] EP1024: Cannot resolve worktree path for ${moduleUid} - missing config slugs`);
|
|
9334
|
+
return;
|
|
9335
|
+
}
|
|
9336
|
+
if (!worktree.exists) {
|
|
9337
|
+
console.error(`[Daemon] EP1024: Worktree not found at ${worktree.path}`);
|
|
9338
|
+
return;
|
|
9339
|
+
}
|
|
9340
|
+
const devServerScript = config.project_settings?.worktree_dev_server_script;
|
|
9341
|
+
const result = await previewManager.startPreview({
|
|
9342
|
+
moduleUid,
|
|
9343
|
+
worktreePath: worktree.path,
|
|
9344
|
+
port,
|
|
9345
|
+
customCommand: devServerScript
|
|
8362
9346
|
});
|
|
8363
|
-
if (
|
|
8364
|
-
console.log(`[Daemon] EP833:
|
|
9347
|
+
if (result.success && result.previewUrl) {
|
|
9348
|
+
console.log(`[Daemon] EP833: Preview started for ${moduleUid}: ${result.previewUrl}`);
|
|
9349
|
+
try {
|
|
9350
|
+
await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}/tunnel`, {
|
|
9351
|
+
method: "POST",
|
|
9352
|
+
body: JSON.stringify({
|
|
9353
|
+
tunnel_url: result.previewUrl,
|
|
9354
|
+
tunnel_error: null,
|
|
9355
|
+
restart_reason: "health_check_failure"
|
|
9356
|
+
})
|
|
9357
|
+
});
|
|
9358
|
+
} catch (e) {
|
|
9359
|
+
console.warn(`[Daemon] EP833: Failed to report restarted tunnel URL`);
|
|
9360
|
+
}
|
|
8365
9361
|
} else {
|
|
8366
|
-
console.error(`[Daemon] EP833:
|
|
9362
|
+
console.error(`[Daemon] EP833: Preview start failed for ${moduleUid}: ${result.error}`);
|
|
8367
9363
|
}
|
|
8368
9364
|
} catch (error) {
|
|
8369
|
-
console.error(`[Daemon] EP833: Error restarting
|
|
9365
|
+
console.error(`[Daemon] EP833: Error restarting preview for ${moduleUid}:`, error);
|
|
8370
9366
|
}
|
|
8371
9367
|
}
|
|
8372
9368
|
/**
|
|
@@ -8544,8 +9540,8 @@ var Daemon = class _Daemon {
|
|
|
8544
9540
|
await this.shutdown();
|
|
8545
9541
|
try {
|
|
8546
9542
|
const pidPath = getPidFilePath();
|
|
8547
|
-
if (
|
|
8548
|
-
|
|
9543
|
+
if (fs17.existsSync(pidPath)) {
|
|
9544
|
+
fs17.unlinkSync(pidPath);
|
|
8549
9545
|
console.log("[Daemon] PID file cleaned up");
|
|
8550
9546
|
}
|
|
8551
9547
|
} catch (error) {
|