episoda 0.2.53 → 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.
|
@@ -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 {
|
|
@@ -6927,7 +7329,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
6927
7329
|
let prunedCount = 0;
|
|
6928
7330
|
await this.updateConfigSafe((config) => {
|
|
6929
7331
|
const initialCount = config.worktrees.length;
|
|
6930
|
-
config.worktrees = config.worktrees.filter((w) =>
|
|
7332
|
+
config.worktrees = config.worktrees.filter((w) => fs15.existsSync(w.worktreePath));
|
|
6931
7333
|
prunedCount = initialCount - config.worktrees.length;
|
|
6932
7334
|
return config;
|
|
6933
7335
|
});
|
|
@@ -7008,16 +7410,16 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7008
7410
|
const retryInterval = 50;
|
|
7009
7411
|
while (Date.now() - startTime < timeoutMs) {
|
|
7010
7412
|
try {
|
|
7011
|
-
|
|
7413
|
+
fs15.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
7012
7414
|
return true;
|
|
7013
7415
|
} catch (err) {
|
|
7014
7416
|
if (err.code === "EEXIST") {
|
|
7015
7417
|
try {
|
|
7016
|
-
const stats =
|
|
7418
|
+
const stats = fs15.statSync(lockPath);
|
|
7017
7419
|
const lockAge = Date.now() - stats.mtimeMs;
|
|
7018
7420
|
if (lockAge > 3e4) {
|
|
7019
7421
|
try {
|
|
7020
|
-
const lockContent =
|
|
7422
|
+
const lockContent = fs15.readFileSync(lockPath, "utf-8").trim();
|
|
7021
7423
|
const lockPid = parseInt(lockContent, 10);
|
|
7022
7424
|
if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
|
|
7023
7425
|
await new Promise((resolve3) => setTimeout(resolve3, retryInterval));
|
|
@@ -7026,7 +7428,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7026
7428
|
} catch {
|
|
7027
7429
|
}
|
|
7028
7430
|
try {
|
|
7029
|
-
|
|
7431
|
+
fs15.unlinkSync(lockPath);
|
|
7030
7432
|
} catch {
|
|
7031
7433
|
}
|
|
7032
7434
|
continue;
|
|
@@ -7047,16 +7449,16 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7047
7449
|
*/
|
|
7048
7450
|
releaseLock() {
|
|
7049
7451
|
try {
|
|
7050
|
-
|
|
7452
|
+
fs15.unlinkSync(this.getLockPath());
|
|
7051
7453
|
} catch {
|
|
7052
7454
|
}
|
|
7053
7455
|
}
|
|
7054
7456
|
readConfig() {
|
|
7055
7457
|
try {
|
|
7056
|
-
if (!
|
|
7458
|
+
if (!fs15.existsSync(this.configPath)) {
|
|
7057
7459
|
return null;
|
|
7058
7460
|
}
|
|
7059
|
-
const content =
|
|
7461
|
+
const content = fs15.readFileSync(this.configPath, "utf-8");
|
|
7060
7462
|
return JSON.parse(content);
|
|
7061
7463
|
} catch (error) {
|
|
7062
7464
|
if (error instanceof SyntaxError) {
|
|
@@ -7070,11 +7472,11 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7070
7472
|
}
|
|
7071
7473
|
writeConfig(config) {
|
|
7072
7474
|
try {
|
|
7073
|
-
const dir =
|
|
7074
|
-
if (!
|
|
7075
|
-
|
|
7475
|
+
const dir = path16.dirname(this.configPath);
|
|
7476
|
+
if (!fs15.existsSync(dir)) {
|
|
7477
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
7076
7478
|
}
|
|
7077
|
-
|
|
7479
|
+
fs15.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
7078
7480
|
} catch (error) {
|
|
7079
7481
|
console.error("[WorktreeManager] Failed to write config:", error);
|
|
7080
7482
|
throw error;
|
|
@@ -7155,14 +7557,14 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7155
7557
|
}
|
|
7156
7558
|
try {
|
|
7157
7559
|
for (const file of files) {
|
|
7158
|
-
const srcPath =
|
|
7159
|
-
const destPath =
|
|
7160
|
-
if (
|
|
7161
|
-
const destDir =
|
|
7162
|
-
if (!
|
|
7163
|
-
|
|
7164
|
-
}
|
|
7165
|
-
|
|
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);
|
|
7166
7568
|
console.log(`[WorktreeManager] EP964: Copied ${file} to ${moduleUid} (deprecated)`);
|
|
7167
7569
|
} else {
|
|
7168
7570
|
console.log(`[WorktreeManager] EP964: Skipped ${file} (not found in main)`);
|
|
@@ -7193,8 +7595,8 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7193
7595
|
console.log(`[WorktreeManager] EP959: Timeout: ${TIMEOUT_MINUTES} minutes`);
|
|
7194
7596
|
console.log(`[WorktreeManager] EP959: Script: ${scriptPreview}`);
|
|
7195
7597
|
try {
|
|
7196
|
-
const { execSync:
|
|
7197
|
-
|
|
7598
|
+
const { execSync: execSync9 } = require("child_process");
|
|
7599
|
+
execSync9(script, {
|
|
7198
7600
|
cwd: worktree.worktreePath,
|
|
7199
7601
|
stdio: "inherit",
|
|
7200
7602
|
timeout: TIMEOUT_MINUTES * 60 * 1e3,
|
|
@@ -7228,8 +7630,8 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7228
7630
|
console.log(`[WorktreeManager] EP959: Timeout: ${TIMEOUT_MINUTES} minutes`);
|
|
7229
7631
|
console.log(`[WorktreeManager] EP959: Script: ${scriptPreview}`);
|
|
7230
7632
|
try {
|
|
7231
|
-
const { execSync:
|
|
7232
|
-
|
|
7633
|
+
const { execSync: execSync9 } = require("child_process");
|
|
7634
|
+
execSync9(script, {
|
|
7233
7635
|
cwd: worktree.worktreePath,
|
|
7234
7636
|
stdio: "inherit",
|
|
7235
7637
|
timeout: TIMEOUT_MINUTES * 60 * 1e3,
|
|
@@ -7245,27 +7647,27 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
7245
7647
|
}
|
|
7246
7648
|
};
|
|
7247
7649
|
function getEpisodaRoot() {
|
|
7248
|
-
return process.env.EPISODA_ROOT ||
|
|
7650
|
+
return process.env.EPISODA_ROOT || path16.join(require("os").homedir(), "episoda");
|
|
7249
7651
|
}
|
|
7250
7652
|
async function isWorktreeProject(projectRoot) {
|
|
7251
7653
|
const manager = new WorktreeManager(projectRoot);
|
|
7252
7654
|
return manager.initialize();
|
|
7253
7655
|
}
|
|
7254
7656
|
async function findProjectRoot(startPath) {
|
|
7255
|
-
let current =
|
|
7657
|
+
let current = path16.resolve(startPath);
|
|
7256
7658
|
const episodaRoot = getEpisodaRoot();
|
|
7257
7659
|
if (!current.startsWith(episodaRoot)) {
|
|
7258
7660
|
return null;
|
|
7259
7661
|
}
|
|
7260
7662
|
for (let i = 0; i < 10; i++) {
|
|
7261
|
-
const bareDir =
|
|
7262
|
-
const episodaDir =
|
|
7263
|
-
if (
|
|
7663
|
+
const bareDir = path16.join(current, ".bare");
|
|
7664
|
+
const episodaDir = path16.join(current, ".episoda");
|
|
7665
|
+
if (fs15.existsSync(bareDir) && fs15.existsSync(episodaDir)) {
|
|
7264
7666
|
if (await isWorktreeProject(current)) {
|
|
7265
7667
|
return current;
|
|
7266
7668
|
}
|
|
7267
7669
|
}
|
|
7268
|
-
const parent =
|
|
7670
|
+
const parent = path16.dirname(current);
|
|
7269
7671
|
if (parent === current) {
|
|
7270
7672
|
break;
|
|
7271
7673
|
}
|
|
@@ -7275,19 +7677,19 @@ async function findProjectRoot(startPath) {
|
|
|
7275
7677
|
}
|
|
7276
7678
|
|
|
7277
7679
|
// src/utils/worktree.ts
|
|
7278
|
-
var
|
|
7279
|
-
var
|
|
7280
|
-
var
|
|
7680
|
+
var path17 = __toESM(require("path"));
|
|
7681
|
+
var fs16 = __toESM(require("fs"));
|
|
7682
|
+
var os7 = __toESM(require("os"));
|
|
7281
7683
|
var import_core11 = __toESM(require_dist());
|
|
7282
7684
|
function getEpisodaRoot2() {
|
|
7283
|
-
return process.env.EPISODA_ROOT ||
|
|
7685
|
+
return process.env.EPISODA_ROOT || path17.join(os7.homedir(), "episoda");
|
|
7284
7686
|
}
|
|
7285
7687
|
function getWorktreeInfo(moduleUid, workspaceSlug, projectSlug) {
|
|
7286
7688
|
const root = getEpisodaRoot2();
|
|
7287
|
-
const worktreePath =
|
|
7689
|
+
const worktreePath = path17.join(root, workspaceSlug, projectSlug, moduleUid);
|
|
7288
7690
|
return {
|
|
7289
7691
|
path: worktreePath,
|
|
7290
|
-
exists:
|
|
7692
|
+
exists: fs16.existsSync(worktreePath),
|
|
7291
7693
|
moduleUid
|
|
7292
7694
|
};
|
|
7293
7695
|
}
|
|
@@ -7301,61 +7703,61 @@ async function getWorktreeInfoForModule(moduleUid) {
|
|
|
7301
7703
|
}
|
|
7302
7704
|
|
|
7303
7705
|
// src/framework-detector.ts
|
|
7304
|
-
var
|
|
7305
|
-
var
|
|
7706
|
+
var fs17 = __toESM(require("fs"));
|
|
7707
|
+
var path18 = __toESM(require("path"));
|
|
7306
7708
|
function getInstallCommand(cwd) {
|
|
7307
|
-
if (
|
|
7709
|
+
if (fs17.existsSync(path18.join(cwd, "bun.lockb"))) {
|
|
7308
7710
|
return {
|
|
7309
7711
|
command: ["bun", "install"],
|
|
7310
7712
|
description: "Installing dependencies with bun",
|
|
7311
7713
|
detectedFrom: "bun.lockb"
|
|
7312
7714
|
};
|
|
7313
7715
|
}
|
|
7314
|
-
if (
|
|
7716
|
+
if (fs17.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) {
|
|
7315
7717
|
return {
|
|
7316
7718
|
command: ["pnpm", "install"],
|
|
7317
7719
|
description: "Installing dependencies with pnpm",
|
|
7318
7720
|
detectedFrom: "pnpm-lock.yaml"
|
|
7319
7721
|
};
|
|
7320
7722
|
}
|
|
7321
|
-
if (
|
|
7723
|
+
if (fs17.existsSync(path18.join(cwd, "yarn.lock"))) {
|
|
7322
7724
|
return {
|
|
7323
7725
|
command: ["yarn", "install"],
|
|
7324
7726
|
description: "Installing dependencies with yarn",
|
|
7325
7727
|
detectedFrom: "yarn.lock"
|
|
7326
7728
|
};
|
|
7327
7729
|
}
|
|
7328
|
-
if (
|
|
7730
|
+
if (fs17.existsSync(path18.join(cwd, "package-lock.json"))) {
|
|
7329
7731
|
return {
|
|
7330
7732
|
command: ["npm", "ci"],
|
|
7331
7733
|
description: "Installing dependencies with npm ci",
|
|
7332
7734
|
detectedFrom: "package-lock.json"
|
|
7333
7735
|
};
|
|
7334
7736
|
}
|
|
7335
|
-
if (
|
|
7737
|
+
if (fs17.existsSync(path18.join(cwd, "package.json"))) {
|
|
7336
7738
|
return {
|
|
7337
7739
|
command: ["npm", "install"],
|
|
7338
7740
|
description: "Installing dependencies with npm",
|
|
7339
7741
|
detectedFrom: "package.json"
|
|
7340
7742
|
};
|
|
7341
7743
|
}
|
|
7342
|
-
if (
|
|
7744
|
+
if (fs17.existsSync(path18.join(cwd, "Pipfile.lock")) || fs17.existsSync(path18.join(cwd, "Pipfile"))) {
|
|
7343
7745
|
return {
|
|
7344
7746
|
command: ["pipenv", "install"],
|
|
7345
7747
|
description: "Installing dependencies with pipenv",
|
|
7346
|
-
detectedFrom:
|
|
7748
|
+
detectedFrom: fs17.existsSync(path18.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
|
|
7347
7749
|
};
|
|
7348
7750
|
}
|
|
7349
|
-
if (
|
|
7751
|
+
if (fs17.existsSync(path18.join(cwd, "poetry.lock"))) {
|
|
7350
7752
|
return {
|
|
7351
7753
|
command: ["poetry", "install"],
|
|
7352
7754
|
description: "Installing dependencies with poetry",
|
|
7353
7755
|
detectedFrom: "poetry.lock"
|
|
7354
7756
|
};
|
|
7355
7757
|
}
|
|
7356
|
-
if (
|
|
7357
|
-
const pyprojectPath =
|
|
7358
|
-
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");
|
|
7359
7761
|
if (content.includes("[tool.poetry]")) {
|
|
7360
7762
|
return {
|
|
7361
7763
|
command: ["poetry", "install"],
|
|
@@ -7364,41 +7766,41 @@ function getInstallCommand(cwd) {
|
|
|
7364
7766
|
};
|
|
7365
7767
|
}
|
|
7366
7768
|
}
|
|
7367
|
-
if (
|
|
7769
|
+
if (fs17.existsSync(path18.join(cwd, "requirements.txt"))) {
|
|
7368
7770
|
return {
|
|
7369
7771
|
command: ["pip", "install", "-r", "requirements.txt"],
|
|
7370
7772
|
description: "Installing dependencies with pip",
|
|
7371
7773
|
detectedFrom: "requirements.txt"
|
|
7372
7774
|
};
|
|
7373
7775
|
}
|
|
7374
|
-
if (
|
|
7776
|
+
if (fs17.existsSync(path18.join(cwd, "Gemfile.lock")) || fs17.existsSync(path18.join(cwd, "Gemfile"))) {
|
|
7375
7777
|
return {
|
|
7376
7778
|
command: ["bundle", "install"],
|
|
7377
7779
|
description: "Installing dependencies with bundler",
|
|
7378
|
-
detectedFrom:
|
|
7780
|
+
detectedFrom: fs17.existsSync(path18.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
|
|
7379
7781
|
};
|
|
7380
7782
|
}
|
|
7381
|
-
if (
|
|
7783
|
+
if (fs17.existsSync(path18.join(cwd, "go.sum")) || fs17.existsSync(path18.join(cwd, "go.mod"))) {
|
|
7382
7784
|
return {
|
|
7383
7785
|
command: ["go", "mod", "download"],
|
|
7384
7786
|
description: "Downloading Go modules",
|
|
7385
|
-
detectedFrom:
|
|
7787
|
+
detectedFrom: fs17.existsSync(path18.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
|
|
7386
7788
|
};
|
|
7387
7789
|
}
|
|
7388
|
-
if (
|
|
7790
|
+
if (fs17.existsSync(path18.join(cwd, "Cargo.lock")) || fs17.existsSync(path18.join(cwd, "Cargo.toml"))) {
|
|
7389
7791
|
return {
|
|
7390
7792
|
command: ["cargo", "build"],
|
|
7391
7793
|
description: "Building Rust project (downloads dependencies)",
|
|
7392
|
-
detectedFrom:
|
|
7794
|
+
detectedFrom: fs17.existsSync(path18.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
|
|
7393
7795
|
};
|
|
7394
7796
|
}
|
|
7395
7797
|
return null;
|
|
7396
7798
|
}
|
|
7397
7799
|
|
|
7398
7800
|
// src/daemon/daemon-process.ts
|
|
7399
|
-
var
|
|
7400
|
-
var
|
|
7401
|
-
var
|
|
7801
|
+
var fs18 = __toESM(require("fs"));
|
|
7802
|
+
var os8 = __toESM(require("os"));
|
|
7803
|
+
var path19 = __toESM(require("path"));
|
|
7402
7804
|
var packageJson = require_package();
|
|
7403
7805
|
async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
|
|
7404
7806
|
const now = Date.now();
|
|
@@ -7562,6 +7964,7 @@ var Daemon = class _Daemon {
|
|
|
7562
7964
|
this.registerIPCHandlers();
|
|
7563
7965
|
await this.restoreConnections();
|
|
7564
7966
|
await this.cleanupOrphanedTunnels();
|
|
7967
|
+
await this.cleanupOrphanedDevServers();
|
|
7565
7968
|
await this.auditWorktreesOnStartup();
|
|
7566
7969
|
this.startHealthCheckPolling();
|
|
7567
7970
|
this.setupShutdownHandlers();
|
|
@@ -7608,9 +8011,9 @@ var Daemon = class _Daemon {
|
|
|
7608
8011
|
machineId: this.machineId,
|
|
7609
8012
|
deviceId: this.deviceId,
|
|
7610
8013
|
// EP726: UUID for unified device identification
|
|
7611
|
-
hostname:
|
|
7612
|
-
platform:
|
|
7613
|
-
arch:
|
|
8014
|
+
hostname: os8.hostname(),
|
|
8015
|
+
platform: os8.platform(),
|
|
8016
|
+
arch: os8.arch(),
|
|
7614
8017
|
projects
|
|
7615
8018
|
};
|
|
7616
8019
|
});
|
|
@@ -7900,7 +8303,7 @@ var Daemon = class _Daemon {
|
|
|
7900
8303
|
client.updateActivity();
|
|
7901
8304
|
try {
|
|
7902
8305
|
const gitCmd = message.command;
|
|
7903
|
-
const bareRepoPath =
|
|
8306
|
+
const bareRepoPath = path19.join(projectPath, ".bare");
|
|
7904
8307
|
const cwd = gitCmd.worktreePath || bareRepoPath;
|
|
7905
8308
|
if (gitCmd.worktreePath) {
|
|
7906
8309
|
console.log(`[Daemon] Routing command to worktree: ${gitCmd.worktreePath}`);
|
|
@@ -8245,8 +8648,8 @@ var Daemon = class _Daemon {
|
|
|
8245
8648
|
};
|
|
8246
8649
|
} else {
|
|
8247
8650
|
const manager = new WorktreeManager(projectRootPath);
|
|
8248
|
-
const
|
|
8249
|
-
if (!
|
|
8651
|
+
const initialized2 = await manager.initialize();
|
|
8652
|
+
if (!initialized2) {
|
|
8250
8653
|
console.warn(`[Daemon] EP1035: Failed to initialize WorktreeManager for ${projectRootPath}`);
|
|
8251
8654
|
result = {
|
|
8252
8655
|
success: false,
|
|
@@ -8388,8 +8791,8 @@ var Daemon = class _Daemon {
|
|
|
8388
8791
|
let daemonPid;
|
|
8389
8792
|
try {
|
|
8390
8793
|
const pidPath = getPidFilePath();
|
|
8391
|
-
if (
|
|
8392
|
-
const pidStr =
|
|
8794
|
+
if (fs18.existsSync(pidPath)) {
|
|
8795
|
+
const pidStr = fs18.readFileSync(pidPath, "utf-8").trim();
|
|
8393
8796
|
daemonPid = parseInt(pidStr, 10);
|
|
8394
8797
|
}
|
|
8395
8798
|
} catch (pidError) {
|
|
@@ -8413,9 +8816,9 @@ var Daemon = class _Daemon {
|
|
|
8413
8816
|
client.once("auth_error", errorHandler);
|
|
8414
8817
|
});
|
|
8415
8818
|
await client.connect(wsUrl, config.access_token, this.machineId, {
|
|
8416
|
-
hostname:
|
|
8417
|
-
osPlatform:
|
|
8418
|
-
osArch:
|
|
8819
|
+
hostname: os8.hostname(),
|
|
8820
|
+
osPlatform: os8.platform(),
|
|
8821
|
+
osArch: os8.arch(),
|
|
8419
8822
|
daemonPid
|
|
8420
8823
|
});
|
|
8421
8824
|
console.log(`[Daemon] Successfully connected to project ${projectId}`);
|
|
@@ -8468,29 +8871,29 @@ var Daemon = class _Daemon {
|
|
|
8468
8871
|
*/
|
|
8469
8872
|
async configureGitUser(projectPath, userId, workspaceId, machineId, projectId, deviceId) {
|
|
8470
8873
|
try {
|
|
8471
|
-
const { execSync:
|
|
8472
|
-
|
|
8874
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
8875
|
+
execSync9(`git config episoda.userId ${userId}`, {
|
|
8473
8876
|
cwd: projectPath,
|
|
8474
8877
|
encoding: "utf8",
|
|
8475
8878
|
stdio: "pipe"
|
|
8476
8879
|
});
|
|
8477
|
-
|
|
8880
|
+
execSync9(`git config episoda.workspaceId ${workspaceId}`, {
|
|
8478
8881
|
cwd: projectPath,
|
|
8479
8882
|
encoding: "utf8",
|
|
8480
8883
|
stdio: "pipe"
|
|
8481
8884
|
});
|
|
8482
|
-
|
|
8885
|
+
execSync9(`git config episoda.machineId ${machineId}`, {
|
|
8483
8886
|
cwd: projectPath,
|
|
8484
8887
|
encoding: "utf8",
|
|
8485
8888
|
stdio: "pipe"
|
|
8486
8889
|
});
|
|
8487
|
-
|
|
8890
|
+
execSync9(`git config episoda.projectId ${projectId}`, {
|
|
8488
8891
|
cwd: projectPath,
|
|
8489
8892
|
encoding: "utf8",
|
|
8490
8893
|
stdio: "pipe"
|
|
8491
8894
|
});
|
|
8492
8895
|
if (deviceId) {
|
|
8493
|
-
|
|
8896
|
+
execSync9(`git config episoda.deviceId ${deviceId}`, {
|
|
8494
8897
|
cwd: projectPath,
|
|
8495
8898
|
encoding: "utf8",
|
|
8496
8899
|
stdio: "pipe"
|
|
@@ -8510,27 +8913,27 @@ var Daemon = class _Daemon {
|
|
|
8510
8913
|
*/
|
|
8511
8914
|
async installGitHooks(projectPath) {
|
|
8512
8915
|
const hooks = ["post-checkout", "pre-commit", "post-commit"];
|
|
8513
|
-
const hooksDir =
|
|
8514
|
-
if (!
|
|
8916
|
+
const hooksDir = path19.join(projectPath, ".git", "hooks");
|
|
8917
|
+
if (!fs18.existsSync(hooksDir)) {
|
|
8515
8918
|
console.warn(`[Daemon] Hooks directory not found: ${hooksDir}`);
|
|
8516
8919
|
return;
|
|
8517
8920
|
}
|
|
8518
8921
|
for (const hookName of hooks) {
|
|
8519
8922
|
try {
|
|
8520
|
-
const hookPath =
|
|
8521
|
-
const bundledHookPath =
|
|
8522
|
-
if (!
|
|
8923
|
+
const hookPath = path19.join(hooksDir, hookName);
|
|
8924
|
+
const bundledHookPath = path19.join(__dirname, "..", "hooks", hookName);
|
|
8925
|
+
if (!fs18.existsSync(bundledHookPath)) {
|
|
8523
8926
|
console.warn(`[Daemon] Bundled hook not found: ${bundledHookPath}`);
|
|
8524
8927
|
continue;
|
|
8525
8928
|
}
|
|
8526
|
-
const hookContent =
|
|
8527
|
-
if (
|
|
8528
|
-
const existingContent =
|
|
8929
|
+
const hookContent = fs18.readFileSync(bundledHookPath, "utf-8");
|
|
8930
|
+
if (fs18.existsSync(hookPath)) {
|
|
8931
|
+
const existingContent = fs18.readFileSync(hookPath, "utf-8");
|
|
8529
8932
|
if (existingContent === hookContent) {
|
|
8530
8933
|
continue;
|
|
8531
8934
|
}
|
|
8532
8935
|
}
|
|
8533
|
-
|
|
8936
|
+
fs18.writeFileSync(hookPath, hookContent, { mode: 493 });
|
|
8534
8937
|
console.log(`[Daemon] Installed git hook: ${hookName}`);
|
|
8535
8938
|
} catch (error) {
|
|
8536
8939
|
console.warn(`[Daemon] Failed to install ${hookName} hook:`, error instanceof Error ? error.message : error);
|
|
@@ -8846,8 +9249,8 @@ var Daemon = class _Daemon {
|
|
|
8846
9249
|
console.log(`[Daemon] EP1002: ${installCmd.description} (detected from ${installCmd.detectedFrom})`);
|
|
8847
9250
|
console.log(`[Daemon] EP1002: Running: ${installCmd.command.join(" ")}`);
|
|
8848
9251
|
try {
|
|
8849
|
-
const { execSync:
|
|
8850
|
-
|
|
9252
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
9253
|
+
execSync9(installCmd.command.join(" "), {
|
|
8851
9254
|
cwd: worktreePath,
|
|
8852
9255
|
stdio: "inherit",
|
|
8853
9256
|
timeout: 10 * 60 * 1e3,
|
|
@@ -8900,8 +9303,8 @@ var Daemon = class _Daemon {
|
|
|
8900
9303
|
console.log(`[Daemon] EP986: ${installCmd.description} (detected from ${installCmd.detectedFrom})`);
|
|
8901
9304
|
console.log(`[Daemon] EP986: Running: ${installCmd.command.join(" ")}`);
|
|
8902
9305
|
try {
|
|
8903
|
-
const { execSync:
|
|
8904
|
-
|
|
9306
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
9307
|
+
execSync9(installCmd.command.join(" "), {
|
|
8905
9308
|
cwd: worktreePath,
|
|
8906
9309
|
stdio: "inherit",
|
|
8907
9310
|
timeout: 10 * 60 * 1e3,
|
|
@@ -9035,6 +9438,65 @@ var Daemon = class _Daemon {
|
|
|
9035
9438
|
console.error("[Daemon] EP904: Failed to clean up orphaned tunnels:", error);
|
|
9036
9439
|
}
|
|
9037
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
|
+
}
|
|
9038
9500
|
/**
|
|
9039
9501
|
* EP957: Audit worktrees on daemon startup to detect orphaned worktrees
|
|
9040
9502
|
*
|
|
@@ -9178,9 +9640,20 @@ var Daemon = class _Daemon {
|
|
|
9178
9640
|
}
|
|
9179
9641
|
/**
|
|
9180
9642
|
* EP833: Check if a tunnel is healthy
|
|
9643
|
+
* EP1042: Now also verifies dev server ownership (correct worktree)
|
|
9181
9644
|
* Verifies both the tunnel URL and local dev server respond
|
|
9182
9645
|
*/
|
|
9183
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
|
+
}
|
|
9184
9657
|
try {
|
|
9185
9658
|
const controller = new AbortController();
|
|
9186
9659
|
const timeout = setTimeout(() => controller.abort(), _Daemon.HEALTH_CHECK_TIMEOUT_MS);
|
|
@@ -9415,8 +9888,8 @@ var Daemon = class _Daemon {
|
|
|
9415
9888
|
await this.shutdown();
|
|
9416
9889
|
try {
|
|
9417
9890
|
const pidPath = getPidFilePath();
|
|
9418
|
-
if (
|
|
9419
|
-
|
|
9891
|
+
if (fs18.existsSync(pidPath)) {
|
|
9892
|
+
fs18.unlinkSync(pidPath);
|
|
9420
9893
|
console.log("[Daemon] PID file cleaned up");
|
|
9421
9894
|
}
|
|
9422
9895
|
} catch (error) {
|