episoda 0.2.52 → 0.2.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon/daemon-process.js +778 -199
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +18 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1563,15 +1563,15 @@ var require_git_executor = __commonJS({
|
|
|
1563
1563
|
try {
|
|
1564
1564
|
const { stdout: gitDir } = await execAsync2("git rev-parse --git-dir", { cwd, timeout: 5e3 });
|
|
1565
1565
|
const gitDirPath = gitDir.trim();
|
|
1566
|
-
const
|
|
1566
|
+
const fs19 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1567
1567
|
const rebaseMergePath = `${gitDirPath}/rebase-merge`;
|
|
1568
1568
|
const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
|
|
1569
1569
|
try {
|
|
1570
|
-
await
|
|
1570
|
+
await fs19.access(rebaseMergePath);
|
|
1571
1571
|
inRebase = true;
|
|
1572
1572
|
} catch {
|
|
1573
1573
|
try {
|
|
1574
|
-
await
|
|
1574
|
+
await fs19.access(rebaseApplyPath);
|
|
1575
1575
|
inRebase = true;
|
|
1576
1576
|
} catch {
|
|
1577
1577
|
inRebase = false;
|
|
@@ -1625,9 +1625,9 @@ var require_git_executor = __commonJS({
|
|
|
1625
1625
|
error: validation.error || "UNKNOWN_ERROR"
|
|
1626
1626
|
};
|
|
1627
1627
|
}
|
|
1628
|
-
const
|
|
1628
|
+
const fs19 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1629
1629
|
try {
|
|
1630
|
-
await
|
|
1630
|
+
await fs19.access(command.path);
|
|
1631
1631
|
return {
|
|
1632
1632
|
success: false,
|
|
1633
1633
|
error: "WORKTREE_EXISTS",
|
|
@@ -1681,9 +1681,9 @@ var require_git_executor = __commonJS({
|
|
|
1681
1681
|
*/
|
|
1682
1682
|
async executeWorktreeRemove(command, cwd, options) {
|
|
1683
1683
|
try {
|
|
1684
|
-
const
|
|
1684
|
+
const fs19 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1685
1685
|
try {
|
|
1686
|
-
await
|
|
1686
|
+
await fs19.access(command.path);
|
|
1687
1687
|
} catch {
|
|
1688
1688
|
return {
|
|
1689
1689
|
success: false,
|
|
@@ -1718,7 +1718,7 @@ var require_git_executor = __commonJS({
|
|
|
1718
1718
|
const result = await this.runGitCommand(args, cwd, options);
|
|
1719
1719
|
if (result.success) {
|
|
1720
1720
|
try {
|
|
1721
|
-
await
|
|
1721
|
+
await fs19.rm(command.path, { recursive: true, force: true });
|
|
1722
1722
|
} catch {
|
|
1723
1723
|
}
|
|
1724
1724
|
return {
|
|
@@ -1852,10 +1852,10 @@ var require_git_executor = __commonJS({
|
|
|
1852
1852
|
*/
|
|
1853
1853
|
async executeCloneBare(command, options) {
|
|
1854
1854
|
try {
|
|
1855
|
-
const
|
|
1856
|
-
const
|
|
1855
|
+
const fs19 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1856
|
+
const path20 = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1857
1857
|
try {
|
|
1858
|
-
await
|
|
1858
|
+
await fs19.access(command.path);
|
|
1859
1859
|
return {
|
|
1860
1860
|
success: false,
|
|
1861
1861
|
error: "BRANCH_ALREADY_EXISTS",
|
|
@@ -1864,9 +1864,9 @@ var require_git_executor = __commonJS({
|
|
|
1864
1864
|
};
|
|
1865
1865
|
} catch {
|
|
1866
1866
|
}
|
|
1867
|
-
const parentDir =
|
|
1867
|
+
const parentDir = path20.dirname(command.path);
|
|
1868
1868
|
try {
|
|
1869
|
-
await
|
|
1869
|
+
await fs19.mkdir(parentDir, { recursive: true });
|
|
1870
1870
|
} catch {
|
|
1871
1871
|
}
|
|
1872
1872
|
const { stdout, stderr } = await execAsync2(
|
|
@@ -1914,22 +1914,22 @@ var require_git_executor = __commonJS({
|
|
|
1914
1914
|
*/
|
|
1915
1915
|
async executeProjectInfo(cwd, options) {
|
|
1916
1916
|
try {
|
|
1917
|
-
const
|
|
1918
|
-
const
|
|
1917
|
+
const fs19 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1918
|
+
const path20 = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1919
1919
|
let currentPath = cwd;
|
|
1920
1920
|
let projectPath = cwd;
|
|
1921
1921
|
let bareRepoPath;
|
|
1922
1922
|
for (let i = 0; i < 10; i++) {
|
|
1923
|
-
const bareDir =
|
|
1924
|
-
const episodaDir =
|
|
1923
|
+
const bareDir = path20.join(currentPath, ".bare");
|
|
1924
|
+
const episodaDir = path20.join(currentPath, ".episoda");
|
|
1925
1925
|
try {
|
|
1926
|
-
await
|
|
1927
|
-
await
|
|
1926
|
+
await fs19.access(bareDir);
|
|
1927
|
+
await fs19.access(episodaDir);
|
|
1928
1928
|
projectPath = currentPath;
|
|
1929
1929
|
bareRepoPath = bareDir;
|
|
1930
1930
|
break;
|
|
1931
1931
|
} catch {
|
|
1932
|
-
const parentPath =
|
|
1932
|
+
const parentPath = path20.dirname(currentPath);
|
|
1933
1933
|
if (parentPath === currentPath) {
|
|
1934
1934
|
break;
|
|
1935
1935
|
}
|
|
@@ -2538,31 +2538,31 @@ var require_auth = __commonJS({
|
|
|
2538
2538
|
exports2.loadConfig = loadConfig7;
|
|
2539
2539
|
exports2.saveConfig = saveConfig2;
|
|
2540
2540
|
exports2.validateToken = validateToken;
|
|
2541
|
-
var
|
|
2542
|
-
var
|
|
2543
|
-
var
|
|
2541
|
+
var fs19 = __importStar(require("fs"));
|
|
2542
|
+
var path20 = __importStar(require("path"));
|
|
2543
|
+
var os9 = __importStar(require("os"));
|
|
2544
2544
|
var child_process_1 = require("child_process");
|
|
2545
2545
|
var DEFAULT_CONFIG_FILE = "config.json";
|
|
2546
2546
|
function getConfigDir8() {
|
|
2547
|
-
return process.env.EPISODA_CONFIG_DIR ||
|
|
2547
|
+
return process.env.EPISODA_CONFIG_DIR || path20.join(os9.homedir(), ".episoda");
|
|
2548
2548
|
}
|
|
2549
2549
|
function getConfigPath(configPath) {
|
|
2550
2550
|
if (configPath) {
|
|
2551
2551
|
return configPath;
|
|
2552
2552
|
}
|
|
2553
|
-
return
|
|
2553
|
+
return path20.join(getConfigDir8(), DEFAULT_CONFIG_FILE);
|
|
2554
2554
|
}
|
|
2555
2555
|
function ensureConfigDir(configPath) {
|
|
2556
|
-
const dir =
|
|
2557
|
-
const isNew = !
|
|
2556
|
+
const dir = path20.dirname(configPath);
|
|
2557
|
+
const isNew = !fs19.existsSync(dir);
|
|
2558
2558
|
if (isNew) {
|
|
2559
|
-
|
|
2559
|
+
fs19.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2560
2560
|
}
|
|
2561
2561
|
if (process.platform === "darwin") {
|
|
2562
|
-
const nosyncPath =
|
|
2563
|
-
if (isNew || !
|
|
2562
|
+
const nosyncPath = path20.join(dir, ".nosync");
|
|
2563
|
+
if (isNew || !fs19.existsSync(nosyncPath)) {
|
|
2564
2564
|
try {
|
|
2565
|
-
|
|
2565
|
+
fs19.writeFileSync(nosyncPath, "", { mode: 384 });
|
|
2566
2566
|
(0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
|
|
2567
2567
|
stdio: "ignore",
|
|
2568
2568
|
timeout: 5e3
|
|
@@ -2574,9 +2574,9 @@ var require_auth = __commonJS({
|
|
|
2574
2574
|
}
|
|
2575
2575
|
async function loadConfig7(configPath) {
|
|
2576
2576
|
const fullPath = getConfigPath(configPath);
|
|
2577
|
-
if (
|
|
2577
|
+
if (fs19.existsSync(fullPath)) {
|
|
2578
2578
|
try {
|
|
2579
|
-
const content =
|
|
2579
|
+
const content = fs19.readFileSync(fullPath, "utf8");
|
|
2580
2580
|
const config = JSON.parse(content);
|
|
2581
2581
|
return config;
|
|
2582
2582
|
} catch (error) {
|
|
@@ -2601,7 +2601,7 @@ var require_auth = __commonJS({
|
|
|
2601
2601
|
ensureConfigDir(fullPath);
|
|
2602
2602
|
try {
|
|
2603
2603
|
const content = JSON.stringify(config, null, 2);
|
|
2604
|
-
|
|
2604
|
+
fs19.writeFileSync(fullPath, content, { mode: 384 });
|
|
2605
2605
|
} catch (error) {
|
|
2606
2606
|
throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
|
|
2607
2607
|
}
|
|
@@ -2718,7 +2718,7 @@ var require_package = __commonJS({
|
|
|
2718
2718
|
"package.json"(exports2, module2) {
|
|
2719
2719
|
module2.exports = {
|
|
2720
2720
|
name: "episoda",
|
|
2721
|
-
version: "0.2.
|
|
2721
|
+
version: "0.2.54",
|
|
2722
2722
|
description: "CLI tool for Episoda local development workflow orchestration",
|
|
2723
2723
|
main: "dist/index.js",
|
|
2724
2724
|
types: "dist/index.d.ts",
|
|
@@ -5272,10 +5272,10 @@ var import_events3 = require("events");
|
|
|
5272
5272
|
var import_fs = require("fs");
|
|
5273
5273
|
|
|
5274
5274
|
// src/preview/dev-server-runner.ts
|
|
5275
|
-
var
|
|
5275
|
+
var import_child_process10 = require("child_process");
|
|
5276
5276
|
var http = __toESM(require("http"));
|
|
5277
|
-
var
|
|
5278
|
-
var
|
|
5277
|
+
var fs12 = __toESM(require("fs"));
|
|
5278
|
+
var path13 = __toESM(require("path"));
|
|
5279
5279
|
var import_events2 = require("events");
|
|
5280
5280
|
var import_core8 = __toESM(require_dist());
|
|
5281
5281
|
|
|
@@ -5450,6 +5450,296 @@ No cached values available as fallback.`
|
|
|
5450
5450
|
}
|
|
5451
5451
|
}
|
|
5452
5452
|
|
|
5453
|
+
// src/preview/dev-server-registry.ts
|
|
5454
|
+
var fs11 = __toESM(require("fs"));
|
|
5455
|
+
var path12 = __toESM(require("path"));
|
|
5456
|
+
var os5 = __toESM(require("os"));
|
|
5457
|
+
var import_child_process9 = require("child_process");
|
|
5458
|
+
var DEV_SERVER_REGISTRY_DIR = path12.join(os5.homedir(), ".episoda", "dev-servers");
|
|
5459
|
+
var DevServerRegistry = class {
|
|
5460
|
+
constructor() {
|
|
5461
|
+
this.ensureRegistryDir();
|
|
5462
|
+
}
|
|
5463
|
+
/**
|
|
5464
|
+
* Ensure the registry directory exists
|
|
5465
|
+
*/
|
|
5466
|
+
ensureRegistryDir() {
|
|
5467
|
+
try {
|
|
5468
|
+
if (!fs11.existsSync(DEV_SERVER_REGISTRY_DIR)) {
|
|
5469
|
+
console.log(`[DevServerRegistry] EP1042: Creating registry directory: ${DEV_SERVER_REGISTRY_DIR}`);
|
|
5470
|
+
fs11.mkdirSync(DEV_SERVER_REGISTRY_DIR, { recursive: true });
|
|
5471
|
+
}
|
|
5472
|
+
} catch (error) {
|
|
5473
|
+
console.error(`[DevServerRegistry] EP1042: Failed to create registry directory:`, error);
|
|
5474
|
+
throw error;
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
/**
|
|
5478
|
+
* Get the registry file path for a module
|
|
5479
|
+
*/
|
|
5480
|
+
getEntryPath(moduleUid) {
|
|
5481
|
+
return path12.join(DEV_SERVER_REGISTRY_DIR, `${moduleUid}.json`);
|
|
5482
|
+
}
|
|
5483
|
+
/**
|
|
5484
|
+
* Register a dev server
|
|
5485
|
+
*
|
|
5486
|
+
* @param entry - Dev server entry to register
|
|
5487
|
+
*/
|
|
5488
|
+
register(entry) {
|
|
5489
|
+
try {
|
|
5490
|
+
this.ensureRegistryDir();
|
|
5491
|
+
const entryPath = this.getEntryPath(entry.moduleUid);
|
|
5492
|
+
fs11.writeFileSync(entryPath, JSON.stringify(entry, null, 2), "utf8");
|
|
5493
|
+
console.log(`[DevServerRegistry] EP1042: Registered ${entry.moduleUid} (PID ${entry.pid}, port ${entry.port})`);
|
|
5494
|
+
} catch (error) {
|
|
5495
|
+
console.error(`[DevServerRegistry] EP1042: Failed to register ${entry.moduleUid}:`, error);
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
/**
|
|
5499
|
+
* Unregister a dev server
|
|
5500
|
+
*
|
|
5501
|
+
* @param moduleUid - Module UID to unregister
|
|
5502
|
+
*/
|
|
5503
|
+
unregister(moduleUid) {
|
|
5504
|
+
try {
|
|
5505
|
+
const entryPath = this.getEntryPath(moduleUid);
|
|
5506
|
+
if (fs11.existsSync(entryPath)) {
|
|
5507
|
+
fs11.unlinkSync(entryPath);
|
|
5508
|
+
console.log(`[DevServerRegistry] EP1042: Unregistered ${moduleUid}`);
|
|
5509
|
+
}
|
|
5510
|
+
} catch (error) {
|
|
5511
|
+
console.error(`[DevServerRegistry] EP1042: Failed to unregister ${moduleUid}:`, error);
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
/**
|
|
5515
|
+
* Get a dev server entry by module UID
|
|
5516
|
+
*
|
|
5517
|
+
* @param moduleUid - Module UID to look up
|
|
5518
|
+
* @returns Entry if found and valid, null otherwise
|
|
5519
|
+
*/
|
|
5520
|
+
getByModule(moduleUid) {
|
|
5521
|
+
try {
|
|
5522
|
+
const entryPath = this.getEntryPath(moduleUid);
|
|
5523
|
+
if (!fs11.existsSync(entryPath)) {
|
|
5524
|
+
return null;
|
|
5525
|
+
}
|
|
5526
|
+
const content = fs11.readFileSync(entryPath, "utf8");
|
|
5527
|
+
const entry = JSON.parse(content);
|
|
5528
|
+
if (!entry.pid || !entry.port || !entry.worktreePath) {
|
|
5529
|
+
console.warn(`[DevServerRegistry] EP1042: Invalid entry for ${moduleUid}, removing`);
|
|
5530
|
+
this.unregister(moduleUid);
|
|
5531
|
+
return null;
|
|
5532
|
+
}
|
|
5533
|
+
if (!this.isProcessRunning(entry.pid)) {
|
|
5534
|
+
console.log(`[DevServerRegistry] EP1042: PID ${entry.pid} for ${moduleUid} is not running, removing stale entry`);
|
|
5535
|
+
this.unregister(moduleUid);
|
|
5536
|
+
return null;
|
|
5537
|
+
}
|
|
5538
|
+
return entry;
|
|
5539
|
+
} catch (error) {
|
|
5540
|
+
console.warn(`[DevServerRegistry] EP1042: Failed to read entry for ${moduleUid}:`, error);
|
|
5541
|
+
this.unregister(moduleUid);
|
|
5542
|
+
return null;
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
/**
|
|
5546
|
+
* Get a dev server entry by port
|
|
5547
|
+
*
|
|
5548
|
+
* @param port - Port to look up
|
|
5549
|
+
* @returns Entry if found, null otherwise
|
|
5550
|
+
*/
|
|
5551
|
+
getByPort(port) {
|
|
5552
|
+
const entries = this.getAll();
|
|
5553
|
+
return entries.find((e) => e.port === port) || null;
|
|
5554
|
+
}
|
|
5555
|
+
/**
|
|
5556
|
+
* Get all registered dev server entries
|
|
5557
|
+
*
|
|
5558
|
+
* @returns Array of valid entries
|
|
5559
|
+
*/
|
|
5560
|
+
getAll() {
|
|
5561
|
+
const entries = [];
|
|
5562
|
+
try {
|
|
5563
|
+
this.ensureRegistryDir();
|
|
5564
|
+
const files = fs11.readdirSync(DEV_SERVER_REGISTRY_DIR).filter((f) => f.endsWith(".json"));
|
|
5565
|
+
for (const file of files) {
|
|
5566
|
+
const moduleUid = file.replace(".json", "");
|
|
5567
|
+
const entry = this.getByModule(moduleUid);
|
|
5568
|
+
if (entry) {
|
|
5569
|
+
entries.push(entry);
|
|
5570
|
+
}
|
|
5571
|
+
}
|
|
5572
|
+
} catch (error) {
|
|
5573
|
+
console.error(`[DevServerRegistry] EP1042: Failed to read registry:`, error);
|
|
5574
|
+
}
|
|
5575
|
+
return entries;
|
|
5576
|
+
}
|
|
5577
|
+
/**
|
|
5578
|
+
* Check if a process is running by PID
|
|
5579
|
+
*/
|
|
5580
|
+
isProcessRunning(pid) {
|
|
5581
|
+
try {
|
|
5582
|
+
process.kill(pid, 0);
|
|
5583
|
+
return true;
|
|
5584
|
+
} catch {
|
|
5585
|
+
return false;
|
|
5586
|
+
}
|
|
5587
|
+
}
|
|
5588
|
+
/**
|
|
5589
|
+
* Kill a process by PID
|
|
5590
|
+
*
|
|
5591
|
+
* @param pid - Process ID to kill
|
|
5592
|
+
* @param signal - Signal to send (default: SIGTERM)
|
|
5593
|
+
* @returns True if signal was sent
|
|
5594
|
+
*/
|
|
5595
|
+
killProcess(pid, signal = "SIGTERM") {
|
|
5596
|
+
try {
|
|
5597
|
+
process.kill(pid, signal);
|
|
5598
|
+
console.log(`[DevServerRegistry] EP1042: Sent ${signal} to PID ${pid}`);
|
|
5599
|
+
return true;
|
|
5600
|
+
} catch {
|
|
5601
|
+
return false;
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
/**
|
|
5605
|
+
* Get the working directory of a process by PID
|
|
5606
|
+
*
|
|
5607
|
+
* @param pid - Process ID
|
|
5608
|
+
* @returns Working directory or null
|
|
5609
|
+
*/
|
|
5610
|
+
getProcessCwd(pid) {
|
|
5611
|
+
try {
|
|
5612
|
+
const output = (0, import_child_process9.execSync)(`lsof -p ${pid} -Fn | grep ^n | grep cwd | head -1`, {
|
|
5613
|
+
encoding: "utf8",
|
|
5614
|
+
timeout: 5e3
|
|
5615
|
+
}).trim();
|
|
5616
|
+
if (output.startsWith("n")) {
|
|
5617
|
+
return output.slice(1);
|
|
5618
|
+
}
|
|
5619
|
+
return null;
|
|
5620
|
+
} catch {
|
|
5621
|
+
try {
|
|
5622
|
+
return fs11.readlinkSync(`/proc/${pid}/cwd`);
|
|
5623
|
+
} catch {
|
|
5624
|
+
return null;
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
/**
|
|
5629
|
+
* Find node processes running in a specific directory
|
|
5630
|
+
*
|
|
5631
|
+
* @param worktreePath - Path to search for
|
|
5632
|
+
* @returns Array of PIDs
|
|
5633
|
+
*/
|
|
5634
|
+
findProcessesInWorktree(worktreePath) {
|
|
5635
|
+
const pids = [];
|
|
5636
|
+
try {
|
|
5637
|
+
const output = (0, import_child_process9.execSync)(
|
|
5638
|
+
`lsof -c node -c next | grep "${worktreePath}" | awk '{print $2}' | sort -u`,
|
|
5639
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
5640
|
+
).trim();
|
|
5641
|
+
if (output) {
|
|
5642
|
+
for (const line of output.split("\n")) {
|
|
5643
|
+
const pid = parseInt(line, 10);
|
|
5644
|
+
if (!isNaN(pid) && pid > 0) {
|
|
5645
|
+
pids.push(pid);
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
} catch {
|
|
5650
|
+
}
|
|
5651
|
+
return [...new Set(pids)];
|
|
5652
|
+
}
|
|
5653
|
+
/**
|
|
5654
|
+
* Clean up orphaned dev servers
|
|
5655
|
+
*
|
|
5656
|
+
* An orphan is a dev server whose:
|
|
5657
|
+
* - PID is no longer running
|
|
5658
|
+
* - Module is not in doing/review state
|
|
5659
|
+
* - Worktree no longer exists
|
|
5660
|
+
*
|
|
5661
|
+
* @param activeModuleUids - UIDs of modules in doing/review state
|
|
5662
|
+
* @returns Cleanup stats
|
|
5663
|
+
*/
|
|
5664
|
+
async cleanupOrphans(activeModuleUids) {
|
|
5665
|
+
const cleaned = [];
|
|
5666
|
+
try {
|
|
5667
|
+
const entries = this.getAll();
|
|
5668
|
+
for (const entry of entries) {
|
|
5669
|
+
if (!activeModuleUids.includes(entry.moduleUid)) {
|
|
5670
|
+
console.log(`[DevServerRegistry] EP1042: Module ${entry.moduleUid} not active, cleaning up PID ${entry.pid}`);
|
|
5671
|
+
if (this.isProcessRunning(entry.pid)) {
|
|
5672
|
+
this.killProcess(entry.pid, "SIGTERM");
|
|
5673
|
+
await this.wait(1e3);
|
|
5674
|
+
if (this.isProcessRunning(entry.pid)) {
|
|
5675
|
+
this.killProcess(entry.pid, "SIGKILL");
|
|
5676
|
+
}
|
|
5677
|
+
cleaned.push(entry.pid);
|
|
5678
|
+
}
|
|
5679
|
+
this.unregister(entry.moduleUid);
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
if (cleaned.length > 0) {
|
|
5683
|
+
console.log(`[DevServerRegistry] EP1042: Cleaned up ${cleaned.length} orphaned dev server(s): ${cleaned.join(", ")}`);
|
|
5684
|
+
}
|
|
5685
|
+
} catch (error) {
|
|
5686
|
+
console.error(`[DevServerRegistry] EP1042: Error during orphan cleanup:`, error);
|
|
5687
|
+
}
|
|
5688
|
+
return { cleaned: cleaned.length, pids: cleaned };
|
|
5689
|
+
}
|
|
5690
|
+
/**
|
|
5691
|
+
* Find processes on a specific port
|
|
5692
|
+
*
|
|
5693
|
+
* @param port - Port to check
|
|
5694
|
+
* @returns Array of PIDs
|
|
5695
|
+
*/
|
|
5696
|
+
findProcessesOnPort(port) {
|
|
5697
|
+
try {
|
|
5698
|
+
const output = (0, import_child_process9.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
|
|
5699
|
+
if (!output) {
|
|
5700
|
+
return [];
|
|
5701
|
+
}
|
|
5702
|
+
return output.split("\n").map((pid) => parseInt(pid, 10)).filter((pid) => !isNaN(pid));
|
|
5703
|
+
} catch {
|
|
5704
|
+
return [];
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
/**
|
|
5708
|
+
* Kill all processes on a specific port
|
|
5709
|
+
*
|
|
5710
|
+
* @param port - Port to clear
|
|
5711
|
+
* @returns PIDs that were killed
|
|
5712
|
+
*/
|
|
5713
|
+
async killProcessesOnPort(port) {
|
|
5714
|
+
const pids = this.findProcessesOnPort(port);
|
|
5715
|
+
const killed = [];
|
|
5716
|
+
for (const pid of pids) {
|
|
5717
|
+
this.killProcess(pid, "SIGTERM");
|
|
5718
|
+
killed.push(pid);
|
|
5719
|
+
}
|
|
5720
|
+
if (killed.length > 0) {
|
|
5721
|
+
await this.wait(1e3);
|
|
5722
|
+
for (const pid of killed) {
|
|
5723
|
+
if (this.isProcessRunning(pid)) {
|
|
5724
|
+
this.killProcess(pid, "SIGKILL");
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
console.log(`[DevServerRegistry] EP1042: Killed ${killed.length} process(es) on port ${port}`);
|
|
5728
|
+
}
|
|
5729
|
+
return killed;
|
|
5730
|
+
}
|
|
5731
|
+
wait(ms) {
|
|
5732
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5733
|
+
}
|
|
5734
|
+
};
|
|
5735
|
+
var registryInstance = null;
|
|
5736
|
+
function getDevServerRegistry() {
|
|
5737
|
+
if (!registryInstance) {
|
|
5738
|
+
registryInstance = new DevServerRegistry();
|
|
5739
|
+
}
|
|
5740
|
+
return registryInstance;
|
|
5741
|
+
}
|
|
5742
|
+
|
|
5453
5743
|
// src/preview/dev-server-runner.ts
|
|
5454
5744
|
var DevServerRunner = class extends import_events2.EventEmitter {
|
|
5455
5745
|
constructor() {
|
|
@@ -5458,6 +5748,9 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
5458
5748
|
}
|
|
5459
5749
|
/**
|
|
5460
5750
|
* Start a dev server for a module
|
|
5751
|
+
*
|
|
5752
|
+
* EP1042: Now validates worktree ownership when port is already in use.
|
|
5753
|
+
* If a different worktree's server is on the port, it kills and restarts.
|
|
5461
5754
|
*/
|
|
5462
5755
|
async start(config) {
|
|
5463
5756
|
const {
|
|
@@ -5467,9 +5760,26 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
5467
5760
|
customCommand,
|
|
5468
5761
|
autoRestart = true
|
|
5469
5762
|
} = config;
|
|
5763
|
+
const registry = getDevServerRegistry();
|
|
5470
5764
|
if (await isPortInUse(port)) {
|
|
5471
|
-
|
|
5472
|
-
|
|
5765
|
+
const existingEntry = registry.getByPort(port);
|
|
5766
|
+
if (existingEntry) {
|
|
5767
|
+
if (existingEntry.worktreePath === projectPath && existingEntry.moduleUid === moduleUid) {
|
|
5768
|
+
console.log(`[DevServerRunner] EP1042: Correct server already running on port ${port} for ${moduleUid}`);
|
|
5769
|
+
return { success: true, alreadyRunning: true };
|
|
5770
|
+
}
|
|
5771
|
+
console.log(`[DevServerRunner] EP1042: Port ${port} owned by ${existingEntry.moduleUid} (${existingEntry.worktreePath}), killing...`);
|
|
5772
|
+
await this.killProcessOnPort(port);
|
|
5773
|
+
registry.unregister(existingEntry.moduleUid);
|
|
5774
|
+
this.servers.delete(existingEntry.moduleUid);
|
|
5775
|
+
} else {
|
|
5776
|
+
console.log(`[DevServerRunner] EP1042: Unknown process on port ${port}, killing...`);
|
|
5777
|
+
await this.killProcessOnPort(port);
|
|
5778
|
+
}
|
|
5779
|
+
await this.wait(1e3);
|
|
5780
|
+
if (await isPortInUse(port)) {
|
|
5781
|
+
return { success: false, error: `Port ${port} still in use after cleanup` };
|
|
5782
|
+
}
|
|
5473
5783
|
}
|
|
5474
5784
|
const existing = this.servers.get(moduleUid);
|
|
5475
5785
|
if (existing && !existing.process.killed) {
|
|
@@ -5505,6 +5815,15 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
5505
5815
|
this.writeToLog(logPath, "Failed to start within timeout", true);
|
|
5506
5816
|
return { success: false, error: "Dev server failed to start within timeout" };
|
|
5507
5817
|
}
|
|
5818
|
+
if (process2.pid) {
|
|
5819
|
+
registry.register({
|
|
5820
|
+
pid: process2.pid,
|
|
5821
|
+
port,
|
|
5822
|
+
worktreePath: projectPath,
|
|
5823
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5824
|
+
moduleUid
|
|
5825
|
+
});
|
|
5826
|
+
}
|
|
5508
5827
|
console.log(`[DevServerRunner] Server started successfully on port ${port}`);
|
|
5509
5828
|
this.writeToLog(logPath, "Server started successfully");
|
|
5510
5829
|
this.emit("started", moduleUid, port);
|
|
@@ -5517,23 +5836,39 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
5517
5836
|
}
|
|
5518
5837
|
/**
|
|
5519
5838
|
* Stop a dev server
|
|
5839
|
+
*
|
|
5840
|
+
* EP1042: Now also unregisters from persistent registry.
|
|
5520
5841
|
*/
|
|
5521
5842
|
async stop(moduleUid) {
|
|
5522
5843
|
const state = this.servers.get(moduleUid);
|
|
5523
|
-
|
|
5844
|
+
const registry = getDevServerRegistry();
|
|
5845
|
+
const registryEntry = registry.getByModule(moduleUid);
|
|
5846
|
+
if (!state && !registryEntry) {
|
|
5524
5847
|
return;
|
|
5525
5848
|
}
|
|
5526
|
-
state
|
|
5527
|
-
|
|
5528
|
-
console.log(`[DevServerRunner] Stopping server for ${moduleUid}`);
|
|
5529
|
-
this.writeToLog(state.logFile, "Stopping server (manual stop)");
|
|
5530
|
-
state.process.kill("SIGTERM");
|
|
5531
|
-
await this.wait(2e3);
|
|
5849
|
+
if (state) {
|
|
5850
|
+
state.autoRestartEnabled = false;
|
|
5532
5851
|
if (!state.process.killed) {
|
|
5533
|
-
|
|
5852
|
+
console.log(`[DevServerRunner] Stopping server for ${moduleUid}`);
|
|
5853
|
+
this.writeToLog(state.logFile, "Stopping server (manual stop)");
|
|
5854
|
+
state.process.kill("SIGTERM");
|
|
5855
|
+
await this.wait(2e3);
|
|
5856
|
+
if (!state.process.killed) {
|
|
5857
|
+
state.process.kill("SIGKILL");
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5860
|
+
} else if (registryEntry) {
|
|
5861
|
+
console.log(`[DevServerRunner] EP1042: Stopping orphaned server for ${moduleUid} (PID ${registryEntry.pid})`);
|
|
5862
|
+
if (registry.isProcessRunning(registryEntry.pid)) {
|
|
5863
|
+
registry.killProcess(registryEntry.pid, "SIGTERM");
|
|
5864
|
+
await this.wait(2e3);
|
|
5865
|
+
if (registry.isProcessRunning(registryEntry.pid)) {
|
|
5866
|
+
registry.killProcess(registryEntry.pid, "SIGKILL");
|
|
5867
|
+
}
|
|
5534
5868
|
}
|
|
5535
5869
|
}
|
|
5536
5870
|
this.servers.delete(moduleUid);
|
|
5871
|
+
registry.unregister(moduleUid);
|
|
5537
5872
|
this.emit("stopped", moduleUid);
|
|
5538
5873
|
}
|
|
5539
5874
|
/**
|
|
@@ -5614,7 +5949,7 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
5614
5949
|
*/
|
|
5615
5950
|
async killProcessOnPort(port) {
|
|
5616
5951
|
try {
|
|
5617
|
-
const result = (0,
|
|
5952
|
+
const result = (0, import_child_process10.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
|
|
5618
5953
|
if (!result) {
|
|
5619
5954
|
return true;
|
|
5620
5955
|
}
|
|
@@ -5622,15 +5957,15 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
5622
5957
|
console.log(`[DevServerRunner] Found ${pids.length} process(es) on port ${port}`);
|
|
5623
5958
|
for (const pid of pids) {
|
|
5624
5959
|
try {
|
|
5625
|
-
(0,
|
|
5960
|
+
(0, import_child_process10.execSync)(`kill -15 ${pid} 2>/dev/null || true`);
|
|
5626
5961
|
} catch {
|
|
5627
5962
|
}
|
|
5628
5963
|
}
|
|
5629
5964
|
await this.wait(1e3);
|
|
5630
5965
|
for (const pid of pids) {
|
|
5631
5966
|
try {
|
|
5632
|
-
(0,
|
|
5633
|
-
(0,
|
|
5967
|
+
(0, import_child_process10.execSync)(`kill -0 ${pid} 2>/dev/null`);
|
|
5968
|
+
(0, import_child_process10.execSync)(`kill -9 ${pid} 2>/dev/null || true`);
|
|
5634
5969
|
} catch {
|
|
5635
5970
|
}
|
|
5636
5971
|
}
|
|
@@ -5654,8 +5989,8 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
5654
5989
|
cacheTtl: 300
|
|
5655
5990
|
});
|
|
5656
5991
|
console.log(`[DevServerRunner] Loaded ${Object.keys(result.envVars).length} env vars`);
|
|
5657
|
-
const envFilePath =
|
|
5658
|
-
if (!
|
|
5992
|
+
const envFilePath = path13.join(projectPath, ".env");
|
|
5993
|
+
if (!fs12.existsSync(envFilePath) && Object.keys(result.envVars).length > 0) {
|
|
5659
5994
|
console.log(`[DevServerRunner] Writing .env file`);
|
|
5660
5995
|
writeEnvFile(projectPath, result.envVars);
|
|
5661
5996
|
}
|
|
@@ -5679,7 +6014,7 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
5679
6014
|
PORT: String(port),
|
|
5680
6015
|
NODE_OPTIONS: enhancedNodeOptions
|
|
5681
6016
|
};
|
|
5682
|
-
const proc = (0,
|
|
6017
|
+
const proc = (0, import_child_process10.spawn)(cmd, args, {
|
|
5683
6018
|
cwd: projectPath,
|
|
5684
6019
|
env: mergedEnv,
|
|
5685
6020
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -5804,25 +6139,25 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
5804
6139
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5805
6140
|
}
|
|
5806
6141
|
getLogsDir() {
|
|
5807
|
-
const logsDir =
|
|
5808
|
-
if (!
|
|
5809
|
-
|
|
6142
|
+
const logsDir = path13.join((0, import_core8.getConfigDir)(), "logs");
|
|
6143
|
+
if (!fs12.existsSync(logsDir)) {
|
|
6144
|
+
fs12.mkdirSync(logsDir, { recursive: true });
|
|
5810
6145
|
}
|
|
5811
6146
|
return logsDir;
|
|
5812
6147
|
}
|
|
5813
6148
|
getLogFilePath(moduleUid) {
|
|
5814
|
-
return
|
|
6149
|
+
return path13.join(this.getLogsDir(), `dev-${moduleUid}.log`);
|
|
5815
6150
|
}
|
|
5816
6151
|
rotateLogIfNeeded(logPath) {
|
|
5817
6152
|
try {
|
|
5818
|
-
if (
|
|
5819
|
-
const stats =
|
|
6153
|
+
if (fs12.existsSync(logPath)) {
|
|
6154
|
+
const stats = fs12.statSync(logPath);
|
|
5820
6155
|
if (stats.size > DEV_SERVER_CONSTANTS.MAX_LOG_SIZE_BYTES) {
|
|
5821
6156
|
const backupPath = `${logPath}.1`;
|
|
5822
|
-
if (
|
|
5823
|
-
|
|
6157
|
+
if (fs12.existsSync(backupPath)) {
|
|
6158
|
+
fs12.unlinkSync(backupPath);
|
|
5824
6159
|
}
|
|
5825
|
-
|
|
6160
|
+
fs12.renameSync(logPath, backupPath);
|
|
5826
6161
|
}
|
|
5827
6162
|
}
|
|
5828
6163
|
} catch {
|
|
@@ -5833,7 +6168,7 @@ var DevServerRunner = class extends import_events2.EventEmitter {
|
|
|
5833
6168
|
try {
|
|
5834
6169
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
5835
6170
|
const prefix = isError ? "ERR" : "OUT";
|
|
5836
|
-
|
|
6171
|
+
fs12.appendFileSync(logPath, `[${timestamp}] [${prefix}] ${line}
|
|
5837
6172
|
`);
|
|
5838
6173
|
} catch {
|
|
5839
6174
|
}
|
|
@@ -5861,13 +6196,55 @@ function getDevServerRunner() {
|
|
|
5861
6196
|
}
|
|
5862
6197
|
|
|
5863
6198
|
// src/utils/port-allocator.ts
|
|
6199
|
+
var fs13 = __toESM(require("fs"));
|
|
6200
|
+
var path14 = __toESM(require("path"));
|
|
6201
|
+
var os6 = __toESM(require("os"));
|
|
5864
6202
|
var PORT_RANGE_START = 3100;
|
|
5865
6203
|
var PORT_RANGE_END = 3199;
|
|
5866
6204
|
var PORT_WARNING_THRESHOLD = 80;
|
|
6205
|
+
var PORTS_FILE = path14.join(os6.homedir(), ".episoda", "ports.json");
|
|
5867
6206
|
var portAssignments = /* @__PURE__ */ new Map();
|
|
6207
|
+
var initialized = false;
|
|
6208
|
+
function loadFromDisk() {
|
|
6209
|
+
if (initialized) return;
|
|
6210
|
+
initialized = true;
|
|
6211
|
+
try {
|
|
6212
|
+
if (fs13.existsSync(PORTS_FILE)) {
|
|
6213
|
+
const content = fs13.readFileSync(PORTS_FILE, "utf8");
|
|
6214
|
+
const data = JSON.parse(content);
|
|
6215
|
+
for (const [moduleUid, port] of Object.entries(data)) {
|
|
6216
|
+
if (typeof port === "number" && port >= PORT_RANGE_START && port <= PORT_RANGE_END) {
|
|
6217
|
+
portAssignments.set(moduleUid, port);
|
|
6218
|
+
}
|
|
6219
|
+
}
|
|
6220
|
+
if (portAssignments.size > 0) {
|
|
6221
|
+
console.log(`[PortAllocator] EP1042: Loaded ${portAssignments.size} port assignments from disk`);
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
} catch (error) {
|
|
6225
|
+
console.warn(`[PortAllocator] EP1042: Failed to load ports.json:`, error);
|
|
6226
|
+
}
|
|
6227
|
+
}
|
|
6228
|
+
function saveToDisk() {
|
|
6229
|
+
try {
|
|
6230
|
+
const dir = path14.dirname(PORTS_FILE);
|
|
6231
|
+
if (!fs13.existsSync(dir)) {
|
|
6232
|
+
fs13.mkdirSync(dir, { recursive: true });
|
|
6233
|
+
}
|
|
6234
|
+
const data = {};
|
|
6235
|
+
for (const [moduleUid, port] of portAssignments) {
|
|
6236
|
+
data[moduleUid] = port;
|
|
6237
|
+
}
|
|
6238
|
+
fs13.writeFileSync(PORTS_FILE, JSON.stringify(data, null, 2), "utf8");
|
|
6239
|
+
} catch (error) {
|
|
6240
|
+
console.warn(`[PortAllocator] EP1042: Failed to save ports.json:`, error);
|
|
6241
|
+
}
|
|
6242
|
+
}
|
|
5868
6243
|
function allocatePort(moduleUid) {
|
|
6244
|
+
loadFromDisk();
|
|
5869
6245
|
const existing = portAssignments.get(moduleUid);
|
|
5870
6246
|
if (existing) {
|
|
6247
|
+
console.log(`[PortAllocator] EP1042: Returning existing port ${existing} for ${moduleUid}`);
|
|
5871
6248
|
return existing;
|
|
5872
6249
|
}
|
|
5873
6250
|
const usedPorts = new Set(portAssignments.values());
|
|
@@ -5879,7 +6256,8 @@ function allocatePort(moduleUid) {
|
|
|
5879
6256
|
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
|
|
5880
6257
|
if (!usedPorts.has(port)) {
|
|
5881
6258
|
portAssignments.set(moduleUid, port);
|
|
5882
|
-
|
|
6259
|
+
saveToDisk();
|
|
6260
|
+
console.log(`[PortAllocator] EP1042: Allocated port ${port} to ${moduleUid}`);
|
|
5883
6261
|
return port;
|
|
5884
6262
|
}
|
|
5885
6263
|
}
|
|
@@ -5888,17 +6266,41 @@ function allocatePort(moduleUid) {
|
|
|
5888
6266
|
);
|
|
5889
6267
|
}
|
|
5890
6268
|
function releasePort(moduleUid) {
|
|
6269
|
+
loadFromDisk();
|
|
5891
6270
|
const port = portAssignments.get(moduleUid);
|
|
5892
6271
|
if (port) {
|
|
5893
6272
|
portAssignments.delete(moduleUid);
|
|
5894
|
-
|
|
6273
|
+
saveToDisk();
|
|
6274
|
+
console.log(`[PortAllocator] EP1042: Released port ${port} from ${moduleUid}`);
|
|
5895
6275
|
}
|
|
5896
6276
|
}
|
|
5897
6277
|
function clearAllPorts() {
|
|
5898
6278
|
const count = portAssignments.size;
|
|
5899
6279
|
portAssignments.clear();
|
|
6280
|
+
saveToDisk();
|
|
5900
6281
|
if (count > 0) {
|
|
5901
|
-
console.log(`[PortAllocator] Cleared ${count} port assignments`);
|
|
6282
|
+
console.log(`[PortAllocator] EP1042: Cleared ${count} port assignments`);
|
|
6283
|
+
}
|
|
6284
|
+
}
|
|
6285
|
+
function reconcileWithRegistry(activeModules) {
|
|
6286
|
+
loadFromDisk();
|
|
6287
|
+
let changed = false;
|
|
6288
|
+
for (const [moduleUid, port] of portAssignments) {
|
|
6289
|
+
if (!activeModules.has(moduleUid)) {
|
|
6290
|
+
console.log(`[PortAllocator] EP1042: Removing stale allocation ${moduleUid} -> ${port}`);
|
|
6291
|
+
portAssignments.delete(moduleUid);
|
|
6292
|
+
changed = true;
|
|
6293
|
+
}
|
|
6294
|
+
}
|
|
6295
|
+
for (const [moduleUid, port] of activeModules) {
|
|
6296
|
+
if (!portAssignments.has(moduleUid)) {
|
|
6297
|
+
console.log(`[PortAllocator] EP1042: Adding allocation from registry ${moduleUid} -> ${port}`);
|
|
6298
|
+
portAssignments.set(moduleUid, port);
|
|
6299
|
+
changed = true;
|
|
6300
|
+
}
|
|
6301
|
+
}
|
|
6302
|
+
if (changed) {
|
|
6303
|
+
saveToDisk();
|
|
5902
6304
|
}
|
|
5903
6305
|
}
|
|
5904
6306
|
|
|
@@ -6292,10 +6694,10 @@ function getPreviewManager() {
|
|
|
6292
6694
|
}
|
|
6293
6695
|
|
|
6294
6696
|
// src/utils/dev-server.ts
|
|
6295
|
-
var
|
|
6697
|
+
var import_child_process11 = require("child_process");
|
|
6296
6698
|
var import_core9 = __toESM(require_dist());
|
|
6297
|
-
var
|
|
6298
|
-
var
|
|
6699
|
+
var fs14 = __toESM(require("fs"));
|
|
6700
|
+
var path15 = __toESM(require("path"));
|
|
6299
6701
|
var MAX_RESTART_ATTEMPTS = 5;
|
|
6300
6702
|
var INITIAL_RESTART_DELAY_MS = 2e3;
|
|
6301
6703
|
var MAX_RESTART_DELAY_MS = 3e4;
|
|
@@ -6303,26 +6705,26 @@ var MAX_LOG_SIZE_BYTES = 5 * 1024 * 1024;
|
|
|
6303
6705
|
var NODE_MEMORY_LIMIT_MB = 2048;
|
|
6304
6706
|
var activeServers = /* @__PURE__ */ new Map();
|
|
6305
6707
|
function getLogsDir() {
|
|
6306
|
-
const logsDir =
|
|
6307
|
-
if (!
|
|
6308
|
-
|
|
6708
|
+
const logsDir = path15.join((0, import_core9.getConfigDir)(), "logs");
|
|
6709
|
+
if (!fs14.existsSync(logsDir)) {
|
|
6710
|
+
fs14.mkdirSync(logsDir, { recursive: true });
|
|
6309
6711
|
}
|
|
6310
6712
|
return logsDir;
|
|
6311
6713
|
}
|
|
6312
6714
|
function getLogFilePath(moduleUid) {
|
|
6313
|
-
return
|
|
6715
|
+
return path15.join(getLogsDir(), `dev-${moduleUid}.log`);
|
|
6314
6716
|
}
|
|
6315
6717
|
function rotateLogIfNeeded(logPath) {
|
|
6316
6718
|
try {
|
|
6317
|
-
if (
|
|
6318
|
-
const stats =
|
|
6719
|
+
if (fs14.existsSync(logPath)) {
|
|
6720
|
+
const stats = fs14.statSync(logPath);
|
|
6319
6721
|
if (stats.size > MAX_LOG_SIZE_BYTES) {
|
|
6320
6722
|
const backupPath = `${logPath}.1`;
|
|
6321
|
-
if (
|
|
6322
|
-
|
|
6723
|
+
if (fs14.existsSync(backupPath)) {
|
|
6724
|
+
fs14.unlinkSync(backupPath);
|
|
6323
6725
|
}
|
|
6324
|
-
|
|
6325
|
-
console.log(`[DevServer] EP932: Rotated log file for ${
|
|
6726
|
+
fs14.renameSync(logPath, backupPath);
|
|
6727
|
+
console.log(`[DevServer] EP932: Rotated log file for ${path15.basename(logPath)}`);
|
|
6326
6728
|
}
|
|
6327
6729
|
}
|
|
6328
6730
|
} catch (error) {
|
|
@@ -6335,13 +6737,13 @@ function writeToLog(logPath, line, isError = false) {
|
|
|
6335
6737
|
const prefix = isError ? "ERR" : "OUT";
|
|
6336
6738
|
const logLine = `[${timestamp}] [${prefix}] ${line}
|
|
6337
6739
|
`;
|
|
6338
|
-
|
|
6740
|
+
fs14.appendFileSync(logPath, logLine);
|
|
6339
6741
|
} catch {
|
|
6340
6742
|
}
|
|
6341
6743
|
}
|
|
6342
6744
|
async function killProcessOnPort(port) {
|
|
6343
6745
|
try {
|
|
6344
|
-
const result = (0,
|
|
6746
|
+
const result = (0, import_child_process11.execSync)(`lsof -ti:${port} 2>/dev/null || true`, { encoding: "utf8" }).trim();
|
|
6345
6747
|
if (!result) {
|
|
6346
6748
|
console.log(`[DevServer] EP929: No process found on port ${port}`);
|
|
6347
6749
|
return true;
|
|
@@ -6350,7 +6752,7 @@ async function killProcessOnPort(port) {
|
|
|
6350
6752
|
console.log(`[DevServer] EP929: Found ${pids.length} process(es) on port ${port}: ${pids.join(", ")}`);
|
|
6351
6753
|
for (const pid of pids) {
|
|
6352
6754
|
try {
|
|
6353
|
-
(0,
|
|
6755
|
+
(0, import_child_process11.execSync)(`kill -15 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
|
|
6354
6756
|
console.log(`[DevServer] EP929: Sent SIGTERM to PID ${pid}`);
|
|
6355
6757
|
} catch {
|
|
6356
6758
|
}
|
|
@@ -6358,8 +6760,8 @@ async function killProcessOnPort(port) {
|
|
|
6358
6760
|
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
6359
6761
|
for (const pid of pids) {
|
|
6360
6762
|
try {
|
|
6361
|
-
(0,
|
|
6362
|
-
(0,
|
|
6763
|
+
(0, import_child_process11.execSync)(`kill -0 ${pid} 2>/dev/null`, { encoding: "utf8" });
|
|
6764
|
+
(0, import_child_process11.execSync)(`kill -9 ${pid} 2>/dev/null || true`, { encoding: "utf8" });
|
|
6363
6765
|
console.log(`[DevServer] EP929: Force killed PID ${pid}`);
|
|
6364
6766
|
} catch {
|
|
6365
6767
|
}
|
|
@@ -6410,7 +6812,7 @@ function spawnDevServerProcess(projectPath, port, moduleUid, logPath, customComm
|
|
|
6410
6812
|
if (injectedCount > 0) {
|
|
6411
6813
|
console.log(`[DevServer] EP998: Injecting ${injectedCount} env vars from database`);
|
|
6412
6814
|
}
|
|
6413
|
-
const devProcess = (0,
|
|
6815
|
+
const devProcess = (0, import_child_process11.spawn)(cmd, args, {
|
|
6414
6816
|
cwd: projectPath,
|
|
6415
6817
|
env: mergedEnv,
|
|
6416
6818
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -6514,8 +6916,8 @@ async function startDevServer(projectPath, port = 3e3, moduleUid = "default", op
|
|
|
6514
6916
|
});
|
|
6515
6917
|
injectedEnvVars = result.envVars;
|
|
6516
6918
|
console.log(`[DevServer] EP998: Loaded ${Object.keys(injectedEnvVars).length} env vars (from ${result.fromCache ? "cache" : "server"})`);
|
|
6517
|
-
const envFilePath =
|
|
6518
|
-
if (!
|
|
6919
|
+
const envFilePath = path15.join(projectPath, ".env");
|
|
6920
|
+
if (!fs14.existsSync(envFilePath) && Object.keys(injectedEnvVars).length > 0) {
|
|
6519
6921
|
console.log(`[DevServer] EP1004: .env file missing, writing ${Object.keys(injectedEnvVars).length} vars to ${envFilePath}`);
|
|
6520
6922
|
writeEnvFile(projectPath, injectedEnvVars);
|
|
6521
6923
|
}
|
|
@@ -6621,8 +7023,8 @@ function getDevServerStatus() {
|
|
|
6621
7023
|
}
|
|
6622
7024
|
|
|
6623
7025
|
// src/daemon/worktree-manager.ts
|
|
6624
|
-
var
|
|
6625
|
-
var
|
|
7026
|
+
var fs15 = __toESM(require("fs"));
|
|
7027
|
+
var path16 = __toESM(require("path"));
|
|
6626
7028
|
var import_core10 = __toESM(require_dist());
|
|
6627
7029
|
function validateModuleUid(moduleUid) {
|
|
6628
7030
|
if (!moduleUid || typeof moduleUid !== "string" || !moduleUid.trim()) {
|
|
@@ -6646,8 +7048,8 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6646
7048
|
// ============================================================
|
|
6647
7049
|
this.lockPath = "";
|
|
6648
7050
|
this.projectRoot = projectRoot;
|
|
6649
|
-
this.bareRepoPath =
|
|
6650
|
-
this.configPath =
|
|
7051
|
+
this.bareRepoPath = path16.join(projectRoot, ".bare");
|
|
7052
|
+
this.configPath = path16.join(projectRoot, ".episoda", "config.json");
|
|
6651
7053
|
this.gitExecutor = new import_core10.GitExecutor();
|
|
6652
7054
|
}
|
|
6653
7055
|
/**
|
|
@@ -6656,10 +7058,10 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6656
7058
|
* @returns true if valid project, false otherwise
|
|
6657
7059
|
*/
|
|
6658
7060
|
async initialize() {
|
|
6659
|
-
if (!
|
|
7061
|
+
if (!fs15.existsSync(this.bareRepoPath)) {
|
|
6660
7062
|
return false;
|
|
6661
7063
|
}
|
|
6662
|
-
if (!
|
|
7064
|
+
if (!fs15.existsSync(this.configPath)) {
|
|
6663
7065
|
return false;
|
|
6664
7066
|
}
|
|
6665
7067
|
try {
|
|
@@ -6679,10 +7081,10 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6679
7081
|
*/
|
|
6680
7082
|
async ensureFetchRefspecConfigured() {
|
|
6681
7083
|
try {
|
|
6682
|
-
const { execSync:
|
|
7084
|
+
const { execSync: execSync9 } = require("child_process");
|
|
6683
7085
|
let fetchRefspec = null;
|
|
6684
7086
|
try {
|
|
6685
|
-
fetchRefspec =
|
|
7087
|
+
fetchRefspec = execSync9("git config --get remote.origin.fetch", {
|
|
6686
7088
|
cwd: this.bareRepoPath,
|
|
6687
7089
|
encoding: "utf-8",
|
|
6688
7090
|
timeout: 5e3
|
|
@@ -6691,7 +7093,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6691
7093
|
}
|
|
6692
7094
|
if (!fetchRefspec) {
|
|
6693
7095
|
console.log("[WorktreeManager] EP1014: Configuring missing fetch refspec for bare repo");
|
|
6694
|
-
|
|
7096
|
+
execSync9('git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"', {
|
|
6695
7097
|
cwd: this.bareRepoPath,
|
|
6696
7098
|
timeout: 5e3
|
|
6697
7099
|
});
|
|
@@ -6706,8 +7108,8 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6706
7108
|
*/
|
|
6707
7109
|
static async createProject(projectRoot, repoUrl, projectId, workspaceSlug, projectSlug) {
|
|
6708
7110
|
const manager = new _WorktreeManager(projectRoot);
|
|
6709
|
-
const episodaDir =
|
|
6710
|
-
|
|
7111
|
+
const episodaDir = path16.join(projectRoot, ".episoda");
|
|
7112
|
+
fs15.mkdirSync(episodaDir, { recursive: true });
|
|
6711
7113
|
const cloneResult = await manager.gitExecutor.execute({
|
|
6712
7114
|
action: "clone_bare",
|
|
6713
7115
|
url: repoUrl,
|
|
@@ -6738,7 +7140,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6738
7140
|
error: `Invalid module UID: "${moduleUid}" - contains disallowed characters`
|
|
6739
7141
|
};
|
|
6740
7142
|
}
|
|
6741
|
-
const worktreePath =
|
|
7143
|
+
const worktreePath = path16.join(this.projectRoot, moduleUid);
|
|
6742
7144
|
const lockAcquired = await this.acquireLock();
|
|
6743
7145
|
if (!lockAcquired) {
|
|
6744
7146
|
return {
|
|
@@ -6838,14 +7240,21 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6838
7240
|
error: result.output || "Failed to remove worktree"
|
|
6839
7241
|
};
|
|
6840
7242
|
}
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
config
|
|
6844
|
-
|
|
7243
|
+
let configUpdated = false;
|
|
7244
|
+
try {
|
|
7245
|
+
const config = this.readConfig();
|
|
7246
|
+
if (config) {
|
|
7247
|
+
config.worktrees = config.worktrees.filter((w) => w.moduleUid !== moduleUid);
|
|
7248
|
+
this.writeConfig(config);
|
|
7249
|
+
configUpdated = true;
|
|
7250
|
+
}
|
|
7251
|
+
} catch (configError) {
|
|
7252
|
+
console.warn(`[WorktreeManager] EP1035: Config update failed after removing ${moduleUid} (non-blocking):`, configError.message);
|
|
6845
7253
|
}
|
|
6846
7254
|
return {
|
|
6847
7255
|
success: true,
|
|
6848
|
-
worktreePath: existing.worktreePath
|
|
7256
|
+
worktreePath: existing.worktreePath,
|
|
7257
|
+
configUpdated
|
|
6849
7258
|
};
|
|
6850
7259
|
} finally {
|
|
6851
7260
|
this.releaseLock();
|
|
@@ -6920,7 +7329,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6920
7329
|
let prunedCount = 0;
|
|
6921
7330
|
await this.updateConfigSafe((config) => {
|
|
6922
7331
|
const initialCount = config.worktrees.length;
|
|
6923
|
-
config.worktrees = config.worktrees.filter((w) =>
|
|
7332
|
+
config.worktrees = config.worktrees.filter((w) => fs15.existsSync(w.worktreePath));
|
|
6924
7333
|
prunedCount = initialCount - config.worktrees.length;
|
|
6925
7334
|
return config;
|
|
6926
7335
|
});
|
|
@@ -7001,16 +7410,16 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7001
7410
|
const retryInterval = 50;
|
|
7002
7411
|
while (Date.now() - startTime < timeoutMs) {
|
|
7003
7412
|
try {
|
|
7004
|
-
|
|
7413
|
+
fs15.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
7005
7414
|
return true;
|
|
7006
7415
|
} catch (err) {
|
|
7007
7416
|
if (err.code === "EEXIST") {
|
|
7008
7417
|
try {
|
|
7009
|
-
const stats =
|
|
7418
|
+
const stats = fs15.statSync(lockPath);
|
|
7010
7419
|
const lockAge = Date.now() - stats.mtimeMs;
|
|
7011
7420
|
if (lockAge > 3e4) {
|
|
7012
7421
|
try {
|
|
7013
|
-
const lockContent =
|
|
7422
|
+
const lockContent = fs15.readFileSync(lockPath, "utf-8").trim();
|
|
7014
7423
|
const lockPid = parseInt(lockContent, 10);
|
|
7015
7424
|
if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
|
|
7016
7425
|
await new Promise((resolve3) => setTimeout(resolve3, retryInterval));
|
|
@@ -7019,7 +7428,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7019
7428
|
} catch {
|
|
7020
7429
|
}
|
|
7021
7430
|
try {
|
|
7022
|
-
|
|
7431
|
+
fs15.unlinkSync(lockPath);
|
|
7023
7432
|
} catch {
|
|
7024
7433
|
}
|
|
7025
7434
|
continue;
|
|
@@ -7040,29 +7449,34 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7040
7449
|
*/
|
|
7041
7450
|
releaseLock() {
|
|
7042
7451
|
try {
|
|
7043
|
-
|
|
7452
|
+
fs15.unlinkSync(this.getLockPath());
|
|
7044
7453
|
} catch {
|
|
7045
7454
|
}
|
|
7046
7455
|
}
|
|
7047
7456
|
readConfig() {
|
|
7048
7457
|
try {
|
|
7049
|
-
if (!
|
|
7458
|
+
if (!fs15.existsSync(this.configPath)) {
|
|
7050
7459
|
return null;
|
|
7051
7460
|
}
|
|
7052
|
-
const content =
|
|
7461
|
+
const content = fs15.readFileSync(this.configPath, "utf-8");
|
|
7053
7462
|
return JSON.parse(content);
|
|
7054
7463
|
} catch (error) {
|
|
7055
|
-
|
|
7464
|
+
if (error instanceof SyntaxError) {
|
|
7465
|
+
console.warn(`[WorktreeManager] EP1035: Config file corrupted at ${this.configPath} - JSON parse error:`, error.message);
|
|
7466
|
+
console.warn("[WorktreeManager] EP1035: Config will be recovered on next pruneStaleWorktrees call");
|
|
7467
|
+
} else {
|
|
7468
|
+
console.error("[WorktreeManager] Failed to read config:", error.message);
|
|
7469
|
+
}
|
|
7056
7470
|
return null;
|
|
7057
7471
|
}
|
|
7058
7472
|
}
|
|
7059
7473
|
writeConfig(config) {
|
|
7060
7474
|
try {
|
|
7061
|
-
const dir =
|
|
7062
|
-
if (!
|
|
7063
|
-
|
|
7475
|
+
const dir = path16.dirname(this.configPath);
|
|
7476
|
+
if (!fs15.existsSync(dir)) {
|
|
7477
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
7064
7478
|
}
|
|
7065
|
-
|
|
7479
|
+
fs15.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
7066
7480
|
} catch (error) {
|
|
7067
7481
|
console.error("[WorktreeManager] Failed to write config:", error);
|
|
7068
7482
|
throw error;
|
|
@@ -7143,14 +7557,14 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7143
7557
|
}
|
|
7144
7558
|
try {
|
|
7145
7559
|
for (const file of files) {
|
|
7146
|
-
const srcPath =
|
|
7147
|
-
const destPath =
|
|
7148
|
-
if (
|
|
7149
|
-
const destDir =
|
|
7150
|
-
if (!
|
|
7151
|
-
|
|
7152
|
-
}
|
|
7153
|
-
|
|
7560
|
+
const srcPath = path16.join(mainWorktree.worktreePath, file);
|
|
7561
|
+
const destPath = path16.join(worktree.worktreePath, file);
|
|
7562
|
+
if (fs15.existsSync(srcPath)) {
|
|
7563
|
+
const destDir = path16.dirname(destPath);
|
|
7564
|
+
if (!fs15.existsSync(destDir)) {
|
|
7565
|
+
fs15.mkdirSync(destDir, { recursive: true });
|
|
7566
|
+
}
|
|
7567
|
+
fs15.copyFileSync(srcPath, destPath);
|
|
7154
7568
|
console.log(`[WorktreeManager] EP964: Copied ${file} to ${moduleUid} (deprecated)`);
|
|
7155
7569
|
} else {
|
|
7156
7570
|
console.log(`[WorktreeManager] EP964: Skipped ${file} (not found in main)`);
|
|
@@ -7181,8 +7595,8 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7181
7595
|
console.log(`[WorktreeManager] EP959: Timeout: ${TIMEOUT_MINUTES} minutes`);
|
|
7182
7596
|
console.log(`[WorktreeManager] EP959: Script: ${scriptPreview}`);
|
|
7183
7597
|
try {
|
|
7184
|
-
const { execSync:
|
|
7185
|
-
|
|
7598
|
+
const { execSync: execSync9 } = require("child_process");
|
|
7599
|
+
execSync9(script, {
|
|
7186
7600
|
cwd: worktree.worktreePath,
|
|
7187
7601
|
stdio: "inherit",
|
|
7188
7602
|
timeout: TIMEOUT_MINUTES * 60 * 1e3,
|
|
@@ -7216,8 +7630,8 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7216
7630
|
console.log(`[WorktreeManager] EP959: Timeout: ${TIMEOUT_MINUTES} minutes`);
|
|
7217
7631
|
console.log(`[WorktreeManager] EP959: Script: ${scriptPreview}`);
|
|
7218
7632
|
try {
|
|
7219
|
-
const { execSync:
|
|
7220
|
-
|
|
7633
|
+
const { execSync: execSync9 } = require("child_process");
|
|
7634
|
+
execSync9(script, {
|
|
7221
7635
|
cwd: worktree.worktreePath,
|
|
7222
7636
|
stdio: "inherit",
|
|
7223
7637
|
timeout: TIMEOUT_MINUTES * 60 * 1e3,
|
|
@@ -7233,27 +7647,27 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7233
7647
|
}
|
|
7234
7648
|
};
|
|
7235
7649
|
function getEpisodaRoot() {
|
|
7236
|
-
return process.env.EPISODA_ROOT ||
|
|
7650
|
+
return process.env.EPISODA_ROOT || path16.join(require("os").homedir(), "episoda");
|
|
7237
7651
|
}
|
|
7238
7652
|
async function isWorktreeProject(projectRoot) {
|
|
7239
7653
|
const manager = new WorktreeManager(projectRoot);
|
|
7240
7654
|
return manager.initialize();
|
|
7241
7655
|
}
|
|
7242
7656
|
async function findProjectRoot(startPath) {
|
|
7243
|
-
let current =
|
|
7657
|
+
let current = path16.resolve(startPath);
|
|
7244
7658
|
const episodaRoot = getEpisodaRoot();
|
|
7245
7659
|
if (!current.startsWith(episodaRoot)) {
|
|
7246
7660
|
return null;
|
|
7247
7661
|
}
|
|
7248
7662
|
for (let i = 0; i < 10; i++) {
|
|
7249
|
-
const bareDir =
|
|
7250
|
-
const episodaDir =
|
|
7251
|
-
if (
|
|
7663
|
+
const bareDir = path16.join(current, ".bare");
|
|
7664
|
+
const episodaDir = path16.join(current, ".episoda");
|
|
7665
|
+
if (fs15.existsSync(bareDir) && fs15.existsSync(episodaDir)) {
|
|
7252
7666
|
if (await isWorktreeProject(current)) {
|
|
7253
7667
|
return current;
|
|
7254
7668
|
}
|
|
7255
7669
|
}
|
|
7256
|
-
const parent =
|
|
7670
|
+
const parent = path16.dirname(current);
|
|
7257
7671
|
if (parent === current) {
|
|
7258
7672
|
break;
|
|
7259
7673
|
}
|
|
@@ -7263,19 +7677,19 @@ async function findProjectRoot(startPath) {
|
|
|
7263
7677
|
}
|
|
7264
7678
|
|
|
7265
7679
|
// src/utils/worktree.ts
|
|
7266
|
-
var
|
|
7267
|
-
var
|
|
7268
|
-
var
|
|
7680
|
+
var path17 = __toESM(require("path"));
|
|
7681
|
+
var fs16 = __toESM(require("fs"));
|
|
7682
|
+
var os7 = __toESM(require("os"));
|
|
7269
7683
|
var import_core11 = __toESM(require_dist());
|
|
7270
7684
|
function getEpisodaRoot2() {
|
|
7271
|
-
return process.env.EPISODA_ROOT ||
|
|
7685
|
+
return process.env.EPISODA_ROOT || path17.join(os7.homedir(), "episoda");
|
|
7272
7686
|
}
|
|
7273
7687
|
function getWorktreeInfo(moduleUid, workspaceSlug, projectSlug) {
|
|
7274
7688
|
const root = getEpisodaRoot2();
|
|
7275
|
-
const worktreePath =
|
|
7689
|
+
const worktreePath = path17.join(root, workspaceSlug, projectSlug, moduleUid);
|
|
7276
7690
|
return {
|
|
7277
7691
|
path: worktreePath,
|
|
7278
|
-
exists:
|
|
7692
|
+
exists: fs16.existsSync(worktreePath),
|
|
7279
7693
|
moduleUid
|
|
7280
7694
|
};
|
|
7281
7695
|
}
|
|
@@ -7289,61 +7703,61 @@ async function getWorktreeInfoForModule(moduleUid) {
|
|
|
7289
7703
|
}
|
|
7290
7704
|
|
|
7291
7705
|
// src/framework-detector.ts
|
|
7292
|
-
var
|
|
7293
|
-
var
|
|
7706
|
+
var fs17 = __toESM(require("fs"));
|
|
7707
|
+
var path18 = __toESM(require("path"));
|
|
7294
7708
|
function getInstallCommand(cwd) {
|
|
7295
|
-
if (
|
|
7709
|
+
if (fs17.existsSync(path18.join(cwd, "bun.lockb"))) {
|
|
7296
7710
|
return {
|
|
7297
7711
|
command: ["bun", "install"],
|
|
7298
7712
|
description: "Installing dependencies with bun",
|
|
7299
7713
|
detectedFrom: "bun.lockb"
|
|
7300
7714
|
};
|
|
7301
7715
|
}
|
|
7302
|
-
if (
|
|
7716
|
+
if (fs17.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) {
|
|
7303
7717
|
return {
|
|
7304
7718
|
command: ["pnpm", "install"],
|
|
7305
7719
|
description: "Installing dependencies with pnpm",
|
|
7306
7720
|
detectedFrom: "pnpm-lock.yaml"
|
|
7307
7721
|
};
|
|
7308
7722
|
}
|
|
7309
|
-
if (
|
|
7723
|
+
if (fs17.existsSync(path18.join(cwd, "yarn.lock"))) {
|
|
7310
7724
|
return {
|
|
7311
7725
|
command: ["yarn", "install"],
|
|
7312
7726
|
description: "Installing dependencies with yarn",
|
|
7313
7727
|
detectedFrom: "yarn.lock"
|
|
7314
7728
|
};
|
|
7315
7729
|
}
|
|
7316
|
-
if (
|
|
7730
|
+
if (fs17.existsSync(path18.join(cwd, "package-lock.json"))) {
|
|
7317
7731
|
return {
|
|
7318
7732
|
command: ["npm", "ci"],
|
|
7319
7733
|
description: "Installing dependencies with npm ci",
|
|
7320
7734
|
detectedFrom: "package-lock.json"
|
|
7321
7735
|
};
|
|
7322
7736
|
}
|
|
7323
|
-
if (
|
|
7737
|
+
if (fs17.existsSync(path18.join(cwd, "package.json"))) {
|
|
7324
7738
|
return {
|
|
7325
7739
|
command: ["npm", "install"],
|
|
7326
7740
|
description: "Installing dependencies with npm",
|
|
7327
7741
|
detectedFrom: "package.json"
|
|
7328
7742
|
};
|
|
7329
7743
|
}
|
|
7330
|
-
if (
|
|
7744
|
+
if (fs17.existsSync(path18.join(cwd, "Pipfile.lock")) || fs17.existsSync(path18.join(cwd, "Pipfile"))) {
|
|
7331
7745
|
return {
|
|
7332
7746
|
command: ["pipenv", "install"],
|
|
7333
7747
|
description: "Installing dependencies with pipenv",
|
|
7334
|
-
detectedFrom:
|
|
7748
|
+
detectedFrom: fs17.existsSync(path18.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
|
|
7335
7749
|
};
|
|
7336
7750
|
}
|
|
7337
|
-
if (
|
|
7751
|
+
if (fs17.existsSync(path18.join(cwd, "poetry.lock"))) {
|
|
7338
7752
|
return {
|
|
7339
7753
|
command: ["poetry", "install"],
|
|
7340
7754
|
description: "Installing dependencies with poetry",
|
|
7341
7755
|
detectedFrom: "poetry.lock"
|
|
7342
7756
|
};
|
|
7343
7757
|
}
|
|
7344
|
-
if (
|
|
7345
|
-
const pyprojectPath =
|
|
7346
|
-
const content =
|
|
7758
|
+
if (fs17.existsSync(path18.join(cwd, "pyproject.toml"))) {
|
|
7759
|
+
const pyprojectPath = path18.join(cwd, "pyproject.toml");
|
|
7760
|
+
const content = fs17.readFileSync(pyprojectPath, "utf-8");
|
|
7347
7761
|
if (content.includes("[tool.poetry]")) {
|
|
7348
7762
|
return {
|
|
7349
7763
|
command: ["poetry", "install"],
|
|
@@ -7352,41 +7766,41 @@ function getInstallCommand(cwd) {
|
|
|
7352
7766
|
};
|
|
7353
7767
|
}
|
|
7354
7768
|
}
|
|
7355
|
-
if (
|
|
7769
|
+
if (fs17.existsSync(path18.join(cwd, "requirements.txt"))) {
|
|
7356
7770
|
return {
|
|
7357
7771
|
command: ["pip", "install", "-r", "requirements.txt"],
|
|
7358
7772
|
description: "Installing dependencies with pip",
|
|
7359
7773
|
detectedFrom: "requirements.txt"
|
|
7360
7774
|
};
|
|
7361
7775
|
}
|
|
7362
|
-
if (
|
|
7776
|
+
if (fs17.existsSync(path18.join(cwd, "Gemfile.lock")) || fs17.existsSync(path18.join(cwd, "Gemfile"))) {
|
|
7363
7777
|
return {
|
|
7364
7778
|
command: ["bundle", "install"],
|
|
7365
7779
|
description: "Installing dependencies with bundler",
|
|
7366
|
-
detectedFrom:
|
|
7780
|
+
detectedFrom: fs17.existsSync(path18.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
|
|
7367
7781
|
};
|
|
7368
7782
|
}
|
|
7369
|
-
if (
|
|
7783
|
+
if (fs17.existsSync(path18.join(cwd, "go.sum")) || fs17.existsSync(path18.join(cwd, "go.mod"))) {
|
|
7370
7784
|
return {
|
|
7371
7785
|
command: ["go", "mod", "download"],
|
|
7372
7786
|
description: "Downloading Go modules",
|
|
7373
|
-
detectedFrom:
|
|
7787
|
+
detectedFrom: fs17.existsSync(path18.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
|
|
7374
7788
|
};
|
|
7375
7789
|
}
|
|
7376
|
-
if (
|
|
7790
|
+
if (fs17.existsSync(path18.join(cwd, "Cargo.lock")) || fs17.existsSync(path18.join(cwd, "Cargo.toml"))) {
|
|
7377
7791
|
return {
|
|
7378
7792
|
command: ["cargo", "build"],
|
|
7379
7793
|
description: "Building Rust project (downloads dependencies)",
|
|
7380
|
-
detectedFrom:
|
|
7794
|
+
detectedFrom: fs17.existsSync(path18.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
|
|
7381
7795
|
};
|
|
7382
7796
|
}
|
|
7383
7797
|
return null;
|
|
7384
7798
|
}
|
|
7385
7799
|
|
|
7386
7800
|
// src/daemon/daemon-process.ts
|
|
7387
|
-
var
|
|
7388
|
-
var
|
|
7389
|
-
var
|
|
7801
|
+
var fs18 = __toESM(require("fs"));
|
|
7802
|
+
var os8 = __toESM(require("os"));
|
|
7803
|
+
var path19 = __toESM(require("path"));
|
|
7390
7804
|
var packageJson = require_package();
|
|
7391
7805
|
async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
|
|
7392
7806
|
const now = Date.now();
|
|
@@ -7550,6 +7964,7 @@ var Daemon = class _Daemon {
|
|
|
7550
7964
|
this.registerIPCHandlers();
|
|
7551
7965
|
await this.restoreConnections();
|
|
7552
7966
|
await this.cleanupOrphanedTunnels();
|
|
7967
|
+
await this.cleanupOrphanedDevServers();
|
|
7553
7968
|
await this.auditWorktreesOnStartup();
|
|
7554
7969
|
this.startHealthCheckPolling();
|
|
7555
7970
|
this.setupShutdownHandlers();
|
|
@@ -7596,9 +8011,9 @@ var Daemon = class _Daemon {
|
|
|
7596
8011
|
machineId: this.machineId,
|
|
7597
8012
|
deviceId: this.deviceId,
|
|
7598
8013
|
// EP726: UUID for unified device identification
|
|
7599
|
-
hostname:
|
|
7600
|
-
platform:
|
|
7601
|
-
arch:
|
|
8014
|
+
hostname: os8.hostname(),
|
|
8015
|
+
platform: os8.platform(),
|
|
8016
|
+
arch: os8.arch(),
|
|
7602
8017
|
projects
|
|
7603
8018
|
};
|
|
7604
8019
|
});
|
|
@@ -7888,7 +8303,7 @@ var Daemon = class _Daemon {
|
|
|
7888
8303
|
client.updateActivity();
|
|
7889
8304
|
try {
|
|
7890
8305
|
const gitCmd = message.command;
|
|
7891
|
-
const bareRepoPath =
|
|
8306
|
+
const bareRepoPath = path19.join(projectPath, ".bare");
|
|
7892
8307
|
const cwd = gitCmd.worktreePath || bareRepoPath;
|
|
7893
8308
|
if (gitCmd.worktreePath) {
|
|
7894
8309
|
console.log(`[Daemon] Routing command to worktree: ${gitCmd.worktreePath}`);
|
|
@@ -8217,6 +8632,78 @@ var Daemon = class _Daemon {
|
|
|
8217
8632
|
}
|
|
8218
8633
|
}
|
|
8219
8634
|
});
|
|
8635
|
+
client.on("worktree_cleanup_command", async (message) => {
|
|
8636
|
+
if (message.type === "worktree_cleanup_command" && message.command) {
|
|
8637
|
+
const cmd = message.command;
|
|
8638
|
+
console.log(`[Daemon] EP1035: Received worktree cleanup command for ${cmd.moduleUid}`);
|
|
8639
|
+
client.updateActivity();
|
|
8640
|
+
let result;
|
|
8641
|
+
try {
|
|
8642
|
+
const projectRootPath = await findProjectRoot(cmd.worktreePath);
|
|
8643
|
+
if (!projectRootPath) {
|
|
8644
|
+
console.warn(`[Daemon] EP1035: Cannot find project root for ${cmd.worktreePath}`);
|
|
8645
|
+
result = {
|
|
8646
|
+
success: false,
|
|
8647
|
+
error: `Cannot find project root for worktree path: ${cmd.worktreePath}`
|
|
8648
|
+
};
|
|
8649
|
+
} else {
|
|
8650
|
+
const manager = new WorktreeManager(projectRootPath);
|
|
8651
|
+
const initialized2 = await manager.initialize();
|
|
8652
|
+
if (!initialized2) {
|
|
8653
|
+
console.warn(`[Daemon] EP1035: Failed to initialize WorktreeManager for ${projectRootPath}`);
|
|
8654
|
+
result = {
|
|
8655
|
+
success: false,
|
|
8656
|
+
error: `Failed to initialize WorktreeManager for project: ${projectRootPath}`
|
|
8657
|
+
};
|
|
8658
|
+
} else {
|
|
8659
|
+
const removeResult = await manager.removeWorktree(cmd.moduleUid, cmd.force || false);
|
|
8660
|
+
if (removeResult.success) {
|
|
8661
|
+
if (removeResult.configUpdated === false) {
|
|
8662
|
+
console.warn(`[Daemon] EP1035: Worktree ${cmd.moduleUid} removed but config update failed (will sync on next prune)`);
|
|
8663
|
+
} else {
|
|
8664
|
+
console.log(`[Daemon] EP1035: Successfully cleaned up worktree for ${cmd.moduleUid}`);
|
|
8665
|
+
}
|
|
8666
|
+
result = {
|
|
8667
|
+
success: true,
|
|
8668
|
+
worktreePath: removeResult.worktreePath
|
|
8669
|
+
};
|
|
8670
|
+
} else {
|
|
8671
|
+
if (removeResult.error?.includes("not found") || removeResult.error?.includes("No worktree found")) {
|
|
8672
|
+
console.log(`[Daemon] EP1035: Worktree for ${cmd.moduleUid} already removed, syncing config`);
|
|
8673
|
+
await manager.pruneStaleWorktrees();
|
|
8674
|
+
result = {
|
|
8675
|
+
success: true,
|
|
8676
|
+
worktreePath: cmd.worktreePath
|
|
8677
|
+
};
|
|
8678
|
+
} else {
|
|
8679
|
+
console.error(`[Daemon] EP1035: Failed to remove worktree for ${cmd.moduleUid}: ${removeResult.error}`);
|
|
8680
|
+
result = {
|
|
8681
|
+
success: false,
|
|
8682
|
+
error: removeResult.error
|
|
8683
|
+
};
|
|
8684
|
+
}
|
|
8685
|
+
}
|
|
8686
|
+
}
|
|
8687
|
+
}
|
|
8688
|
+
} catch (error) {
|
|
8689
|
+
console.error(`[Daemon] EP1035: Worktree cleanup error for ${cmd.moduleUid}:`, error);
|
|
8690
|
+
result = {
|
|
8691
|
+
success: false,
|
|
8692
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8693
|
+
};
|
|
8694
|
+
}
|
|
8695
|
+
try {
|
|
8696
|
+
await client.send({
|
|
8697
|
+
type: "worktree_cleanup_result",
|
|
8698
|
+
commandId: message.id,
|
|
8699
|
+
result
|
|
8700
|
+
});
|
|
8701
|
+
} catch (sendError) {
|
|
8702
|
+
console.error(`[Daemon] EP1035: Failed to send cleanup result (WebSocket may be disconnected):`, sendError);
|
|
8703
|
+
}
|
|
8704
|
+
console.log(`[Daemon] EP1035: Worktree cleanup for ${cmd.moduleUid} completed:`, result.success ? "success" : "failed");
|
|
8705
|
+
}
|
|
8706
|
+
});
|
|
8220
8707
|
client.on("shutdown", async (message) => {
|
|
8221
8708
|
const shutdownMessage = message;
|
|
8222
8709
|
const reason = shutdownMessage.reason || "unknown";
|
|
@@ -8304,8 +8791,8 @@ var Daemon = class _Daemon {
|
|
|
8304
8791
|
let daemonPid;
|
|
8305
8792
|
try {
|
|
8306
8793
|
const pidPath = getPidFilePath();
|
|
8307
|
-
if (
|
|
8308
|
-
const pidStr =
|
|
8794
|
+
if (fs18.existsSync(pidPath)) {
|
|
8795
|
+
const pidStr = fs18.readFileSync(pidPath, "utf-8").trim();
|
|
8309
8796
|
daemonPid = parseInt(pidStr, 10);
|
|
8310
8797
|
}
|
|
8311
8798
|
} catch (pidError) {
|
|
@@ -8329,9 +8816,9 @@ var Daemon = class _Daemon {
|
|
|
8329
8816
|
client.once("auth_error", errorHandler);
|
|
8330
8817
|
});
|
|
8331
8818
|
await client.connect(wsUrl, config.access_token, this.machineId, {
|
|
8332
|
-
hostname:
|
|
8333
|
-
osPlatform:
|
|
8334
|
-
osArch:
|
|
8819
|
+
hostname: os8.hostname(),
|
|
8820
|
+
osPlatform: os8.platform(),
|
|
8821
|
+
osArch: os8.arch(),
|
|
8335
8822
|
daemonPid
|
|
8336
8823
|
});
|
|
8337
8824
|
console.log(`[Daemon] Successfully connected to project ${projectId}`);
|
|
@@ -8384,29 +8871,29 @@ var Daemon = class _Daemon {
|
|
|
8384
8871
|
*/
|
|
8385
8872
|
async configureGitUser(projectPath, userId, workspaceId, machineId, projectId, deviceId) {
|
|
8386
8873
|
try {
|
|
8387
|
-
const { execSync:
|
|
8388
|
-
|
|
8874
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
8875
|
+
execSync9(`git config episoda.userId ${userId}`, {
|
|
8389
8876
|
cwd: projectPath,
|
|
8390
8877
|
encoding: "utf8",
|
|
8391
8878
|
stdio: "pipe"
|
|
8392
8879
|
});
|
|
8393
|
-
|
|
8880
|
+
execSync9(`git config episoda.workspaceId ${workspaceId}`, {
|
|
8394
8881
|
cwd: projectPath,
|
|
8395
8882
|
encoding: "utf8",
|
|
8396
8883
|
stdio: "pipe"
|
|
8397
8884
|
});
|
|
8398
|
-
|
|
8885
|
+
execSync9(`git config episoda.machineId ${machineId}`, {
|
|
8399
8886
|
cwd: projectPath,
|
|
8400
8887
|
encoding: "utf8",
|
|
8401
8888
|
stdio: "pipe"
|
|
8402
8889
|
});
|
|
8403
|
-
|
|
8890
|
+
execSync9(`git config episoda.projectId ${projectId}`, {
|
|
8404
8891
|
cwd: projectPath,
|
|
8405
8892
|
encoding: "utf8",
|
|
8406
8893
|
stdio: "pipe"
|
|
8407
8894
|
});
|
|
8408
8895
|
if (deviceId) {
|
|
8409
|
-
|
|
8896
|
+
execSync9(`git config episoda.deviceId ${deviceId}`, {
|
|
8410
8897
|
cwd: projectPath,
|
|
8411
8898
|
encoding: "utf8",
|
|
8412
8899
|
stdio: "pipe"
|
|
@@ -8426,27 +8913,27 @@ var Daemon = class _Daemon {
|
|
|
8426
8913
|
*/
|
|
8427
8914
|
async installGitHooks(projectPath) {
|
|
8428
8915
|
const hooks = ["post-checkout", "pre-commit", "post-commit"];
|
|
8429
|
-
const hooksDir =
|
|
8430
|
-
if (!
|
|
8916
|
+
const hooksDir = path19.join(projectPath, ".git", "hooks");
|
|
8917
|
+
if (!fs18.existsSync(hooksDir)) {
|
|
8431
8918
|
console.warn(`[Daemon] Hooks directory not found: ${hooksDir}`);
|
|
8432
8919
|
return;
|
|
8433
8920
|
}
|
|
8434
8921
|
for (const hookName of hooks) {
|
|
8435
8922
|
try {
|
|
8436
|
-
const hookPath =
|
|
8437
|
-
const bundledHookPath =
|
|
8438
|
-
if (!
|
|
8923
|
+
const hookPath = path19.join(hooksDir, hookName);
|
|
8924
|
+
const bundledHookPath = path19.join(__dirname, "..", "hooks", hookName);
|
|
8925
|
+
if (!fs18.existsSync(bundledHookPath)) {
|
|
8439
8926
|
console.warn(`[Daemon] Bundled hook not found: ${bundledHookPath}`);
|
|
8440
8927
|
continue;
|
|
8441
8928
|
}
|
|
8442
|
-
const hookContent =
|
|
8443
|
-
if (
|
|
8444
|
-
const existingContent =
|
|
8929
|
+
const hookContent = fs18.readFileSync(bundledHookPath, "utf-8");
|
|
8930
|
+
if (fs18.existsSync(hookPath)) {
|
|
8931
|
+
const existingContent = fs18.readFileSync(hookPath, "utf-8");
|
|
8445
8932
|
if (existingContent === hookContent) {
|
|
8446
8933
|
continue;
|
|
8447
8934
|
}
|
|
8448
8935
|
}
|
|
8449
|
-
|
|
8936
|
+
fs18.writeFileSync(hookPath, hookContent, { mode: 493 });
|
|
8450
8937
|
console.log(`[Daemon] Installed git hook: ${hookName}`);
|
|
8451
8938
|
} catch (error) {
|
|
8452
8939
|
console.warn(`[Daemon] Failed to install ${hookName} hook:`, error instanceof Error ? error.message : error);
|
|
@@ -8762,8 +9249,8 @@ var Daemon = class _Daemon {
|
|
|
8762
9249
|
console.log(`[Daemon] EP1002: ${installCmd.description} (detected from ${installCmd.detectedFrom})`);
|
|
8763
9250
|
console.log(`[Daemon] EP1002: Running: ${installCmd.command.join(" ")}`);
|
|
8764
9251
|
try {
|
|
8765
|
-
const { execSync:
|
|
8766
|
-
|
|
9252
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
9253
|
+
execSync9(installCmd.command.join(" "), {
|
|
8767
9254
|
cwd: worktreePath,
|
|
8768
9255
|
stdio: "inherit",
|
|
8769
9256
|
timeout: 10 * 60 * 1e3,
|
|
@@ -8816,8 +9303,8 @@ var Daemon = class _Daemon {
|
|
|
8816
9303
|
console.log(`[Daemon] EP986: ${installCmd.description} (detected from ${installCmd.detectedFrom})`);
|
|
8817
9304
|
console.log(`[Daemon] EP986: Running: ${installCmd.command.join(" ")}`);
|
|
8818
9305
|
try {
|
|
8819
|
-
const { execSync:
|
|
8820
|
-
|
|
9306
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
9307
|
+
execSync9(installCmd.command.join(" "), {
|
|
8821
9308
|
cwd: worktreePath,
|
|
8822
9309
|
stdio: "inherit",
|
|
8823
9310
|
timeout: 10 * 60 * 1e3,
|
|
@@ -8951,6 +9438,65 @@ var Daemon = class _Daemon {
|
|
|
8951
9438
|
console.error("[Daemon] EP904: Failed to clean up orphaned tunnels:", error);
|
|
8952
9439
|
}
|
|
8953
9440
|
}
|
|
9441
|
+
/**
|
|
9442
|
+
* EP1042: Clean up orphaned dev servers from previous daemon runs
|
|
9443
|
+
*
|
|
9444
|
+
* When the daemon crashes or is killed, dev servers may continue running.
|
|
9445
|
+
* This method:
|
|
9446
|
+
* 1. Queries the DevServerRegistry for tracked dev servers
|
|
9447
|
+
* 2. Queries API for modules in doing/review state
|
|
9448
|
+
* 3. Kills dev servers for modules NOT in doing/review
|
|
9449
|
+
* 4. Reconciles port allocator with registry
|
|
9450
|
+
*/
|
|
9451
|
+
async cleanupOrphanedDevServers() {
|
|
9452
|
+
try {
|
|
9453
|
+
const registry = getDevServerRegistry();
|
|
9454
|
+
const entries = registry.getAll();
|
|
9455
|
+
if (entries.length === 0) {
|
|
9456
|
+
console.log("[Daemon] EP1042: No registered dev servers to check");
|
|
9457
|
+
return;
|
|
9458
|
+
}
|
|
9459
|
+
console.log(`[Daemon] EP1042: Checking ${entries.length} registered dev server(s)...`);
|
|
9460
|
+
const activeModuleUids = [];
|
|
9461
|
+
const config = await (0, import_core12.loadConfig)();
|
|
9462
|
+
if (config?.access_token) {
|
|
9463
|
+
const apiUrl = config.api_url || "https://episoda.dev";
|
|
9464
|
+
try {
|
|
9465
|
+
const response = await fetch(`${apiUrl}/api/modules?state=doing,review&dev_mode=local`, {
|
|
9466
|
+
headers: {
|
|
9467
|
+
"Authorization": `Bearer ${config.access_token}`,
|
|
9468
|
+
"Content-Type": "application/json"
|
|
9469
|
+
}
|
|
9470
|
+
});
|
|
9471
|
+
if (response.ok) {
|
|
9472
|
+
const data = await response.json();
|
|
9473
|
+
if (data.modules && Array.isArray(data.modules)) {
|
|
9474
|
+
for (const module2 of data.modules) {
|
|
9475
|
+
if (module2.uid) {
|
|
9476
|
+
activeModuleUids.push(module2.uid);
|
|
9477
|
+
}
|
|
9478
|
+
}
|
|
9479
|
+
}
|
|
9480
|
+
}
|
|
9481
|
+
} catch (fetchError) {
|
|
9482
|
+
console.warn("[Daemon] EP1042: Failed to fetch active modules:", fetchError);
|
|
9483
|
+
}
|
|
9484
|
+
}
|
|
9485
|
+
console.log(`[Daemon] EP1042: Found ${activeModuleUids.length} active module(s) in doing/review state`);
|
|
9486
|
+
const cleanup = await registry.cleanupOrphans(activeModuleUids);
|
|
9487
|
+
if (cleanup.cleaned > 0) {
|
|
9488
|
+
console.log(`[Daemon] EP1042: Killed ${cleanup.cleaned} orphaned dev server(s)`);
|
|
9489
|
+
}
|
|
9490
|
+
const registryPorts = /* @__PURE__ */ new Map();
|
|
9491
|
+
for (const entry of registry.getAll()) {
|
|
9492
|
+
registryPorts.set(entry.moduleUid, entry.port);
|
|
9493
|
+
}
|
|
9494
|
+
reconcileWithRegistry(registryPorts);
|
|
9495
|
+
console.log("[Daemon] EP1042: Orphaned dev server cleanup complete");
|
|
9496
|
+
} catch (error) {
|
|
9497
|
+
console.error("[Daemon] EP1042: Failed to clean up orphaned dev servers:", error);
|
|
9498
|
+
}
|
|
9499
|
+
}
|
|
8954
9500
|
/**
|
|
8955
9501
|
* EP957: Audit worktrees on daemon startup to detect orphaned worktrees
|
|
8956
9502
|
*
|
|
@@ -8997,14 +9543,36 @@ var Daemon = class _Daemon {
|
|
|
8997
9543
|
}
|
|
8998
9544
|
const { orphaned } = manager.auditWorktrees(activeModuleUids);
|
|
8999
9545
|
if (orphaned.length > 0) {
|
|
9000
|
-
console.log(`[Daemon]
|
|
9546
|
+
console.log(`[Daemon] EP1035: Found ${orphaned.length} orphaned worktree(s) in ${config.workspaceSlug}/${config.projectSlug}:`);
|
|
9001
9547
|
for (const w of orphaned) {
|
|
9002
9548
|
console.log(` - ${w.moduleUid} (branch: ${w.branchName})`);
|
|
9003
9549
|
}
|
|
9004
|
-
console.log(
|
|
9550
|
+
console.log("[Daemon] EP1035: Auto-cleaning orphaned worktrees...");
|
|
9551
|
+
for (const w of orphaned) {
|
|
9552
|
+
try {
|
|
9553
|
+
const removeResult = await manager.removeWorktree(w.moduleUid, true);
|
|
9554
|
+
if (removeResult.success) {
|
|
9555
|
+
console.log(`[Daemon] EP1035: Cleaned up orphaned worktree ${w.moduleUid}`);
|
|
9556
|
+
} else {
|
|
9557
|
+
if (removeResult.error?.includes("not found") || removeResult.error?.includes("No worktree found")) {
|
|
9558
|
+
console.log(`[Daemon] EP1035: Worktree ${w.moduleUid} already removed, syncing config`);
|
|
9559
|
+
await manager.pruneStaleWorktrees();
|
|
9560
|
+
} else {
|
|
9561
|
+
console.warn(`[Daemon] EP1035: Failed to clean up ${w.moduleUid}: ${removeResult.error}`);
|
|
9562
|
+
}
|
|
9563
|
+
}
|
|
9564
|
+
} catch (cleanupError) {
|
|
9565
|
+
console.warn(`[Daemon] EP1035: Error cleaning up ${w.moduleUid}:`, cleanupError.message);
|
|
9566
|
+
}
|
|
9567
|
+
}
|
|
9568
|
+
try {
|
|
9569
|
+
await manager.pruneStaleWorktrees();
|
|
9570
|
+
} catch (pruneError) {
|
|
9571
|
+
console.warn("[Daemon] EP1035: Failed to prune stale worktrees:", pruneError.message);
|
|
9572
|
+
}
|
|
9005
9573
|
}
|
|
9006
9574
|
} catch (error) {
|
|
9007
|
-
console.warn(`[Daemon]
|
|
9575
|
+
console.warn(`[Daemon] EP1035: Failed to audit ${projectPath}:`, error);
|
|
9008
9576
|
}
|
|
9009
9577
|
}
|
|
9010
9578
|
/**
|
|
@@ -9072,9 +9640,20 @@ var Daemon = class _Daemon {
|
|
|
9072
9640
|
}
|
|
9073
9641
|
/**
|
|
9074
9642
|
* EP833: Check if a tunnel is healthy
|
|
9643
|
+
* EP1042: Now also verifies dev server ownership (correct worktree)
|
|
9075
9644
|
* Verifies both the tunnel URL and local dev server respond
|
|
9076
9645
|
*/
|
|
9077
9646
|
async checkTunnelHealth(tunnel) {
|
|
9647
|
+
const registry = getDevServerRegistry();
|
|
9648
|
+
const entry = registry.getByPort(tunnel.port);
|
|
9649
|
+
if (entry && entry.moduleUid !== tunnel.moduleUid) {
|
|
9650
|
+
console.log(`[Daemon] EP1042: Port ${tunnel.port} owned by ${entry.moduleUid}, not ${tunnel.moduleUid} - marking unhealthy`);
|
|
9651
|
+
return false;
|
|
9652
|
+
}
|
|
9653
|
+
if (!entry) {
|
|
9654
|
+
console.log(`[Daemon] EP1042: No registry entry for port ${tunnel.port} (module ${tunnel.moduleUid}) - marking unhealthy`);
|
|
9655
|
+
return false;
|
|
9656
|
+
}
|
|
9078
9657
|
try {
|
|
9079
9658
|
const controller = new AbortController();
|
|
9080
9659
|
const timeout = setTimeout(() => controller.abort(), _Daemon.HEALTH_CHECK_TIMEOUT_MS);
|
|
@@ -9309,8 +9888,8 @@ var Daemon = class _Daemon {
|
|
|
9309
9888
|
await this.shutdown();
|
|
9310
9889
|
try {
|
|
9311
9890
|
const pidPath = getPidFilePath();
|
|
9312
|
-
if (
|
|
9313
|
-
|
|
9891
|
+
if (fs18.existsSync(pidPath)) {
|
|
9892
|
+
fs18.unlinkSync(pidPath);
|
|
9314
9893
|
console.log("[Daemon] PID file cleaned up");
|
|
9315
9894
|
}
|
|
9316
9895
|
} catch (error) {
|