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.
@@ -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 fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
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 fs17.access(rebaseMergePath);
1559
+ await fs18.access(rebaseMergePath);
1567
1560
  inRebase = true;
1568
1561
  } catch {
1569
1562
  try {
1570
- await fs17.access(rebaseApplyPath);
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 fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1617
+ const fs18 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1625
1618
  try {
1626
- await fs17.access(command.path);
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 fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1673
+ const fs18 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1681
1674
  try {
1682
- await fs17.access(command.path);
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 fs17.rm(command.path, { recursive: true, force: true });
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 fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1852
- const path18 = await Promise.resolve().then(() => __importStar(require("path")));
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 fs17.access(command.path);
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 = path18.dirname(command.path);
1856
+ const parentDir = path19.dirname(command.path);
1864
1857
  try {
1865
- await fs17.mkdir(parentDir, { recursive: true });
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 fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1914
- const path18 = await Promise.resolve().then(() => __importStar(require("path")));
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 = path18.join(currentPath, ".bare");
1920
- const episodaDir = path18.join(currentPath, ".episoda");
1912
+ const bareDir = path19.join(currentPath, ".bare");
1913
+ const episodaDir = path19.join(currentPath, ".episoda");
1921
1914
  try {
1922
- await fs17.access(bareDir);
1923
- await fs17.access(episodaDir);
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 = path18.dirname(currentPath);
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 = getConfigDir6;
2510
+ exports2.getConfigDir = getConfigDir7;
2518
2511
  exports2.getConfigPath = getConfigPath;
2519
- exports2.loadConfig = loadConfig6;
2512
+ exports2.loadConfig = loadConfig7;
2520
2513
  exports2.saveConfig = saveConfig2;
2521
2514
  exports2.validateToken = validateToken;
2522
- var fs17 = __importStar(require("fs"));
2523
- var path18 = __importStar(require("path"));
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 getConfigDir6() {
2528
- return process.env.EPISODA_CONFIG_DIR || path18.join(os7.homedir(), ".episoda");
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 path18.join(getConfigDir6(), DEFAULT_CONFIG_FILE);
2527
+ return path19.join(getConfigDir7(), DEFAULT_CONFIG_FILE);
2535
2528
  }
2536
2529
  function ensureConfigDir(configPath) {
2537
- const dir = path18.dirname(configPath);
2538
- const isNew = !fs17.existsSync(dir);
2530
+ const dir = path19.dirname(configPath);
2531
+ const isNew = !fs18.existsSync(dir);
2539
2532
  if (isNew) {
2540
- fs17.mkdirSync(dir, { recursive: true, mode: 448 });
2533
+ fs18.mkdirSync(dir, { recursive: true, mode: 448 });
2541
2534
  }
2542
2535
  if (process.platform === "darwin") {
2543
- const nosyncPath = path18.join(dir, ".nosync");
2544
- if (isNew || !fs17.existsSync(nosyncPath)) {
2536
+ const nosyncPath = path19.join(dir, ".nosync");
2537
+ if (isNew || !fs18.existsSync(nosyncPath)) {
2545
2538
  try {
2546
- fs17.writeFileSync(nosyncPath, "", { mode: 384 });
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 loadConfig6(configPath) {
2549
+ async function loadConfig7(configPath) {
2557
2550
  const fullPath = getConfigPath(configPath);
2558
- if (!fs17.existsSync(fullPath)) {
2551
+ if (!fs18.existsSync(fullPath)) {
2559
2552
  return null;
2560
2553
  }
2561
2554
  try {
2562
- const content = fs17.readFileSync(fullPath, "utf8");
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
- fs17.writeFileSync(fullPath, content, { mode: 384 });
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.40",
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 request = JSON.parse(message);
3060
- const response = await this.handleRequest(request);
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(request) {
3079
- const handler = this.handlers.get(request.command);
3034
+ async handleRequest(request2) {
3035
+ const handler = this.handlers.get(request2.command);
3080
3036
  if (!handler) {
3081
3037
  return {
3082
- id: request.id,
3038
+ id: request2.id,
3083
3039
  success: false,
3084
- error: `Unknown command: ${request.command}`
3040
+ error: `Unknown command: ${request2.command}`
3085
3041
  };
3086
3042
  }
3087
3043
  try {
3088
- const data = await handler(request.params);
3044
+ const data = await handler(request2.params);
3089
3045
  return {
3090
- id: request.id,
3046
+ id: request2.id,
3091
3047
  success: true,
3092
3048
  data
3093
3049
  };
3094
3050
  } catch (error) {
3095
3051
  return {
3096
- id: request.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 import_core10 = __toESM(require_dist());
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 pid = parseInt(fs6.readFileSync(pidPath, "utf8").trim(), 10);
4086
- return isNaN(pid) ? null : pid;
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
- * EP672-9: Internal method to start the tunnel process
4292
- * Separated from startTunnel to support reconnection
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
- }, 3e4);
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
- return this.startTunnelProcess(options);
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/utils/dev-server.ts
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
- init_port_check();
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 import_http = __toESM(require("http"));
5216
- var fs11 = __toESM(require("fs"));
5217
- var path12 = __toESM(require("path"));
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 = path12.join((0, import_core7.getConfigDir)(), "logs");
5226
- if (!fs11.existsSync(logsDir)) {
5227
- fs11.mkdirSync(logsDir, { recursive: true });
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 path12.join(getLogsDir(), `dev-${moduleUid}.log`);
6353
+ return path13.join(getLogsDir(), `dev-${moduleUid}.log`);
5233
6354
  }
5234
6355
  function rotateLogIfNeeded(logPath) {
5235
6356
  try {
5236
- if (fs11.existsSync(logPath)) {
5237
- const stats = fs11.statSync(logPath);
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 (fs11.existsSync(backupPath)) {
5241
- fs11.unlinkSync(backupPath);
6361
+ if (fs12.existsSync(backupPath)) {
6362
+ fs12.unlinkSync(backupPath);
5242
6363
  }
5243
- fs11.renameSync(logPath, backupPath);
5244
- console.log(`[DevServer] EP932: Rotated log file for ${path12.basename(logPath)}`);
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
- fs11.appendFileSync(logPath, logLine);
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, import_child_process9.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
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, import_child_process9.execSync)(`kill -15 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
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, import_child_process9.execSync)(`kill -0 ${pid} 2>/dev/null`, { encoding: "utf8" });
5305
- (0, import_child_process9.execSync)(`kill -9 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
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, import_child_process9.spawn)(cmd, args, {
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, import_core7.loadConfig)();
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 = path12.join(projectPath, ".env");
5461
- if (!fs11.existsSync(envFilePath) && Object.keys(injectedEnvVars).length > 0) {
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 fs12 = __toESM(require("fs"));
5574
- var path13 = __toESM(require("path"));
5575
- var DEFAULT_PORT = 3e3;
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 ${DEFAULT_PORT}`);
5588
- return DEFAULT_PORT;
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
- path13.join(projectPath, ".env"),
5593
- path13.join(projectPath, ".env.local"),
5594
- path13.join(projectPath, ".env.development"),
5595
- path13.join(projectPath, ".env.development.local")
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 (!fs12.existsSync(envPath)) continue;
5600
- const content = fs12.readFileSync(envPath, "utf-8");
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 = path13.join(projectPath, "package.json");
6708
+ const packageJsonPath = path14.join(projectPath, "package.json");
5618
6709
  try {
5619
- if (!fs12.existsSync(packageJsonPath)) return null;
5620
- const content = fs12.readFileSync(packageJsonPath, "utf-8");
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 fs13 = __toESM(require("fs"));
5645
- var path14 = __toESM(require("path"));
5646
- var import_core8 = __toESM(require_dist());
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 = path14.join(projectRoot, ".bare");
5670
- this.configPath = path14.join(projectRoot, ".episoda", "config.json");
5671
- this.gitExecutor = new import_core8.GitExecutor();
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 (!fs13.existsSync(this.bareRepoPath)) {
6770
+ if (!fs14.existsSync(this.bareRepoPath)) {
5680
6771
  return false;
5681
6772
  }
5682
- if (!fs13.existsSync(this.configPath)) {
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: execSync7 } = require("child_process");
6793
+ const { execSync: execSync8 } = require("child_process");
5703
6794
  let fetchRefspec = null;
5704
6795
  try {
5705
- fetchRefspec = execSync7("git config --get remote.origin.fetch", {
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
- execSync7('git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"', {
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 = path14.join(projectRoot, ".episoda");
5730
- fs13.mkdirSync(episodaDir, { recursive: true });
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 = path14.join(this.projectRoot, moduleUid);
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) => fs13.existsSync(w.worktreePath));
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
- fs13.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
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 = fs13.statSync(lockPath);
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 = fs13.readFileSync(lockPath, "utf-8").trim();
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
- fs13.unlinkSync(lockPath);
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
- fs13.unlinkSync(this.getLockPath());
7154
+ fs14.unlinkSync(this.getLockPath());
6064
7155
  } catch {
6065
7156
  }
6066
7157
  }
6067
7158
  readConfig() {
6068
7159
  try {
6069
- if (!fs13.existsSync(this.configPath)) {
7160
+ if (!fs14.existsSync(this.configPath)) {
6070
7161
  return null;
6071
7162
  }
6072
- const content = fs13.readFileSync(this.configPath, "utf-8");
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 = path14.dirname(this.configPath);
6082
- if (!fs13.existsSync(dir)) {
6083
- fs13.mkdirSync(dir, { recursive: true });
7172
+ const dir = path15.dirname(this.configPath);
7173
+ if (!fs14.existsSync(dir)) {
7174
+ fs14.mkdirSync(dir, { recursive: true });
6084
7175
  }
6085
- fs13.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
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 = path14.join(mainWorktree.worktreePath, file);
6167
- const destPath = path14.join(worktree.worktreePath, file);
6168
- if (fs13.existsSync(srcPath)) {
6169
- const destDir = path14.dirname(destPath);
6170
- if (!fs13.existsSync(destDir)) {
6171
- fs13.mkdirSync(destDir, { recursive: true });
6172
- }
6173
- fs13.copyFileSync(srcPath, destPath);
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: execSync7 } = require("child_process");
6205
- execSync7(script, {
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: execSync7 } = require("child_process");
6240
- execSync7(script, {
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 || path14.join(require("os").homedir(), "episoda");
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 = path14.resolve(startPath);
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 = path14.join(current, ".bare");
6270
- const episodaDir = path14.join(current, ".episoda");
6271
- if (fs13.existsSync(bareDir) && fs13.existsSync(episodaDir)) {
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 = path14.dirname(current);
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 path15 = __toESM(require("path"));
6287
- var fs14 = __toESM(require("fs"));
7377
+ var path16 = __toESM(require("path"));
7378
+ var fs15 = __toESM(require("fs"));
6288
7379
  var os5 = __toESM(require("os"));
6289
- var import_core9 = __toESM(require_dist());
7380
+ var import_core10 = __toESM(require_dist());
6290
7381
  function getEpisodaRoot2() {
6291
- return process.env.EPISODA_ROOT || path15.join(os5.homedir(), "episoda");
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 = path15.join(root, workspaceSlug, projectSlug, moduleUid);
7386
+ const worktreePath = path16.join(root, workspaceSlug, projectSlug, moduleUid);
6296
7387
  return {
6297
7388
  path: worktreePath,
6298
- exists: fs14.existsSync(worktreePath),
7389
+ exists: fs15.existsSync(worktreePath),
6299
7390
  moduleUid
6300
7391
  };
6301
7392
  }
6302
7393
  async function getWorktreeInfoForModule(moduleUid) {
6303
- const config = await (0, import_core9.loadConfig)();
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 fs15 = __toESM(require("fs"));
6323
- var path16 = __toESM(require("path"));
7413
+ var fs16 = __toESM(require("fs"));
7414
+ var path17 = __toESM(require("path"));
6324
7415
  function getInstallCommand(cwd) {
6325
- if (fs15.existsSync(path16.join(cwd, "bun.lockb"))) {
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 (fs15.existsSync(path16.join(cwd, "pnpm-lock.yaml"))) {
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 (fs15.existsSync(path16.join(cwd, "yarn.lock"))) {
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 (fs15.existsSync(path16.join(cwd, "package-lock.json"))) {
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 (fs15.existsSync(path16.join(cwd, "package.json"))) {
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 (fs15.existsSync(path16.join(cwd, "Pipfile.lock")) || fs15.existsSync(path16.join(cwd, "Pipfile"))) {
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: fs15.existsSync(path16.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
7455
+ detectedFrom: fs16.existsSync(path17.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
6365
7456
  };
6366
7457
  }
6367
- if (fs15.existsSync(path16.join(cwd, "poetry.lock"))) {
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 (fs15.existsSync(path16.join(cwd, "pyproject.toml"))) {
6375
- const pyprojectPath = path16.join(cwd, "pyproject.toml");
6376
- const content = fs15.readFileSync(pyprojectPath, "utf-8");
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 (fs15.existsSync(path16.join(cwd, "requirements.txt"))) {
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 (fs15.existsSync(path16.join(cwd, "Gemfile.lock")) || fs15.existsSync(path16.join(cwd, "Gemfile"))) {
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: fs15.existsSync(path16.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
7487
+ detectedFrom: fs16.existsSync(path17.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
6397
7488
  };
6398
7489
  }
6399
- if (fs15.existsSync(path16.join(cwd, "go.sum")) || fs15.existsSync(path16.join(cwd, "go.mod"))) {
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: fs15.existsSync(path16.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
7494
+ detectedFrom: fs16.existsSync(path17.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
6404
7495
  };
6405
7496
  }
6406
- if (fs15.existsSync(path16.join(cwd, "Cargo.lock")) || fs15.existsSync(path16.join(cwd, "Cargo.toml"))) {
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: fs15.existsSync(path16.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
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 fs16 = __toESM(require("fs"));
7508
+ var fs17 = __toESM(require("fs"));
6418
7509
  var os6 = __toESM(require("os"));
6419
- var path17 = __toESM(require("path"));
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, import_core10.saveConfig)(updatedConfig);
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, import_core10.loadConfig)();
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, import_core10.loadConfig)();
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, import_core10.loadConfig)();
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, import_core10.loadConfig)();
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, import_core10.loadConfig)();
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 import_core10.EpisodaClient();
6908
- const gitExecutor = new import_core10.GitExecutor();
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 = path17.join(projectPath, ".bare");
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 tunnelManager = getTunnelManager();
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] EP973: Cannot resolve worktree path for ${cmd.moduleUid}`);
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] EP973: Worktree not found at ${worktree.path}`);
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] EP973: Using worktree path ${worktree.path} for ${cmd.moduleUid}`);
8167
+ console.log(`[Daemon] EP1024: Using worktree path ${worktree.path} for ${cmd.moduleUid}`);
7077
8168
  const port = cmd.port || detectDevPort(worktree.path);
7078
- const previewUrl = `https://${cmd.moduleUid.toLowerCase()}-${cmd.projectUid.toLowerCase()}.episoda.site`;
7079
- const reportTunnelStatus = async (data) => {
7080
- const config2 = await (0, import_core10.loadConfig)();
7081
- if (config2?.access_token) {
7082
- try {
7083
- const apiUrl = config2.api_url || "https://episoda.dev";
7084
- const response = await fetch(`${apiUrl}/api/modules/${cmd.moduleUid}/tunnel`, {
7085
- method: "POST",
7086
- headers: {
7087
- "Authorization": `Bearer ${config2.access_token}`,
7088
- "Content-Type": "application/json"
7089
- },
7090
- body: JSON.stringify(data)
7091
- });
7092
- if (response.ok) {
7093
- console.log(`[Daemon] Tunnel status reported for ${cmd.moduleUid}`);
7094
- } else {
7095
- console.warn(`[Daemon] Failed to report tunnel status: ${response.statusText}`);
7096
- }
7097
- } catch (reportError) {
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 (fs16.existsSync(pidPath)) {
7426
- const pidStr = fs16.readFileSync(pidPath, "utf-8").trim();
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: execSync7 } = await import("child_process");
7506
- execSync7(`git config episoda.userId ${userId}`, {
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
- execSync7(`git config episoda.workspaceId ${workspaceId}`, {
8515
+ execSync8(`git config episoda.workspaceId ${workspaceId}`, {
7512
8516
  cwd: projectPath,
7513
8517
  encoding: "utf8",
7514
8518
  stdio: "pipe"
7515
8519
  });
7516
- execSync7(`git config episoda.machineId ${machineId}`, {
8520
+ execSync8(`git config episoda.machineId ${machineId}`, {
7517
8521
  cwd: projectPath,
7518
8522
  encoding: "utf8",
7519
8523
  stdio: "pipe"
7520
8524
  });
7521
- execSync7(`git config episoda.projectId ${projectId}`, {
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
- execSync7(`git config episoda.deviceId ${deviceId}`, {
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 = path17.join(projectPath, ".git", "hooks");
7548
- if (!fs16.existsSync(hooksDir)) {
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 = path17.join(hooksDir, hookName);
7555
- const bundledHookPath = path17.join(__dirname, "..", "hooks", hookName);
7556
- if (!fs16.existsSync(bundledHookPath)) {
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 = fs16.readFileSync(bundledHookPath, "utf-8");
7561
- if (fs16.existsSync(hookPath)) {
7562
- const existingContent = fs16.readFileSync(hookPath, "utf-8");
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
- fs16.writeFileSync(hookPath, hookContent, { mode: 493 });
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, import_core10.loadConfig)();
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, import_core10.saveConfig)(updatedConfig);
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, import_core10.loadConfig)();
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, import_core10.saveConfig)(updatedConfig);
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, import_core10.loadConfig)();
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, import_core10.loadConfig)();
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, import_core10.loadConfig)();
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, import_core10.loadConfig)();
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, import_core10.loadConfig)();
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: execSync7 } = await import("child_process");
7942
- execSync7(installCmd.command.join(" "), {
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: execSync7 } = await import("child_process");
7996
- execSync7(installCmd.command.join(" "), {
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, import_core10.loadConfig)();
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, import_core10.loadConfig)();
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
- * EP932: Now uses restartDevServer() for robust dev server restart with auto-restart
9296
+ * EP1024: Refactored to use PreviewManager for unified preview lifecycle
8293
9297
  */
8294
9298
  async restartTunnel(moduleUid, port) {
8295
- const tunnelManager = getTunnelManager();
9299
+ const previewManager = getPreviewManager();
8296
9300
  try {
8297
- await tunnelManager.stopTunnel(moduleUid);
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 devServerResult = await restartDevServer(moduleUid);
8305
- if (!devServerResult.success) {
8306
- console.log(`[Daemon] EP932: No tracked server for ${moduleUid}, looking up project...`);
8307
- let projectId = null;
8308
- try {
8309
- const moduleResponse = await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}`);
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: 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 (startResult.success) {
8364
- console.log(`[Daemon] EP833: Tunnel restart successful for ${moduleUid}`);
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: Tunnel restart failed for ${moduleUid}: ${startResult.error}`);
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 tunnel for ${moduleUid}:`, error);
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 (fs16.existsSync(pidPath)) {
8548
- fs16.unlinkSync(pidPath);
9543
+ if (fs17.existsSync(pidPath)) {
9544
+ fs17.unlinkSync(pidPath);
8549
9545
  console.log("[Daemon] PID file cleaned up");
8550
9546
  }
8551
9547
  } catch (error) {