episoda 0.2.104 → 0.2.106
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 +336 -165
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1571,15 +1571,15 @@ var require_git_executor = __commonJS({
|
|
|
1571
1571
|
try {
|
|
1572
1572
|
const { stdout: gitDir } = await execAsync3("git rev-parse --git-dir", { cwd, timeout: 5e3 });
|
|
1573
1573
|
const gitDirPath = gitDir.trim();
|
|
1574
|
-
const
|
|
1574
|
+
const fs23 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1575
1575
|
const rebaseMergePath = `${gitDirPath}/rebase-merge`;
|
|
1576
1576
|
const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
|
|
1577
1577
|
try {
|
|
1578
|
-
await
|
|
1578
|
+
await fs23.access(rebaseMergePath);
|
|
1579
1579
|
inRebase = true;
|
|
1580
1580
|
} catch {
|
|
1581
1581
|
try {
|
|
1582
|
-
await
|
|
1582
|
+
await fs23.access(rebaseApplyPath);
|
|
1583
1583
|
inRebase = true;
|
|
1584
1584
|
} catch {
|
|
1585
1585
|
inRebase = false;
|
|
@@ -1633,9 +1633,9 @@ var require_git_executor = __commonJS({
|
|
|
1633
1633
|
error: validation.error || "UNKNOWN_ERROR"
|
|
1634
1634
|
};
|
|
1635
1635
|
}
|
|
1636
|
-
const
|
|
1636
|
+
const fs23 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1637
1637
|
try {
|
|
1638
|
-
await
|
|
1638
|
+
await fs23.access(command.path);
|
|
1639
1639
|
return {
|
|
1640
1640
|
success: false,
|
|
1641
1641
|
error: "WORKTREE_EXISTS",
|
|
@@ -1689,9 +1689,9 @@ var require_git_executor = __commonJS({
|
|
|
1689
1689
|
*/
|
|
1690
1690
|
async executeWorktreeRemove(command, cwd, options) {
|
|
1691
1691
|
try {
|
|
1692
|
-
const
|
|
1692
|
+
const fs23 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1693
1693
|
try {
|
|
1694
|
-
await
|
|
1694
|
+
await fs23.access(command.path);
|
|
1695
1695
|
} catch {
|
|
1696
1696
|
return {
|
|
1697
1697
|
success: false,
|
|
@@ -1726,7 +1726,7 @@ var require_git_executor = __commonJS({
|
|
|
1726
1726
|
const result = await this.runGitCommand(args, cwd, options);
|
|
1727
1727
|
if (result.success) {
|
|
1728
1728
|
try {
|
|
1729
|
-
await
|
|
1729
|
+
await fs23.rm(command.path, { recursive: true, force: true });
|
|
1730
1730
|
} catch {
|
|
1731
1731
|
}
|
|
1732
1732
|
return {
|
|
@@ -1860,10 +1860,10 @@ var require_git_executor = __commonJS({
|
|
|
1860
1860
|
*/
|
|
1861
1861
|
async executeCloneBare(command, options) {
|
|
1862
1862
|
try {
|
|
1863
|
-
const
|
|
1864
|
-
const
|
|
1863
|
+
const fs23 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1864
|
+
const path24 = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1865
1865
|
try {
|
|
1866
|
-
await
|
|
1866
|
+
await fs23.access(command.path);
|
|
1867
1867
|
return {
|
|
1868
1868
|
success: false,
|
|
1869
1869
|
error: "BRANCH_ALREADY_EXISTS",
|
|
@@ -1872,9 +1872,9 @@ var require_git_executor = __commonJS({
|
|
|
1872
1872
|
};
|
|
1873
1873
|
} catch {
|
|
1874
1874
|
}
|
|
1875
|
-
const parentDir =
|
|
1875
|
+
const parentDir = path24.dirname(command.path);
|
|
1876
1876
|
try {
|
|
1877
|
-
await
|
|
1877
|
+
await fs23.mkdir(parentDir, { recursive: true });
|
|
1878
1878
|
} catch {
|
|
1879
1879
|
}
|
|
1880
1880
|
const { stdout, stderr } = await execAsync3(
|
|
@@ -1922,22 +1922,22 @@ var require_git_executor = __commonJS({
|
|
|
1922
1922
|
*/
|
|
1923
1923
|
async executeProjectInfo(cwd, options) {
|
|
1924
1924
|
try {
|
|
1925
|
-
const
|
|
1926
|
-
const
|
|
1925
|
+
const fs23 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1926
|
+
const path24 = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1927
1927
|
let currentPath = cwd;
|
|
1928
1928
|
let projectPath = cwd;
|
|
1929
1929
|
let bareRepoPath;
|
|
1930
1930
|
for (let i = 0; i < 10; i++) {
|
|
1931
|
-
const bareDir =
|
|
1932
|
-
const episodaDir =
|
|
1931
|
+
const bareDir = path24.join(currentPath, ".bare");
|
|
1932
|
+
const episodaDir = path24.join(currentPath, ".episoda");
|
|
1933
1933
|
try {
|
|
1934
|
-
await
|
|
1935
|
-
await
|
|
1934
|
+
await fs23.access(bareDir);
|
|
1935
|
+
await fs23.access(episodaDir);
|
|
1936
1936
|
projectPath = currentPath;
|
|
1937
1937
|
bareRepoPath = bareDir;
|
|
1938
1938
|
break;
|
|
1939
1939
|
} catch {
|
|
1940
|
-
const parentPath =
|
|
1940
|
+
const parentPath = path24.dirname(currentPath);
|
|
1941
1941
|
if (parentPath === currentPath) {
|
|
1942
1942
|
break;
|
|
1943
1943
|
}
|
|
@@ -2117,6 +2117,7 @@ var require_websocket_client = __commonJS({
|
|
|
2117
2117
|
this.lastCommandTime = Date.now();
|
|
2118
2118
|
this.isIntentionalDisconnect = false;
|
|
2119
2119
|
this.lastConnectAttemptTime = 0;
|
|
2120
|
+
this.consecutiveAuthFailures = 0;
|
|
2120
2121
|
}
|
|
2121
2122
|
/**
|
|
2122
2123
|
* Connect to episoda.dev WebSocket gateway
|
|
@@ -2384,6 +2385,10 @@ var require_websocket_client = __commonJS({
|
|
|
2384
2385
|
this.rateLimitBackoffUntil = Date.now() + retryAfterMs;
|
|
2385
2386
|
console.log(`[EpisodaClient] ${errorMessage.code}: will retry after ${retryAfterMs / 1e3}s`);
|
|
2386
2387
|
}
|
|
2388
|
+
if (errorMessage.code === "AUTH_FAILED" || errorMessage.code === "UNAUTHORIZED" || errorMessage.code === "INVALID_TOKEN") {
|
|
2389
|
+
this.consecutiveAuthFailures++;
|
|
2390
|
+
console.warn(`[EpisodaClient] Auth failure (${this.consecutiveAuthFailures}): ${errorMessage.code}`);
|
|
2391
|
+
}
|
|
2387
2392
|
}
|
|
2388
2393
|
const handlers = this.eventHandlers.get(message.type) || [];
|
|
2389
2394
|
handlers.forEach((handler) => {
|
|
@@ -2442,7 +2447,19 @@ var require_websocket_client = __commonJS({
|
|
|
2442
2447
|
}
|
|
2443
2448
|
let delay;
|
|
2444
2449
|
let shouldRetry = true;
|
|
2445
|
-
|
|
2450
|
+
const isCloudMode = this.environment === "cloud";
|
|
2451
|
+
const MAX_CLOUD_AUTH_FAILURES = 3;
|
|
2452
|
+
const MAX_CLOUD_RECONNECT_DELAY = 3e5;
|
|
2453
|
+
if (isCloudMode) {
|
|
2454
|
+
if (this.consecutiveAuthFailures >= MAX_CLOUD_AUTH_FAILURES) {
|
|
2455
|
+
console.error(`[EpisodaClient] Cloud mode: ${MAX_CLOUD_AUTH_FAILURES} consecutive auth failures - token may be invalid. Giving up.`);
|
|
2456
|
+
shouldRetry = false;
|
|
2457
|
+
} else {
|
|
2458
|
+
delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_CLOUD_RECONNECT_DELAY);
|
|
2459
|
+
const delayStr = delay >= 6e4 ? `${Math.round(delay / 6e4)}m` : `${Math.round(delay / 1e3)}s`;
|
|
2460
|
+
console.log(`[EpisodaClient] Cloud mode: reconnecting in ${delayStr}... (attempt ${this.reconnectAttempts + 1}, never giving up)`);
|
|
2461
|
+
}
|
|
2462
|
+
} else if (this.isGracefulShutdown) {
|
|
2446
2463
|
if (this.reconnectAttempts >= 7) {
|
|
2447
2464
|
console.error('[EpisodaClient] Server restart reconnection failed after 7 attempts. Run "episoda dev" to reconnect.');
|
|
2448
2465
|
shouldRetry = false;
|
|
@@ -2485,6 +2502,7 @@ var require_websocket_client = __commonJS({
|
|
|
2485
2502
|
this.isGracefulShutdown = false;
|
|
2486
2503
|
this.firstDisconnectTime = void 0;
|
|
2487
2504
|
this.rateLimitBackoffUntil = void 0;
|
|
2505
|
+
this.consecutiveAuthFailures = 0;
|
|
2488
2506
|
}).catch((error) => {
|
|
2489
2507
|
console.error("[EpisodaClient] Reconnection failed:", error.message);
|
|
2490
2508
|
});
|
|
@@ -2572,31 +2590,31 @@ var require_auth = __commonJS({
|
|
|
2572
2590
|
exports2.loadConfig = loadConfig8;
|
|
2573
2591
|
exports2.saveConfig = saveConfig2;
|
|
2574
2592
|
exports2.validateToken = validateToken;
|
|
2575
|
-
var
|
|
2576
|
-
var
|
|
2593
|
+
var fs23 = __importStar(require("fs"));
|
|
2594
|
+
var path24 = __importStar(require("path"));
|
|
2577
2595
|
var os9 = __importStar(require("os"));
|
|
2578
2596
|
var child_process_1 = require("child_process");
|
|
2579
2597
|
var DEFAULT_CONFIG_FILE = "config.json";
|
|
2580
2598
|
function getConfigDir8() {
|
|
2581
|
-
return process.env.EPISODA_CONFIG_DIR ||
|
|
2599
|
+
return process.env.EPISODA_CONFIG_DIR || path24.join(os9.homedir(), ".episoda");
|
|
2582
2600
|
}
|
|
2583
2601
|
function getConfigPath(configPath) {
|
|
2584
2602
|
if (configPath) {
|
|
2585
2603
|
return configPath;
|
|
2586
2604
|
}
|
|
2587
|
-
return
|
|
2605
|
+
return path24.join(getConfigDir8(), DEFAULT_CONFIG_FILE);
|
|
2588
2606
|
}
|
|
2589
2607
|
function ensureConfigDir(configPath) {
|
|
2590
|
-
const dir =
|
|
2591
|
-
const isNew = !
|
|
2608
|
+
const dir = path24.dirname(configPath);
|
|
2609
|
+
const isNew = !fs23.existsSync(dir);
|
|
2592
2610
|
if (isNew) {
|
|
2593
|
-
|
|
2611
|
+
fs23.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2594
2612
|
}
|
|
2595
2613
|
if (process.platform === "darwin") {
|
|
2596
|
-
const nosyncPath =
|
|
2597
|
-
if (isNew || !
|
|
2614
|
+
const nosyncPath = path24.join(dir, ".nosync");
|
|
2615
|
+
if (isNew || !fs23.existsSync(nosyncPath)) {
|
|
2598
2616
|
try {
|
|
2599
|
-
|
|
2617
|
+
fs23.writeFileSync(nosyncPath, "", { mode: 384 });
|
|
2600
2618
|
(0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
|
|
2601
2619
|
stdio: "ignore",
|
|
2602
2620
|
timeout: 5e3
|
|
@@ -2608,9 +2626,9 @@ var require_auth = __commonJS({
|
|
|
2608
2626
|
}
|
|
2609
2627
|
async function loadConfig8(configPath) {
|
|
2610
2628
|
const fullPath = getConfigPath(configPath);
|
|
2611
|
-
if (
|
|
2629
|
+
if (fs23.existsSync(fullPath)) {
|
|
2612
2630
|
try {
|
|
2613
|
-
const content =
|
|
2631
|
+
const content = fs23.readFileSync(fullPath, "utf8");
|
|
2614
2632
|
const config = JSON.parse(content);
|
|
2615
2633
|
return config;
|
|
2616
2634
|
} catch (error) {
|
|
@@ -2620,9 +2638,9 @@ var require_auth = __commonJS({
|
|
|
2620
2638
|
if (process.env.EPISODA_MODE === "cloud" && process.env.EPISODA_WORKSPACE) {
|
|
2621
2639
|
const homeDir = process.env.HOME || require("os").homedir();
|
|
2622
2640
|
const workspaceConfigPath = require("path").join(homeDir, "episoda", process.env.EPISODA_WORKSPACE, ".episoda", "config.json");
|
|
2623
|
-
if (
|
|
2641
|
+
if (fs23.existsSync(workspaceConfigPath)) {
|
|
2624
2642
|
try {
|
|
2625
|
-
const content =
|
|
2643
|
+
const content = fs23.readFileSync(workspaceConfigPath, "utf8");
|
|
2626
2644
|
const workspaceConfig = JSON.parse(content);
|
|
2627
2645
|
return {
|
|
2628
2646
|
access_token: process.env.EPISODA_ACCESS_TOKEN || workspaceConfig.accessToken,
|
|
@@ -2669,7 +2687,7 @@ var require_auth = __commonJS({
|
|
|
2669
2687
|
ensureConfigDir(fullPath);
|
|
2670
2688
|
try {
|
|
2671
2689
|
const content = JSON.stringify(config, null, 2);
|
|
2672
|
-
|
|
2690
|
+
fs23.writeFileSync(fullPath, content, { mode: 384 });
|
|
2673
2691
|
} catch (error) {
|
|
2674
2692
|
throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
|
|
2675
2693
|
}
|
|
@@ -2786,7 +2804,7 @@ var require_package = __commonJS({
|
|
|
2786
2804
|
"package.json"(exports2, module2) {
|
|
2787
2805
|
module2.exports = {
|
|
2788
2806
|
name: "episoda",
|
|
2789
|
-
version: "0.2.
|
|
2807
|
+
version: "0.2.106",
|
|
2790
2808
|
description: "CLI tool for Episoda local development workflow orchestration",
|
|
2791
2809
|
main: "dist/index.js",
|
|
2792
2810
|
types: "dist/index.d.ts",
|
|
@@ -4763,7 +4781,13 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
4763
4781
|
}
|
|
4764
4782
|
};
|
|
4765
4783
|
function getEpisodaRoot() {
|
|
4766
|
-
|
|
4784
|
+
if (process.env.EPISODA_ROOT) {
|
|
4785
|
+
return process.env.EPISODA_ROOT;
|
|
4786
|
+
}
|
|
4787
|
+
if (process.env.EPISODA_MODE === "cloud") {
|
|
4788
|
+
return process.env.HOME || "/home/episoda";
|
|
4789
|
+
}
|
|
4790
|
+
return path7.join(require("os").homedir(), "episoda");
|
|
4767
4791
|
}
|
|
4768
4792
|
function getProjectPath(workspaceSlug, projectSlug) {
|
|
4769
4793
|
return path7.join(getEpisodaRoot(), workspaceSlug, projectSlug);
|
|
@@ -7712,6 +7736,85 @@ async function persistArtifactsBeforeEject(artifactsPath, projectId, projectSlug
|
|
|
7712
7736
|
}
|
|
7713
7737
|
}
|
|
7714
7738
|
|
|
7739
|
+
// src/daemon/handlers/project-handlers.ts
|
|
7740
|
+
var path16 = __toESM(require("path"));
|
|
7741
|
+
var fs15 = __toESM(require("fs"));
|
|
7742
|
+
function validateSlug(slug, fieldName) {
|
|
7743
|
+
if (!slug || typeof slug !== "string") {
|
|
7744
|
+
return `${fieldName} is required`;
|
|
7745
|
+
}
|
|
7746
|
+
const trimmed = slug.trim();
|
|
7747
|
+
if (!trimmed) {
|
|
7748
|
+
return `${fieldName} cannot be empty`;
|
|
7749
|
+
}
|
|
7750
|
+
if (trimmed.includes("..") || trimmed.includes("/") || trimmed.includes("\\")) {
|
|
7751
|
+
return `${fieldName} contains invalid characters`;
|
|
7752
|
+
}
|
|
7753
|
+
if (trimmed.includes("\0")) {
|
|
7754
|
+
return `${fieldName} contains invalid characters`;
|
|
7755
|
+
}
|
|
7756
|
+
if (trimmed.startsWith(".")) {
|
|
7757
|
+
return `${fieldName} cannot start with a dot`;
|
|
7758
|
+
}
|
|
7759
|
+
return null;
|
|
7760
|
+
}
|
|
7761
|
+
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7762
|
+
async function handleProjectSetup(params) {
|
|
7763
|
+
const { workspaceSlug, projectSlug, projectId } = params;
|
|
7764
|
+
const workspaceError = validateSlug(workspaceSlug, "workspaceSlug");
|
|
7765
|
+
if (workspaceError) {
|
|
7766
|
+
console.error(`[ProjectSetup] EP1199: Validation failed: ${workspaceError}`);
|
|
7767
|
+
return { success: false, error: workspaceError };
|
|
7768
|
+
}
|
|
7769
|
+
const projectError = validateSlug(projectSlug, "projectSlug");
|
|
7770
|
+
if (projectError) {
|
|
7771
|
+
console.error(`[ProjectSetup] EP1199: Validation failed: ${projectError}`);
|
|
7772
|
+
return { success: false, error: projectError };
|
|
7773
|
+
}
|
|
7774
|
+
if (!UUID_REGEX.test(projectId)) {
|
|
7775
|
+
console.error(`[ProjectSetup] EP1199: Invalid projectId format: ${projectId}`);
|
|
7776
|
+
return { success: false, error: "Invalid projectId format" };
|
|
7777
|
+
}
|
|
7778
|
+
console.log(`[ProjectSetup] EP1199: Setting up project ${workspaceSlug}/${projectSlug}`);
|
|
7779
|
+
try {
|
|
7780
|
+
const projectPath = getProjectPath(workspaceSlug, projectSlug);
|
|
7781
|
+
const artifactsPath = path16.join(projectPath, "artifacts");
|
|
7782
|
+
const configDir = path16.join(projectPath, ".episoda");
|
|
7783
|
+
const configPath = path16.join(configDir, "config.json");
|
|
7784
|
+
await fs15.promises.mkdir(artifactsPath, { recursive: true });
|
|
7785
|
+
await fs15.promises.mkdir(configDir, { recursive: true });
|
|
7786
|
+
let existingConfig = {};
|
|
7787
|
+
try {
|
|
7788
|
+
const existing = await fs15.promises.readFile(configPath, "utf-8");
|
|
7789
|
+
existingConfig = JSON.parse(existing);
|
|
7790
|
+
} catch {
|
|
7791
|
+
}
|
|
7792
|
+
const config = {
|
|
7793
|
+
...existingConfig,
|
|
7794
|
+
project_id: projectId,
|
|
7795
|
+
workspace_slug: workspaceSlug,
|
|
7796
|
+
project_slug: projectSlug,
|
|
7797
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7798
|
+
// Only set created_at if not already present
|
|
7799
|
+
created_at: existingConfig.created_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
7800
|
+
};
|
|
7801
|
+
await fs15.promises.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
7802
|
+
console.log(`[ProjectSetup] EP1199: Project setup complete at ${projectPath}`);
|
|
7803
|
+
return {
|
|
7804
|
+
success: true,
|
|
7805
|
+
projectPath,
|
|
7806
|
+
artifactsPath
|
|
7807
|
+
};
|
|
7808
|
+
} catch (error) {
|
|
7809
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
7810
|
+
console.error(`[ProjectSetup] EP1199: Setup failed:`, errorMessage);
|
|
7811
|
+
return {
|
|
7812
|
+
success: false,
|
|
7813
|
+
error: errorMessage
|
|
7814
|
+
};
|
|
7815
|
+
}
|
|
7816
|
+
}
|
|
7817
|
+
|
|
7715
7818
|
// src/daemon/handlers/stale-commit-cleanup.ts
|
|
7716
7819
|
var import_child_process9 = require("child_process");
|
|
7717
7820
|
var import_util2 = require("util");
|
|
@@ -7809,12 +7912,12 @@ async function cleanupStaleCommits(projectPath) {
|
|
|
7809
7912
|
|
|
7810
7913
|
// src/agent/claude-binary.ts
|
|
7811
7914
|
var import_child_process10 = require("child_process");
|
|
7812
|
-
var
|
|
7813
|
-
var
|
|
7915
|
+
var path17 = __toESM(require("path"));
|
|
7916
|
+
var fs16 = __toESM(require("fs"));
|
|
7814
7917
|
var cachedBinaryPath = null;
|
|
7815
7918
|
function isValidClaudeBinary(binaryPath) {
|
|
7816
7919
|
try {
|
|
7817
|
-
|
|
7920
|
+
fs16.accessSync(binaryPath, fs16.constants.X_OK);
|
|
7818
7921
|
const version = (0, import_child_process10.execSync)(`"${binaryPath}" --version`, {
|
|
7819
7922
|
encoding: "utf-8",
|
|
7820
7923
|
timeout: 5e3,
|
|
@@ -7847,14 +7950,14 @@ async function ensureClaudeBinary() {
|
|
|
7847
7950
|
}
|
|
7848
7951
|
const bundledPaths = [
|
|
7849
7952
|
// In production: node_modules/.bin/claude
|
|
7850
|
-
|
|
7953
|
+
path17.join(__dirname, "..", "..", "node_modules", ".bin", "claude"),
|
|
7851
7954
|
// In monorepo development: packages/episoda/node_modules/.bin/claude
|
|
7852
|
-
|
|
7955
|
+
path17.join(__dirname, "..", "..", "..", "..", "node_modules", ".bin", "claude"),
|
|
7853
7956
|
// Root monorepo node_modules
|
|
7854
|
-
|
|
7957
|
+
path17.join(__dirname, "..", "..", "..", "..", "..", "node_modules", ".bin", "claude")
|
|
7855
7958
|
];
|
|
7856
7959
|
for (const bundledPath of bundledPaths) {
|
|
7857
|
-
if (
|
|
7960
|
+
if (fs16.existsSync(bundledPath) && isValidClaudeBinary(bundledPath)) {
|
|
7858
7961
|
cachedBinaryPath = bundledPath;
|
|
7859
7962
|
return cachedBinaryPath;
|
|
7860
7963
|
}
|
|
@@ -7880,12 +7983,12 @@ async function ensureClaudeBinary() {
|
|
|
7880
7983
|
|
|
7881
7984
|
// src/agent/codex-binary.ts
|
|
7882
7985
|
var import_child_process11 = require("child_process");
|
|
7883
|
-
var
|
|
7884
|
-
var
|
|
7986
|
+
var path18 = __toESM(require("path"));
|
|
7987
|
+
var fs17 = __toESM(require("fs"));
|
|
7885
7988
|
var cachedBinaryPath2 = null;
|
|
7886
7989
|
function isValidCodexBinary(binaryPath) {
|
|
7887
7990
|
try {
|
|
7888
|
-
|
|
7991
|
+
fs17.accessSync(binaryPath, fs17.constants.X_OK);
|
|
7889
7992
|
const version = (0, import_child_process11.execSync)(`"${binaryPath}" --version`, {
|
|
7890
7993
|
encoding: "utf-8",
|
|
7891
7994
|
timeout: 5e3,
|
|
@@ -7918,14 +8021,14 @@ async function ensureCodexBinary() {
|
|
|
7918
8021
|
}
|
|
7919
8022
|
const bundledPaths = [
|
|
7920
8023
|
// In production: node_modules/.bin/codex
|
|
7921
|
-
|
|
8024
|
+
path18.join(__dirname, "..", "..", "node_modules", ".bin", "codex"),
|
|
7922
8025
|
// In monorepo development: packages/episoda/node_modules/.bin/codex
|
|
7923
|
-
|
|
8026
|
+
path18.join(__dirname, "..", "..", "..", "..", "node_modules", ".bin", "codex"),
|
|
7924
8027
|
// Root monorepo node_modules
|
|
7925
|
-
|
|
8028
|
+
path18.join(__dirname, "..", "..", "..", "..", "..", "node_modules", ".bin", "codex")
|
|
7926
8029
|
];
|
|
7927
8030
|
for (const bundledPath of bundledPaths) {
|
|
7928
|
-
if (
|
|
8031
|
+
if (fs17.existsSync(bundledPath) && isValidCodexBinary(bundledPath)) {
|
|
7929
8032
|
cachedBinaryPath2 = bundledPath;
|
|
7930
8033
|
return cachedBinaryPath2;
|
|
7931
8034
|
}
|
|
@@ -7992,8 +8095,8 @@ function generateCodexConfig(credentials, projectPath) {
|
|
|
7992
8095
|
|
|
7993
8096
|
// src/agent/agent-manager.ts
|
|
7994
8097
|
var import_child_process12 = require("child_process");
|
|
7995
|
-
var
|
|
7996
|
-
var
|
|
8098
|
+
var path19 = __toESM(require("path"));
|
|
8099
|
+
var fs18 = __toESM(require("fs"));
|
|
7997
8100
|
var os6 = __toESM(require("os"));
|
|
7998
8101
|
|
|
7999
8102
|
// src/agent/claude-config.ts
|
|
@@ -8316,7 +8419,7 @@ var AgentManager = class {
|
|
|
8316
8419
|
this.initialized = false;
|
|
8317
8420
|
// EP1133: Lock for config file writes to prevent race conditions
|
|
8318
8421
|
this.configWriteLock = Promise.resolve();
|
|
8319
|
-
this.pidDir =
|
|
8422
|
+
this.pidDir = path19.join(os6.homedir(), ".episoda", "agent-pids");
|
|
8320
8423
|
}
|
|
8321
8424
|
/**
|
|
8322
8425
|
* EP1133: Acquire lock for config file writes
|
|
@@ -8345,8 +8448,8 @@ var AgentManager = class {
|
|
|
8345
8448
|
return;
|
|
8346
8449
|
}
|
|
8347
8450
|
console.log("[AgentManager] Initializing...");
|
|
8348
|
-
if (!
|
|
8349
|
-
|
|
8451
|
+
if (!fs18.existsSync(this.pidDir)) {
|
|
8452
|
+
fs18.mkdirSync(this.pidDir, { recursive: true });
|
|
8350
8453
|
}
|
|
8351
8454
|
await this.cleanupOrphanedProcesses();
|
|
8352
8455
|
try {
|
|
@@ -8593,9 +8696,9 @@ If changes are needed, explain what needs to be done.`;
|
|
|
8593
8696
|
const useApiKey = !useOAuth && !!session.credentials.apiKey;
|
|
8594
8697
|
if (provider === "codex") {
|
|
8595
8698
|
await this.withConfigLock(async () => {
|
|
8596
|
-
const codexDir =
|
|
8597
|
-
if (!
|
|
8598
|
-
|
|
8699
|
+
const codexDir = path19.join(os6.homedir(), ".codex");
|
|
8700
|
+
if (!fs18.existsSync(codexDir)) {
|
|
8701
|
+
fs18.mkdirSync(codexDir, { recursive: true });
|
|
8599
8702
|
}
|
|
8600
8703
|
if (useOAuth) {
|
|
8601
8704
|
const codexConfig = generateCodexConfig({
|
|
@@ -8605,21 +8708,21 @@ If changes are needed, explain what needs to be done.`;
|
|
|
8605
8708
|
accountId: session.credentials.accountId,
|
|
8606
8709
|
expiresAt: session.credentials.expiresAt
|
|
8607
8710
|
}, session.projectPath);
|
|
8608
|
-
const authJsonPath =
|
|
8609
|
-
|
|
8711
|
+
const authJsonPath = path19.join(codexDir, "auth.json");
|
|
8712
|
+
fs18.writeFileSync(authJsonPath, codexConfig["auth.json"], { mode: 384 });
|
|
8610
8713
|
console.log("[AgentManager] EP1133: Wrote Codex auth.json to ~/.codex/auth.json");
|
|
8611
8714
|
if (codexConfig["config.toml"]) {
|
|
8612
|
-
const configTomlPath =
|
|
8715
|
+
const configTomlPath = path19.join(codexDir, "config.toml");
|
|
8613
8716
|
let existingConfig = "";
|
|
8614
8717
|
try {
|
|
8615
|
-
existingConfig =
|
|
8718
|
+
existingConfig = fs18.readFileSync(configTomlPath, "utf-8");
|
|
8616
8719
|
} catch {
|
|
8617
8720
|
}
|
|
8618
8721
|
const escapedPathForRegex = session.projectPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8619
8722
|
const projectKeyPattern = new RegExp(`\\[projects\\."${escapedPathForRegex}"\\]`);
|
|
8620
8723
|
const projectAlreadyTrusted = projectKeyPattern.test(existingConfig);
|
|
8621
8724
|
if (!projectAlreadyTrusted) {
|
|
8622
|
-
|
|
8725
|
+
fs18.writeFileSync(configTomlPath, existingConfig + "\n" + codexConfig["config.toml"], { mode: 420 });
|
|
8623
8726
|
console.log("[AgentManager] EP1133: Updated Codex config.toml with project trust");
|
|
8624
8727
|
}
|
|
8625
8728
|
}
|
|
@@ -8629,14 +8732,14 @@ If changes are needed, explain what needs to be done.`;
|
|
|
8629
8732
|
});
|
|
8630
8733
|
} else {
|
|
8631
8734
|
await this.withConfigLock(async () => {
|
|
8632
|
-
const claudeDir =
|
|
8633
|
-
const credentialsPath =
|
|
8634
|
-
const statsigDir =
|
|
8635
|
-
if (!
|
|
8636
|
-
|
|
8735
|
+
const claudeDir = path19.join(os6.homedir(), ".claude");
|
|
8736
|
+
const credentialsPath = path19.join(claudeDir, ".credentials.json");
|
|
8737
|
+
const statsigDir = path19.join(claudeDir, "statsig");
|
|
8738
|
+
if (!fs18.existsSync(claudeDir)) {
|
|
8739
|
+
fs18.mkdirSync(claudeDir, { recursive: true });
|
|
8637
8740
|
}
|
|
8638
|
-
if (!
|
|
8639
|
-
|
|
8741
|
+
if (!fs18.existsSync(statsigDir)) {
|
|
8742
|
+
fs18.mkdirSync(statsigDir, { recursive: true });
|
|
8640
8743
|
}
|
|
8641
8744
|
if (useOAuth) {
|
|
8642
8745
|
const oauthCredentials = {
|
|
@@ -8654,7 +8757,7 @@ If changes are needed, explain what needs to be done.`;
|
|
|
8654
8757
|
const credentialsContent = JSON.stringify({
|
|
8655
8758
|
claudeAiOauth: oauthCredentials
|
|
8656
8759
|
}, null, 2);
|
|
8657
|
-
|
|
8760
|
+
fs18.writeFileSync(credentialsPath, credentialsContent, { mode: 384 });
|
|
8658
8761
|
console.log("[AgentManager] Wrote OAuth credentials to ~/.claude/.credentials.json");
|
|
8659
8762
|
try {
|
|
8660
8763
|
const claudeConfig = generateClaudeConfig({
|
|
@@ -8666,11 +8769,11 @@ If changes are needed, explain what needs to be done.`;
|
|
|
8666
8769
|
if (!hasEvaluations || !hasStableId) {
|
|
8667
8770
|
throw new Error(`Invalid statsig config: missing required files`);
|
|
8668
8771
|
}
|
|
8669
|
-
const settingsPath =
|
|
8670
|
-
|
|
8772
|
+
const settingsPath = path19.join(claudeDir, "settings.json");
|
|
8773
|
+
fs18.writeFileSync(settingsPath, claudeConfig["settings.json"], { mode: 384 });
|
|
8671
8774
|
for (const [filename, content] of Object.entries(claudeConfig.statsig)) {
|
|
8672
|
-
const filePath =
|
|
8673
|
-
|
|
8775
|
+
const filePath = path19.join(statsigDir, filename);
|
|
8776
|
+
fs18.writeFileSync(filePath, content, { mode: 420 });
|
|
8674
8777
|
}
|
|
8675
8778
|
if (session.credentials.githubToken) {
|
|
8676
8779
|
console.log("[AgentManager] EP1146: GitHub MCP server enabled with installation token");
|
|
@@ -8940,14 +9043,14 @@ If changes are needed, explain what needs to be done.`;
|
|
|
8940
9043
|
*/
|
|
8941
9044
|
async cleanupOrphanedProcesses() {
|
|
8942
9045
|
let cleaned = 0;
|
|
8943
|
-
if (!
|
|
9046
|
+
if (!fs18.existsSync(this.pidDir)) {
|
|
8944
9047
|
return { cleaned };
|
|
8945
9048
|
}
|
|
8946
|
-
const pidFiles =
|
|
9049
|
+
const pidFiles = fs18.readdirSync(this.pidDir).filter((f) => f.endsWith(".pid"));
|
|
8947
9050
|
for (const pidFile of pidFiles) {
|
|
8948
|
-
const pidPath =
|
|
9051
|
+
const pidPath = path19.join(this.pidDir, pidFile);
|
|
8949
9052
|
try {
|
|
8950
|
-
const pidStr =
|
|
9053
|
+
const pidStr = fs18.readFileSync(pidPath, "utf-8").trim();
|
|
8951
9054
|
const pid = parseInt(pidStr, 10);
|
|
8952
9055
|
if (!isNaN(pid)) {
|
|
8953
9056
|
try {
|
|
@@ -8958,7 +9061,7 @@ If changes are needed, explain what needs to be done.`;
|
|
|
8958
9061
|
} catch {
|
|
8959
9062
|
}
|
|
8960
9063
|
}
|
|
8961
|
-
|
|
9064
|
+
fs18.unlinkSync(pidPath);
|
|
8962
9065
|
} catch (error) {
|
|
8963
9066
|
console.warn(`[AgentManager] Error cleaning PID file ${pidFile}:`, error);
|
|
8964
9067
|
}
|
|
@@ -8972,17 +9075,17 @@ If changes are needed, explain what needs to be done.`;
|
|
|
8972
9075
|
* Write PID file for session tracking
|
|
8973
9076
|
*/
|
|
8974
9077
|
writePidFile(sessionId, pid) {
|
|
8975
|
-
const pidPath =
|
|
8976
|
-
|
|
9078
|
+
const pidPath = path19.join(this.pidDir, `${sessionId}.pid`);
|
|
9079
|
+
fs18.writeFileSync(pidPath, pid.toString());
|
|
8977
9080
|
}
|
|
8978
9081
|
/**
|
|
8979
9082
|
* Remove PID file for session
|
|
8980
9083
|
*/
|
|
8981
9084
|
removePidFile(sessionId) {
|
|
8982
|
-
const pidPath =
|
|
9085
|
+
const pidPath = path19.join(this.pidDir, `${sessionId}.pid`);
|
|
8983
9086
|
try {
|
|
8984
|
-
if (
|
|
8985
|
-
|
|
9087
|
+
if (fs18.existsSync(pidPath)) {
|
|
9088
|
+
fs18.unlinkSync(pidPath);
|
|
8986
9089
|
}
|
|
8987
9090
|
} catch {
|
|
8988
9091
|
}
|
|
@@ -8992,8 +9095,8 @@ If changes are needed, explain what needs to be done.`;
|
|
|
8992
9095
|
// src/utils/dev-server.ts
|
|
8993
9096
|
var import_child_process13 = require("child_process");
|
|
8994
9097
|
var import_core11 = __toESM(require_dist());
|
|
8995
|
-
var
|
|
8996
|
-
var
|
|
9098
|
+
var fs19 = __toESM(require("fs"));
|
|
9099
|
+
var path20 = __toESM(require("path"));
|
|
8997
9100
|
var MAX_RESTART_ATTEMPTS = 5;
|
|
8998
9101
|
var INITIAL_RESTART_DELAY_MS = 2e3;
|
|
8999
9102
|
var MAX_RESTART_DELAY_MS = 3e4;
|
|
@@ -9001,26 +9104,26 @@ var MAX_LOG_SIZE_BYTES = 5 * 1024 * 1024;
|
|
|
9001
9104
|
var NODE_MEMORY_LIMIT_MB = 2048;
|
|
9002
9105
|
var activeServers = /* @__PURE__ */ new Map();
|
|
9003
9106
|
function getLogsDir() {
|
|
9004
|
-
const logsDir =
|
|
9005
|
-
if (!
|
|
9006
|
-
|
|
9107
|
+
const logsDir = path20.join((0, import_core11.getConfigDir)(), "logs");
|
|
9108
|
+
if (!fs19.existsSync(logsDir)) {
|
|
9109
|
+
fs19.mkdirSync(logsDir, { recursive: true });
|
|
9007
9110
|
}
|
|
9008
9111
|
return logsDir;
|
|
9009
9112
|
}
|
|
9010
9113
|
function getLogFilePath(moduleUid) {
|
|
9011
|
-
return
|
|
9114
|
+
return path20.join(getLogsDir(), `dev-${moduleUid}.log`);
|
|
9012
9115
|
}
|
|
9013
9116
|
function rotateLogIfNeeded(logPath) {
|
|
9014
9117
|
try {
|
|
9015
|
-
if (
|
|
9016
|
-
const stats =
|
|
9118
|
+
if (fs19.existsSync(logPath)) {
|
|
9119
|
+
const stats = fs19.statSync(logPath);
|
|
9017
9120
|
if (stats.size > MAX_LOG_SIZE_BYTES) {
|
|
9018
9121
|
const backupPath = `${logPath}.1`;
|
|
9019
|
-
if (
|
|
9020
|
-
|
|
9122
|
+
if (fs19.existsSync(backupPath)) {
|
|
9123
|
+
fs19.unlinkSync(backupPath);
|
|
9021
9124
|
}
|
|
9022
|
-
|
|
9023
|
-
console.log(`[DevServer] EP932: Rotated log file for ${
|
|
9125
|
+
fs19.renameSync(logPath, backupPath);
|
|
9126
|
+
console.log(`[DevServer] EP932: Rotated log file for ${path20.basename(logPath)}`);
|
|
9024
9127
|
}
|
|
9025
9128
|
}
|
|
9026
9129
|
} catch (error) {
|
|
@@ -9033,7 +9136,7 @@ function writeToLog(logPath, line, isError = false) {
|
|
|
9033
9136
|
const prefix = isError ? "ERR" : "OUT";
|
|
9034
9137
|
const logLine = `[${timestamp}] [${prefix}] ${line}
|
|
9035
9138
|
`;
|
|
9036
|
-
|
|
9139
|
+
fs19.appendFileSync(logPath, logLine);
|
|
9037
9140
|
} catch {
|
|
9038
9141
|
}
|
|
9039
9142
|
}
|
|
@@ -9212,8 +9315,8 @@ async function startDevServer(projectPath, port = 3e3, moduleUid = "default", op
|
|
|
9212
9315
|
});
|
|
9213
9316
|
injectedEnvVars = result.envVars;
|
|
9214
9317
|
console.log(`[DevServer] EP998: Loaded ${Object.keys(injectedEnvVars).length} env vars (from ${result.fromCache ? "cache" : "server"})`);
|
|
9215
|
-
const envFilePath =
|
|
9216
|
-
if (!
|
|
9318
|
+
const envFilePath = path20.join(projectPath, ".env");
|
|
9319
|
+
if (!fs19.existsSync(envFilePath) && Object.keys(injectedEnvVars).length > 0) {
|
|
9217
9320
|
console.log(`[DevServer] EP1004: .env file missing, writing ${Object.keys(injectedEnvVars).length} vars to ${envFilePath}`);
|
|
9218
9321
|
writeEnvFile(projectPath, injectedEnvVars);
|
|
9219
9322
|
}
|
|
@@ -9319,19 +9422,19 @@ function getDevServerStatus() {
|
|
|
9319
9422
|
}
|
|
9320
9423
|
|
|
9321
9424
|
// src/utils/worktree.ts
|
|
9322
|
-
var
|
|
9323
|
-
var
|
|
9425
|
+
var path21 = __toESM(require("path"));
|
|
9426
|
+
var fs20 = __toESM(require("fs"));
|
|
9324
9427
|
var os7 = __toESM(require("os"));
|
|
9325
9428
|
var import_core12 = __toESM(require_dist());
|
|
9326
9429
|
function getEpisodaRoot2() {
|
|
9327
|
-
return process.env.EPISODA_ROOT ||
|
|
9430
|
+
return process.env.EPISODA_ROOT || path21.join(os7.homedir(), "episoda");
|
|
9328
9431
|
}
|
|
9329
9432
|
function getWorktreeInfo(moduleUid, workspaceSlug, projectSlug) {
|
|
9330
9433
|
const root = getEpisodaRoot2();
|
|
9331
|
-
const worktreePath =
|
|
9434
|
+
const worktreePath = path21.join(root, workspaceSlug, projectSlug, moduleUid);
|
|
9332
9435
|
return {
|
|
9333
9436
|
path: worktreePath,
|
|
9334
|
-
exists:
|
|
9437
|
+
exists: fs20.existsSync(worktreePath),
|
|
9335
9438
|
moduleUid
|
|
9336
9439
|
};
|
|
9337
9440
|
}
|
|
@@ -9345,15 +9448,15 @@ async function getWorktreeInfoForModule(moduleUid) {
|
|
|
9345
9448
|
return null;
|
|
9346
9449
|
}
|
|
9347
9450
|
const root = getEpisodaRoot2();
|
|
9348
|
-
const workspaceRoot =
|
|
9451
|
+
const workspaceRoot = path21.join(root, config.workspace_slug);
|
|
9349
9452
|
try {
|
|
9350
|
-
const entries =
|
|
9453
|
+
const entries = fs20.readdirSync(workspaceRoot, { withFileTypes: true });
|
|
9351
9454
|
for (const entry of entries) {
|
|
9352
9455
|
if (!entry.isDirectory()) {
|
|
9353
9456
|
continue;
|
|
9354
9457
|
}
|
|
9355
|
-
const worktreePath =
|
|
9356
|
-
if (
|
|
9458
|
+
const worktreePath = path21.join(workspaceRoot, entry.name, moduleUid);
|
|
9459
|
+
if (fs20.existsSync(worktreePath)) {
|
|
9357
9460
|
return {
|
|
9358
9461
|
path: worktreePath,
|
|
9359
9462
|
exists: true,
|
|
@@ -9370,61 +9473,61 @@ async function getWorktreeInfoForModule(moduleUid) {
|
|
|
9370
9473
|
}
|
|
9371
9474
|
|
|
9372
9475
|
// src/framework-detector.ts
|
|
9373
|
-
var
|
|
9374
|
-
var
|
|
9476
|
+
var fs21 = __toESM(require("fs"));
|
|
9477
|
+
var path22 = __toESM(require("path"));
|
|
9375
9478
|
function getInstallCommand(cwd) {
|
|
9376
|
-
if (
|
|
9479
|
+
if (fs21.existsSync(path22.join(cwd, "bun.lockb"))) {
|
|
9377
9480
|
return {
|
|
9378
9481
|
command: ["bun", "install"],
|
|
9379
9482
|
description: "Installing dependencies with bun",
|
|
9380
9483
|
detectedFrom: "bun.lockb"
|
|
9381
9484
|
};
|
|
9382
9485
|
}
|
|
9383
|
-
if (
|
|
9486
|
+
if (fs21.existsSync(path22.join(cwd, "pnpm-lock.yaml"))) {
|
|
9384
9487
|
return {
|
|
9385
9488
|
command: ["pnpm", "install"],
|
|
9386
9489
|
description: "Installing dependencies with pnpm",
|
|
9387
9490
|
detectedFrom: "pnpm-lock.yaml"
|
|
9388
9491
|
};
|
|
9389
9492
|
}
|
|
9390
|
-
if (
|
|
9493
|
+
if (fs21.existsSync(path22.join(cwd, "yarn.lock"))) {
|
|
9391
9494
|
return {
|
|
9392
9495
|
command: ["yarn", "install"],
|
|
9393
9496
|
description: "Installing dependencies with yarn",
|
|
9394
9497
|
detectedFrom: "yarn.lock"
|
|
9395
9498
|
};
|
|
9396
9499
|
}
|
|
9397
|
-
if (
|
|
9500
|
+
if (fs21.existsSync(path22.join(cwd, "package-lock.json"))) {
|
|
9398
9501
|
return {
|
|
9399
9502
|
command: ["npm", "ci"],
|
|
9400
9503
|
description: "Installing dependencies with npm ci",
|
|
9401
9504
|
detectedFrom: "package-lock.json"
|
|
9402
9505
|
};
|
|
9403
9506
|
}
|
|
9404
|
-
if (
|
|
9507
|
+
if (fs21.existsSync(path22.join(cwd, "package.json"))) {
|
|
9405
9508
|
return {
|
|
9406
9509
|
command: ["npm", "install"],
|
|
9407
9510
|
description: "Installing dependencies with npm",
|
|
9408
9511
|
detectedFrom: "package.json"
|
|
9409
9512
|
};
|
|
9410
9513
|
}
|
|
9411
|
-
if (
|
|
9514
|
+
if (fs21.existsSync(path22.join(cwd, "Pipfile.lock")) || fs21.existsSync(path22.join(cwd, "Pipfile"))) {
|
|
9412
9515
|
return {
|
|
9413
9516
|
command: ["pipenv", "install"],
|
|
9414
9517
|
description: "Installing dependencies with pipenv",
|
|
9415
|
-
detectedFrom:
|
|
9518
|
+
detectedFrom: fs21.existsSync(path22.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
|
|
9416
9519
|
};
|
|
9417
9520
|
}
|
|
9418
|
-
if (
|
|
9521
|
+
if (fs21.existsSync(path22.join(cwd, "poetry.lock"))) {
|
|
9419
9522
|
return {
|
|
9420
9523
|
command: ["poetry", "install"],
|
|
9421
9524
|
description: "Installing dependencies with poetry",
|
|
9422
9525
|
detectedFrom: "poetry.lock"
|
|
9423
9526
|
};
|
|
9424
9527
|
}
|
|
9425
|
-
if (
|
|
9426
|
-
const pyprojectPath =
|
|
9427
|
-
const content =
|
|
9528
|
+
if (fs21.existsSync(path22.join(cwd, "pyproject.toml"))) {
|
|
9529
|
+
const pyprojectPath = path22.join(cwd, "pyproject.toml");
|
|
9530
|
+
const content = fs21.readFileSync(pyprojectPath, "utf-8");
|
|
9428
9531
|
if (content.includes("[tool.poetry]")) {
|
|
9429
9532
|
return {
|
|
9430
9533
|
command: ["poetry", "install"],
|
|
@@ -9433,41 +9536,42 @@ function getInstallCommand(cwd) {
|
|
|
9433
9536
|
};
|
|
9434
9537
|
}
|
|
9435
9538
|
}
|
|
9436
|
-
if (
|
|
9539
|
+
if (fs21.existsSync(path22.join(cwd, "requirements.txt"))) {
|
|
9437
9540
|
return {
|
|
9438
9541
|
command: ["pip", "install", "-r", "requirements.txt"],
|
|
9439
9542
|
description: "Installing dependencies with pip",
|
|
9440
9543
|
detectedFrom: "requirements.txt"
|
|
9441
9544
|
};
|
|
9442
9545
|
}
|
|
9443
|
-
if (
|
|
9546
|
+
if (fs21.existsSync(path22.join(cwd, "Gemfile.lock")) || fs21.existsSync(path22.join(cwd, "Gemfile"))) {
|
|
9444
9547
|
return {
|
|
9445
9548
|
command: ["bundle", "install"],
|
|
9446
9549
|
description: "Installing dependencies with bundler",
|
|
9447
|
-
detectedFrom:
|
|
9550
|
+
detectedFrom: fs21.existsSync(path22.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
|
|
9448
9551
|
};
|
|
9449
9552
|
}
|
|
9450
|
-
if (
|
|
9553
|
+
if (fs21.existsSync(path22.join(cwd, "go.sum")) || fs21.existsSync(path22.join(cwd, "go.mod"))) {
|
|
9451
9554
|
return {
|
|
9452
9555
|
command: ["go", "mod", "download"],
|
|
9453
9556
|
description: "Downloading Go modules",
|
|
9454
|
-
detectedFrom:
|
|
9557
|
+
detectedFrom: fs21.existsSync(path22.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
|
|
9455
9558
|
};
|
|
9456
9559
|
}
|
|
9457
|
-
if (
|
|
9560
|
+
if (fs21.existsSync(path22.join(cwd, "Cargo.lock")) || fs21.existsSync(path22.join(cwd, "Cargo.toml"))) {
|
|
9458
9561
|
return {
|
|
9459
9562
|
command: ["cargo", "build"],
|
|
9460
9563
|
description: "Building Rust project (downloads dependencies)",
|
|
9461
|
-
detectedFrom:
|
|
9564
|
+
detectedFrom: fs21.existsSync(path22.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
|
|
9462
9565
|
};
|
|
9463
9566
|
}
|
|
9464
9567
|
return null;
|
|
9465
9568
|
}
|
|
9466
9569
|
|
|
9467
9570
|
// src/daemon/daemon-process.ts
|
|
9468
|
-
var
|
|
9571
|
+
var fs22 = __toESM(require("fs"));
|
|
9572
|
+
var http2 = __toESM(require("http"));
|
|
9469
9573
|
var os8 = __toESM(require("os"));
|
|
9470
|
-
var
|
|
9574
|
+
var path23 = __toESM(require("path"));
|
|
9471
9575
|
var packageJson = require_package();
|
|
9472
9576
|
async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
|
|
9473
9577
|
const now = Date.now();
|
|
@@ -9600,6 +9704,8 @@ var Daemon = class _Daemon {
|
|
|
9600
9704
|
// 60 seconds
|
|
9601
9705
|
// EP1190: Worktree cleanup runs every N health checks (5 * 60s = 5 minutes)
|
|
9602
9706
|
this.healthCheckCounter = 0;
|
|
9707
|
+
// EP1210-7: Health HTTP endpoint for external monitoring
|
|
9708
|
+
this.healthServer = null;
|
|
9603
9709
|
this.ipcServer = new IPCServer();
|
|
9604
9710
|
}
|
|
9605
9711
|
static {
|
|
@@ -9619,6 +9725,9 @@ var Daemon = class _Daemon {
|
|
|
9619
9725
|
static {
|
|
9620
9726
|
this.WORKTREE_CLEANUP_EVERY_N_CHECKS = 5;
|
|
9621
9727
|
}
|
|
9728
|
+
static {
|
|
9729
|
+
this.HEALTH_PORT = 9999;
|
|
9730
|
+
}
|
|
9622
9731
|
/**
|
|
9623
9732
|
* Start the daemon
|
|
9624
9733
|
*/
|
|
@@ -9640,6 +9749,7 @@ var Daemon = class _Daemon {
|
|
|
9640
9749
|
await this.auditWorktreesOnStartup();
|
|
9641
9750
|
this.startHealthCheckPolling();
|
|
9642
9751
|
this.setupShutdownHandlers();
|
|
9752
|
+
this.startHealthEndpoint();
|
|
9643
9753
|
console.log("[Daemon] Daemon started successfully");
|
|
9644
9754
|
const modeConfig = getDaemonModeConfig();
|
|
9645
9755
|
console.log("[Daemon] EP1115: Mode config:", {
|
|
@@ -9669,6 +9779,55 @@ var Daemon = class _Daemon {
|
|
|
9669
9779
|
}
|
|
9670
9780
|
}
|
|
9671
9781
|
// EP738: Removed startHttpServer - device info now flows through WebSocket broadcast + database
|
|
9782
|
+
/**
|
|
9783
|
+
* EP1210-7: Start health HTTP endpoint for external monitoring
|
|
9784
|
+
*
|
|
9785
|
+
* Provides a simple HTTP endpoint that external systems can use to check daemon status.
|
|
9786
|
+
* Returns 200 when daemon has at least one live connection, 503 when disconnected.
|
|
9787
|
+
*
|
|
9788
|
+
* Only binds to localhost (127.0.0.1) for security.
|
|
9789
|
+
*/
|
|
9790
|
+
startHealthEndpoint() {
|
|
9791
|
+
try {
|
|
9792
|
+
this.healthServer = http2.createServer((req, res) => {
|
|
9793
|
+
if (req.url === "/health" || req.url === "/") {
|
|
9794
|
+
const isConnected = this.liveConnections.size > 0;
|
|
9795
|
+
const projects = Array.from(this.connections.entries()).map(([path24, conn]) => ({
|
|
9796
|
+
path: path24,
|
|
9797
|
+
connected: this.liveConnections.has(path24)
|
|
9798
|
+
}));
|
|
9799
|
+
const status = {
|
|
9800
|
+
status: isConnected ? "healthy" : "degraded",
|
|
9801
|
+
connected: isConnected,
|
|
9802
|
+
machineId: this.machineId,
|
|
9803
|
+
uptime: process.uptime(),
|
|
9804
|
+
liveConnections: this.liveConnections.size,
|
|
9805
|
+
totalConnections: this.connections.size,
|
|
9806
|
+
projects
|
|
9807
|
+
};
|
|
9808
|
+
res.writeHead(isConnected ? 200 : 503, { "Content-Type": "application/json" });
|
|
9809
|
+
res.end(JSON.stringify(status));
|
|
9810
|
+
} else {
|
|
9811
|
+
res.writeHead(404);
|
|
9812
|
+
res.end("Not found");
|
|
9813
|
+
}
|
|
9814
|
+
});
|
|
9815
|
+
this.healthServer.listen(_Daemon.HEALTH_PORT, "127.0.0.1", () => {
|
|
9816
|
+
console.log(`[Daemon] EP1210-7: Health endpoint listening on http://127.0.0.1:${_Daemon.HEALTH_PORT}/health`);
|
|
9817
|
+
});
|
|
9818
|
+
this.healthServer.on("error", (err) => {
|
|
9819
|
+
if (err.code === "EADDRINUSE") {
|
|
9820
|
+
console.warn(`[Daemon] EP1210-7: Health port ${_Daemon.HEALTH_PORT} already in use, skipping health endpoint`);
|
|
9821
|
+
} else {
|
|
9822
|
+
console.warn("[Daemon] EP1210-7: Health endpoint failed to start:", err.message);
|
|
9823
|
+
}
|
|
9824
|
+
this.healthServer = null;
|
|
9825
|
+
});
|
|
9826
|
+
} catch (err) {
|
|
9827
|
+
console.warn("[Daemon] EP1210-7: Failed to create health server:", err.message);
|
|
9828
|
+
this.healthServer = null;
|
|
9829
|
+
}
|
|
9830
|
+
}
|
|
9672
9831
|
/**
|
|
9673
9832
|
* Register IPC command handlers
|
|
9674
9833
|
*/
|
|
@@ -9865,9 +10024,12 @@ var Daemon = class _Daemon {
|
|
|
9865
10024
|
this.ipcServer.on("worktree-list", async (params) => {
|
|
9866
10025
|
return handleWorktreeList(params.workspaceSlug, params.projectSlug);
|
|
9867
10026
|
});
|
|
9868
|
-
this.ipcServer.on("project
|
|
10027
|
+
this.ipcServer.on("project:eject", async (params) => {
|
|
9869
10028
|
return handleProjectEject(params);
|
|
9870
10029
|
});
|
|
10030
|
+
this.ipcServer.on("project:setup", async (params) => {
|
|
10031
|
+
return handleProjectSetup(params);
|
|
10032
|
+
});
|
|
9871
10033
|
}
|
|
9872
10034
|
/**
|
|
9873
10035
|
* Restore WebSocket connections for tracked projects
|
|
@@ -10000,7 +10162,7 @@ var Daemon = class _Daemon {
|
|
|
10000
10162
|
client.updateActivity();
|
|
10001
10163
|
try {
|
|
10002
10164
|
const gitCmd = message.command;
|
|
10003
|
-
const bareRepoPath =
|
|
10165
|
+
const bareRepoPath = path23.join(projectPath, ".bare");
|
|
10004
10166
|
const cwd = gitCmd.worktreePath || bareRepoPath;
|
|
10005
10167
|
if (gitCmd.worktreePath) {
|
|
10006
10168
|
console.log(`[Daemon] Routing command to worktree: ${gitCmd.worktreePath}`);
|
|
@@ -10114,9 +10276,14 @@ var Daemon = class _Daemon {
|
|
|
10114
10276
|
);
|
|
10115
10277
|
break;
|
|
10116
10278
|
// EP1144: Project ejection for Tier 1 cleanup
|
|
10117
|
-
|
|
10279
|
+
// EP1199: Renamed from eject_project to project:eject for naming consistency
|
|
10280
|
+
case "project:eject":
|
|
10118
10281
|
result = await handleProjectEject(cmd);
|
|
10119
10282
|
break;
|
|
10283
|
+
// EP1199: Project setup for cloud containers
|
|
10284
|
+
case "project:setup":
|
|
10285
|
+
result = await handleProjectSetup(cmd);
|
|
10286
|
+
break;
|
|
10120
10287
|
default:
|
|
10121
10288
|
result = {
|
|
10122
10289
|
success: false,
|
|
@@ -10580,8 +10747,8 @@ var Daemon = class _Daemon {
|
|
|
10580
10747
|
let daemonPid;
|
|
10581
10748
|
try {
|
|
10582
10749
|
const pidPath = getPidFilePath();
|
|
10583
|
-
if (
|
|
10584
|
-
const pidStr =
|
|
10750
|
+
if (fs22.existsSync(pidPath)) {
|
|
10751
|
+
const pidStr = fs22.readFileSync(pidPath, "utf-8").trim();
|
|
10585
10752
|
daemonPid = parseInt(pidStr, 10);
|
|
10586
10753
|
}
|
|
10587
10754
|
} catch (pidError) {
|
|
@@ -10662,28 +10829,28 @@ var Daemon = class _Daemon {
|
|
|
10662
10829
|
* - workDir: The directory to run git commands in (cwd)
|
|
10663
10830
|
*/
|
|
10664
10831
|
getGitDirs(projectPath) {
|
|
10665
|
-
const bareDir =
|
|
10666
|
-
const gitPath =
|
|
10667
|
-
if (
|
|
10832
|
+
const bareDir = path23.join(projectPath, ".bare");
|
|
10833
|
+
const gitPath = path23.join(projectPath, ".git");
|
|
10834
|
+
if (fs22.existsSync(bareDir) && fs22.statSync(bareDir).isDirectory()) {
|
|
10668
10835
|
return { gitDir: bareDir, workDir: projectPath };
|
|
10669
10836
|
}
|
|
10670
|
-
if (
|
|
10837
|
+
if (fs22.existsSync(gitPath) && fs22.statSync(gitPath).isDirectory()) {
|
|
10671
10838
|
return { gitDir: null, workDir: projectPath };
|
|
10672
10839
|
}
|
|
10673
|
-
if (
|
|
10840
|
+
if (fs22.existsSync(gitPath) && fs22.statSync(gitPath).isFile()) {
|
|
10674
10841
|
return { gitDir: null, workDir: projectPath };
|
|
10675
10842
|
}
|
|
10676
|
-
const entries =
|
|
10843
|
+
const entries = fs22.readdirSync(projectPath, { withFileTypes: true });
|
|
10677
10844
|
for (const entry of entries) {
|
|
10678
10845
|
if (entry.isDirectory() && entry.name.startsWith("EP")) {
|
|
10679
|
-
const worktreePath =
|
|
10680
|
-
const worktreeGit =
|
|
10681
|
-
if (
|
|
10846
|
+
const worktreePath = path23.join(projectPath, entry.name);
|
|
10847
|
+
const worktreeGit = path23.join(worktreePath, ".git");
|
|
10848
|
+
if (fs22.existsSync(worktreeGit)) {
|
|
10682
10849
|
return { gitDir: null, workDir: worktreePath };
|
|
10683
10850
|
}
|
|
10684
10851
|
}
|
|
10685
10852
|
}
|
|
10686
|
-
if (
|
|
10853
|
+
if (fs22.existsSync(bareDir)) {
|
|
10687
10854
|
return { gitDir: bareDir, workDir: projectPath };
|
|
10688
10855
|
}
|
|
10689
10856
|
return { gitDir: null, workDir: projectPath };
|
|
@@ -10747,24 +10914,24 @@ var Daemon = class _Daemon {
|
|
|
10747
10914
|
async installGitHooks(projectPath) {
|
|
10748
10915
|
const hooks = ["post-checkout", "pre-commit", "post-commit"];
|
|
10749
10916
|
let hooksDir;
|
|
10750
|
-
const bareHooksDir =
|
|
10751
|
-
const gitHooksDir =
|
|
10752
|
-
if (
|
|
10917
|
+
const bareHooksDir = path23.join(projectPath, ".bare", "hooks");
|
|
10918
|
+
const gitHooksDir = path23.join(projectPath, ".git", "hooks");
|
|
10919
|
+
if (fs22.existsSync(bareHooksDir)) {
|
|
10753
10920
|
hooksDir = bareHooksDir;
|
|
10754
|
-
} else if (
|
|
10921
|
+
} else if (fs22.existsSync(gitHooksDir) && fs22.statSync(path23.join(projectPath, ".git")).isDirectory()) {
|
|
10755
10922
|
hooksDir = gitHooksDir;
|
|
10756
10923
|
} else {
|
|
10757
|
-
const parentBareHooks =
|
|
10758
|
-
if (
|
|
10924
|
+
const parentBareHooks = path23.join(projectPath, "..", ".bare", "hooks");
|
|
10925
|
+
if (fs22.existsSync(parentBareHooks)) {
|
|
10759
10926
|
hooksDir = parentBareHooks;
|
|
10760
10927
|
} else {
|
|
10761
10928
|
console.warn(`[Daemon] Hooks directory not found for: ${projectPath}`);
|
|
10762
10929
|
return;
|
|
10763
10930
|
}
|
|
10764
10931
|
}
|
|
10765
|
-
if (!
|
|
10932
|
+
if (!fs22.existsSync(hooksDir)) {
|
|
10766
10933
|
try {
|
|
10767
|
-
|
|
10934
|
+
fs22.mkdirSync(hooksDir, { recursive: true });
|
|
10768
10935
|
} catch (error) {
|
|
10769
10936
|
console.warn(`[Daemon] Hooks directory not found and could not create: ${hooksDir}`);
|
|
10770
10937
|
return;
|
|
@@ -10772,20 +10939,20 @@ var Daemon = class _Daemon {
|
|
|
10772
10939
|
}
|
|
10773
10940
|
for (const hookName of hooks) {
|
|
10774
10941
|
try {
|
|
10775
|
-
const hookPath =
|
|
10776
|
-
const bundledHookPath =
|
|
10777
|
-
if (!
|
|
10942
|
+
const hookPath = path23.join(hooksDir, hookName);
|
|
10943
|
+
const bundledHookPath = path23.join(__dirname, "..", "hooks", hookName);
|
|
10944
|
+
if (!fs22.existsSync(bundledHookPath)) {
|
|
10778
10945
|
console.warn(`[Daemon] Bundled hook not found: ${bundledHookPath}`);
|
|
10779
10946
|
continue;
|
|
10780
10947
|
}
|
|
10781
|
-
const hookContent =
|
|
10782
|
-
if (
|
|
10783
|
-
const existingContent =
|
|
10948
|
+
const hookContent = fs22.readFileSync(bundledHookPath, "utf-8");
|
|
10949
|
+
if (fs22.existsSync(hookPath)) {
|
|
10950
|
+
const existingContent = fs22.readFileSync(hookPath, "utf-8");
|
|
10784
10951
|
if (existingContent === hookContent) {
|
|
10785
10952
|
continue;
|
|
10786
10953
|
}
|
|
10787
10954
|
}
|
|
10788
|
-
|
|
10955
|
+
fs22.writeFileSync(hookPath, hookContent, { mode: 493 });
|
|
10789
10956
|
console.log(`[Daemon] Installed git hook: ${hookName}`);
|
|
10790
10957
|
} catch (error) {
|
|
10791
10958
|
console.warn(`[Daemon] Failed to install ${hookName} hook:`, error instanceof Error ? error.message : error);
|
|
@@ -11750,6 +11917,10 @@ var Daemon = class _Daemon {
|
|
|
11750
11917
|
if (this.shuttingDown) return;
|
|
11751
11918
|
this.shuttingDown = true;
|
|
11752
11919
|
console.log("[Daemon] Shutting down...");
|
|
11920
|
+
if (this.healthServer) {
|
|
11921
|
+
this.healthServer.close();
|
|
11922
|
+
this.healthServer = null;
|
|
11923
|
+
}
|
|
11753
11924
|
this.stopTunnelPolling();
|
|
11754
11925
|
this.stopHealthCheckPolling();
|
|
11755
11926
|
for (const [projectPath, connection] of this.connections) {
|
|
@@ -11785,8 +11956,8 @@ var Daemon = class _Daemon {
|
|
|
11785
11956
|
await this.shutdown();
|
|
11786
11957
|
try {
|
|
11787
11958
|
const pidPath = getPidFilePath();
|
|
11788
|
-
if (
|
|
11789
|
-
|
|
11959
|
+
if (fs22.existsSync(pidPath)) {
|
|
11960
|
+
fs22.unlinkSync(pidPath);
|
|
11790
11961
|
console.log("[Daemon] PID file cleaned up");
|
|
11791
11962
|
}
|
|
11792
11963
|
} catch (error) {
|