@episoda/cli 0.2.174 → 0.2.176
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 +496 -141
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +375 -138
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1640,15 +1640,15 @@ var require_git_executor = __commonJS({
|
|
|
1640
1640
|
try {
|
|
1641
1641
|
const { stdout: gitDir } = await execAsync("git rev-parse --git-dir", { cwd, timeout: 5e3 });
|
|
1642
1642
|
const gitDirPath = gitDir.trim();
|
|
1643
|
-
const
|
|
1643
|
+
const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1644
1644
|
const rebaseMergePath = `${gitDirPath}/rebase-merge`;
|
|
1645
1645
|
const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
|
|
1646
1646
|
try {
|
|
1647
|
-
await
|
|
1647
|
+
await fs17.access(rebaseMergePath);
|
|
1648
1648
|
inRebase = true;
|
|
1649
1649
|
} catch {
|
|
1650
1650
|
try {
|
|
1651
|
-
await
|
|
1651
|
+
await fs17.access(rebaseApplyPath);
|
|
1652
1652
|
inRebase = true;
|
|
1653
1653
|
} catch {
|
|
1654
1654
|
inRebase = false;
|
|
@@ -1704,9 +1704,9 @@ var require_git_executor = __commonJS({
|
|
|
1704
1704
|
};
|
|
1705
1705
|
}
|
|
1706
1706
|
}
|
|
1707
|
-
const
|
|
1707
|
+
const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1708
1708
|
try {
|
|
1709
|
-
await
|
|
1709
|
+
await fs17.access(command.path);
|
|
1710
1710
|
return {
|
|
1711
1711
|
success: false,
|
|
1712
1712
|
error: "WORKTREE_EXISTS",
|
|
@@ -1765,9 +1765,9 @@ var require_git_executor = __commonJS({
|
|
|
1765
1765
|
*/
|
|
1766
1766
|
async executeWorktreeRemove(command, cwd, options) {
|
|
1767
1767
|
try {
|
|
1768
|
-
const
|
|
1768
|
+
const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1769
1769
|
try {
|
|
1770
|
-
await
|
|
1770
|
+
await fs17.access(command.path);
|
|
1771
1771
|
} catch {
|
|
1772
1772
|
return {
|
|
1773
1773
|
success: false,
|
|
@@ -1802,7 +1802,7 @@ var require_git_executor = __commonJS({
|
|
|
1802
1802
|
const result = await this.runGitCommand(args, cwd, options);
|
|
1803
1803
|
if (result.success) {
|
|
1804
1804
|
try {
|
|
1805
|
-
await
|
|
1805
|
+
await fs17.rm(command.path, { recursive: true, force: true });
|
|
1806
1806
|
} catch {
|
|
1807
1807
|
}
|
|
1808
1808
|
return {
|
|
@@ -1936,10 +1936,10 @@ var require_git_executor = __commonJS({
|
|
|
1936
1936
|
*/
|
|
1937
1937
|
async executeCloneBare(command, options) {
|
|
1938
1938
|
try {
|
|
1939
|
-
const
|
|
1940
|
-
const
|
|
1939
|
+
const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
1940
|
+
const path20 = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1941
1941
|
try {
|
|
1942
|
-
await
|
|
1942
|
+
await fs17.access(command.path);
|
|
1943
1943
|
return {
|
|
1944
1944
|
success: false,
|
|
1945
1945
|
error: "BRANCH_ALREADY_EXISTS",
|
|
@@ -1948,9 +1948,9 @@ var require_git_executor = __commonJS({
|
|
|
1948
1948
|
};
|
|
1949
1949
|
} catch {
|
|
1950
1950
|
}
|
|
1951
|
-
const parentDir =
|
|
1951
|
+
const parentDir = path20.dirname(command.path);
|
|
1952
1952
|
try {
|
|
1953
|
-
await
|
|
1953
|
+
await fs17.mkdir(parentDir, { recursive: true });
|
|
1954
1954
|
} catch {
|
|
1955
1955
|
}
|
|
1956
1956
|
const { stdout, stderr } = await execAsync(
|
|
@@ -1998,22 +1998,22 @@ var require_git_executor = __commonJS({
|
|
|
1998
1998
|
*/
|
|
1999
1999
|
async executeProjectInfo(cwd, options) {
|
|
2000
2000
|
try {
|
|
2001
|
-
const
|
|
2002
|
-
const
|
|
2001
|
+
const fs17 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
|
|
2002
|
+
const path20 = await Promise.resolve().then(() => __importStar(require("path")));
|
|
2003
2003
|
let currentPath = cwd;
|
|
2004
2004
|
let projectPath = cwd;
|
|
2005
2005
|
let bareRepoPath;
|
|
2006
2006
|
for (let i = 0; i < 10; i++) {
|
|
2007
|
-
const bareDir =
|
|
2008
|
-
const episodaDir =
|
|
2007
|
+
const bareDir = path20.join(currentPath, ".bare");
|
|
2008
|
+
const episodaDir = path20.join(currentPath, ".episoda");
|
|
2009
2009
|
try {
|
|
2010
|
-
await
|
|
2011
|
-
await
|
|
2010
|
+
await fs17.access(bareDir);
|
|
2011
|
+
await fs17.access(episodaDir);
|
|
2012
2012
|
projectPath = currentPath;
|
|
2013
2013
|
bareRepoPath = bareDir;
|
|
2014
2014
|
break;
|
|
2015
2015
|
} catch {
|
|
2016
|
-
const parentPath =
|
|
2016
|
+
const parentPath = path20.dirname(currentPath);
|
|
2017
2017
|
if (parentPath === currentPath) {
|
|
2018
2018
|
break;
|
|
2019
2019
|
}
|
|
@@ -2187,6 +2187,7 @@ var require_websocket_client = __commonJS({
|
|
|
2187
2187
|
constructor() {
|
|
2188
2188
|
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
2189
2189
|
this.reconnectAttempts = 0;
|
|
2190
|
+
this.autoReconnectEnabled = true;
|
|
2190
2191
|
this.url = "";
|
|
2191
2192
|
this.token = "";
|
|
2192
2193
|
this.isConnected = false;
|
|
@@ -2235,7 +2236,7 @@ var require_websocket_client = __commonJS({
|
|
|
2235
2236
|
clearTimeout(this.reconnectTimeout);
|
|
2236
2237
|
this.reconnectTimeout = void 0;
|
|
2237
2238
|
}
|
|
2238
|
-
return new Promise((
|
|
2239
|
+
return new Promise((resolve6, reject) => {
|
|
2239
2240
|
const connectionTimeout = setTimeout(() => {
|
|
2240
2241
|
if (this.ws) {
|
|
2241
2242
|
this.ws.terminate();
|
|
@@ -2266,7 +2267,7 @@ var require_websocket_client = __commonJS({
|
|
|
2266
2267
|
daemonPid: this.daemonPid
|
|
2267
2268
|
});
|
|
2268
2269
|
this.startHeartbeat();
|
|
2269
|
-
|
|
2270
|
+
resolve6();
|
|
2270
2271
|
});
|
|
2271
2272
|
this.ws.on("pong", () => {
|
|
2272
2273
|
if (this.heartbeatTimeoutTimer) {
|
|
@@ -2288,7 +2289,7 @@ var require_websocket_client = __commonJS({
|
|
|
2288
2289
|
this.ws.on("close", (code, reason) => {
|
|
2289
2290
|
console.log(`[EpisodaClient] WebSocket closed: ${code} ${reason.toString()}`);
|
|
2290
2291
|
this.isConnected = false;
|
|
2291
|
-
const willReconnect = !this.isDisconnecting;
|
|
2292
|
+
const willReconnect = !this.isDisconnecting && this.autoReconnectEnabled;
|
|
2292
2293
|
this.emit({
|
|
2293
2294
|
type: "disconnected",
|
|
2294
2295
|
code,
|
|
@@ -2312,6 +2313,19 @@ var require_websocket_client = __commonJS({
|
|
|
2312
2313
|
}
|
|
2313
2314
|
});
|
|
2314
2315
|
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Enable/disable automatic reconnect scheduling.
|
|
2318
|
+
*
|
|
2319
|
+
* This lets daemon startup/handshake own retry policy without competing
|
|
2320
|
+
* with client-side reconnect loops.
|
|
2321
|
+
*/
|
|
2322
|
+
setAutoReconnect(enabled) {
|
|
2323
|
+
this.autoReconnectEnabled = enabled;
|
|
2324
|
+
if (!enabled && this.reconnectTimeout) {
|
|
2325
|
+
clearTimeout(this.reconnectTimeout);
|
|
2326
|
+
this.reconnectTimeout = void 0;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2315
2329
|
/**
|
|
2316
2330
|
* Disconnect from the server
|
|
2317
2331
|
* @param intentional - If true, prevents automatic reconnection (user-initiated disconnect)
|
|
@@ -2397,13 +2411,13 @@ var require_websocket_client = __commonJS({
|
|
|
2397
2411
|
console.warn("[EpisodaClient] Cannot send - WebSocket not connected");
|
|
2398
2412
|
return false;
|
|
2399
2413
|
}
|
|
2400
|
-
return new Promise((
|
|
2414
|
+
return new Promise((resolve6) => {
|
|
2401
2415
|
this.ws.send(JSON.stringify(message), (error) => {
|
|
2402
2416
|
if (error) {
|
|
2403
2417
|
console.error("[EpisodaClient] Failed to send message:", error);
|
|
2404
|
-
|
|
2418
|
+
resolve6(false);
|
|
2405
2419
|
} else {
|
|
2406
|
-
|
|
2420
|
+
resolve6(true);
|
|
2407
2421
|
}
|
|
2408
2422
|
});
|
|
2409
2423
|
});
|
|
@@ -2526,7 +2540,12 @@ var require_websocket_client = __commonJS({
|
|
|
2526
2540
|
if (this.rateLimitBackoffUntil && Date.now() < this.rateLimitBackoffUntil) {
|
|
2527
2541
|
const waitTime = this.rateLimitBackoffUntil - Date.now();
|
|
2528
2542
|
console.log(`[EpisodaClient] Rate limited, waiting ${Math.round(waitTime / 1e3)}s before retry`);
|
|
2529
|
-
this.
|
|
2543
|
+
this.emit({
|
|
2544
|
+
type: "reconnect_scheduled",
|
|
2545
|
+
attempt: this.reconnectAttempts + 1,
|
|
2546
|
+
delayMs: waitTime,
|
|
2547
|
+
strategy: "rate_limited"
|
|
2548
|
+
});
|
|
2530
2549
|
this.reconnectTimeout = setTimeout(() => {
|
|
2531
2550
|
this.rateLimitBackoffUntil = void 0;
|
|
2532
2551
|
this.scheduleReconnect();
|
|
@@ -2534,6 +2553,7 @@ var require_websocket_client = __commonJS({
|
|
|
2534
2553
|
return;
|
|
2535
2554
|
}
|
|
2536
2555
|
let delay;
|
|
2556
|
+
let strategy = "connection_lost";
|
|
2537
2557
|
let shouldRetry = true;
|
|
2538
2558
|
const isCloudMode = this.environment === "cloud";
|
|
2539
2559
|
const MAX_CLOUD_AUTH_FAILURES = 3;
|
|
@@ -2545,20 +2565,28 @@ var require_websocket_client = __commonJS({
|
|
|
2545
2565
|
} else {
|
|
2546
2566
|
delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_CLOUD_RECONNECT_DELAY);
|
|
2547
2567
|
delay = applyJitter(delay);
|
|
2568
|
+
strategy = "cloud";
|
|
2548
2569
|
const delayStr = delay >= 6e4 ? `${Math.round(delay / 6e4)}m` : `${Math.round(delay / 1e3)}s`;
|
|
2549
2570
|
console.log(`[EpisodaClient] Cloud mode: reconnecting in ${delayStr}... (attempt ${this.reconnectAttempts + 1}, never giving up)`);
|
|
2550
2571
|
}
|
|
2551
2572
|
} else if (this.isGracefulShutdown && this.reconnectAttempts < 7) {
|
|
2552
2573
|
delay = Math.min(500 * Math.pow(2, this.reconnectAttempts), 5e3);
|
|
2553
2574
|
delay = applyJitter(delay);
|
|
2575
|
+
strategy = "graceful_shutdown";
|
|
2554
2576
|
console.log(`[EpisodaClient] Server restarting, reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/7)`);
|
|
2555
2577
|
} else {
|
|
2556
2578
|
delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_LOCAL_RECONNECT_DELAY);
|
|
2557
2579
|
delay = applyJitter(delay);
|
|
2580
|
+
strategy = "connection_lost";
|
|
2558
2581
|
const delayStr = delay >= 6e4 ? `${Math.round(delay / 6e4)}m` : `${Math.round(delay / 1e3)}s`;
|
|
2559
2582
|
console.log(`[EpisodaClient] Connection lost, retrying in ${delayStr}... (attempt ${this.reconnectAttempts + 1}, retrying until connected)`);
|
|
2560
2583
|
}
|
|
2561
2584
|
if (!shouldRetry) {
|
|
2585
|
+
this.emit({
|
|
2586
|
+
type: "reconnect_exhausted",
|
|
2587
|
+
attempts: this.reconnectAttempts,
|
|
2588
|
+
reason: `retry_exhausted_after_${this.consecutiveAuthFailures}_auth_failures`
|
|
2589
|
+
});
|
|
2562
2590
|
this.emit({
|
|
2563
2591
|
type: "disconnected",
|
|
2564
2592
|
code: 1006,
|
|
@@ -2567,9 +2595,20 @@ var require_websocket_client = __commonJS({
|
|
|
2567
2595
|
});
|
|
2568
2596
|
return;
|
|
2569
2597
|
}
|
|
2570
|
-
this.reconnectAttempts
|
|
2598
|
+
const reconnectAttempt = this.reconnectAttempts + 1;
|
|
2599
|
+
this.reconnectAttempts = reconnectAttempt;
|
|
2600
|
+
this.emit({
|
|
2601
|
+
type: "reconnect_scheduled",
|
|
2602
|
+
attempt: reconnectAttempt,
|
|
2603
|
+
delayMs: delay,
|
|
2604
|
+
strategy
|
|
2605
|
+
});
|
|
2571
2606
|
this.reconnectTimeout = setTimeout(() => {
|
|
2572
2607
|
console.log("[EpisodaClient] Attempting reconnection...");
|
|
2608
|
+
this.emit({
|
|
2609
|
+
type: "reconnect_attempt",
|
|
2610
|
+
attempt: reconnectAttempt
|
|
2611
|
+
});
|
|
2573
2612
|
this.connect(this.url, this.token, this.machineId, {
|
|
2574
2613
|
hostname: this.hostname,
|
|
2575
2614
|
osPlatform: this.osPlatform,
|
|
@@ -2582,6 +2621,11 @@ var require_websocket_client = __commonJS({
|
|
|
2582
2621
|
containerId: this.containerId
|
|
2583
2622
|
}).then(() => {
|
|
2584
2623
|
console.log("[EpisodaClient] Reconnection successful");
|
|
2624
|
+
this.emit({
|
|
2625
|
+
type: "reconnect_result",
|
|
2626
|
+
attempt: reconnectAttempt,
|
|
2627
|
+
success: true
|
|
2628
|
+
});
|
|
2585
2629
|
this.reconnectAttempts = 0;
|
|
2586
2630
|
this.isGracefulShutdown = false;
|
|
2587
2631
|
this.firstDisconnectTime = void 0;
|
|
@@ -2589,6 +2633,12 @@ var require_websocket_client = __commonJS({
|
|
|
2589
2633
|
this.consecutiveAuthFailures = 0;
|
|
2590
2634
|
}).catch((error) => {
|
|
2591
2635
|
console.error("[EpisodaClient] Reconnection failed:", error.message);
|
|
2636
|
+
this.emit({
|
|
2637
|
+
type: "reconnect_result",
|
|
2638
|
+
attempt: reconnectAttempt,
|
|
2639
|
+
success: false,
|
|
2640
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2641
|
+
});
|
|
2592
2642
|
});
|
|
2593
2643
|
}, delay);
|
|
2594
2644
|
}
|
|
@@ -2669,38 +2719,38 @@ var require_auth = __commonJS({
|
|
|
2669
2719
|
};
|
|
2670
2720
|
})();
|
|
2671
2721
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
2672
|
-
exports2.getConfigDir =
|
|
2722
|
+
exports2.getConfigDir = getConfigDir7;
|
|
2673
2723
|
exports2.getConfigPath = getConfigPath4;
|
|
2674
2724
|
exports2.loadConfig = loadConfig9;
|
|
2675
2725
|
exports2.saveConfig = saveConfig3;
|
|
2676
2726
|
exports2.validateToken = validateToken;
|
|
2677
|
-
var
|
|
2678
|
-
var
|
|
2727
|
+
var fs17 = __importStar(require("fs"));
|
|
2728
|
+
var path20 = __importStar(require("path"));
|
|
2679
2729
|
var os5 = __importStar(require("os"));
|
|
2680
2730
|
var child_process_1 = require("child_process");
|
|
2681
2731
|
var DEFAULT_CONFIG_FILE = "config.json";
|
|
2682
2732
|
var hasWarnedMissingProjectId = false;
|
|
2683
2733
|
var hasWarnedMissingRequiredFields = false;
|
|
2684
|
-
function
|
|
2685
|
-
return process.env.EPISODA_CONFIG_DIR ||
|
|
2734
|
+
function getConfigDir7() {
|
|
2735
|
+
return process.env.EPISODA_CONFIG_DIR || path20.join(os5.homedir(), ".episoda");
|
|
2686
2736
|
}
|
|
2687
2737
|
function getConfigPath4(configPath) {
|
|
2688
2738
|
if (configPath) {
|
|
2689
2739
|
return configPath;
|
|
2690
2740
|
}
|
|
2691
|
-
return
|
|
2741
|
+
return path20.join(getConfigDir7(), DEFAULT_CONFIG_FILE);
|
|
2692
2742
|
}
|
|
2693
2743
|
function ensureConfigDir(configPath) {
|
|
2694
|
-
const dir =
|
|
2695
|
-
const isNew = !
|
|
2744
|
+
const dir = path20.dirname(configPath);
|
|
2745
|
+
const isNew = !fs17.existsSync(dir);
|
|
2696
2746
|
if (isNew) {
|
|
2697
|
-
|
|
2747
|
+
fs17.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2698
2748
|
}
|
|
2699
2749
|
if (process.platform === "darwin") {
|
|
2700
|
-
const nosyncPath =
|
|
2701
|
-
if (isNew || !
|
|
2750
|
+
const nosyncPath = path20.join(dir, ".nosync");
|
|
2751
|
+
if (isNew || !fs17.existsSync(nosyncPath)) {
|
|
2702
2752
|
try {
|
|
2703
|
-
|
|
2753
|
+
fs17.writeFileSync(nosyncPath, "", { mode: 384 });
|
|
2704
2754
|
(0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
|
|
2705
2755
|
stdio: "ignore",
|
|
2706
2756
|
timeout: 5e3
|
|
@@ -2714,11 +2764,11 @@ var require_auth = __commonJS({
|
|
|
2714
2764
|
const fullPath = getConfigPath4(configPath);
|
|
2715
2765
|
const isCloudMode = process.env.EPISODA_MODE === "cloud";
|
|
2716
2766
|
const readConfigFile = (pathToFile) => {
|
|
2717
|
-
if (!
|
|
2767
|
+
if (!fs17.existsSync(pathToFile)) {
|
|
2718
2768
|
return null;
|
|
2719
2769
|
}
|
|
2720
2770
|
try {
|
|
2721
|
-
const content =
|
|
2771
|
+
const content = fs17.readFileSync(pathToFile, "utf8");
|
|
2722
2772
|
return JSON.parse(content);
|
|
2723
2773
|
} catch (error) {
|
|
2724
2774
|
console.error("Error loading config:", error);
|
|
@@ -2757,11 +2807,11 @@ var require_auth = __commonJS({
|
|
|
2757
2807
|
}
|
|
2758
2808
|
const homeDir = process.env.HOME || require("os").homedir();
|
|
2759
2809
|
const workspaceConfigPath = require("path").join(homeDir, "episoda", process.env.EPISODA_WORKSPACE, ".episoda", "config.json");
|
|
2760
|
-
if (!
|
|
2810
|
+
if (!fs17.existsSync(workspaceConfigPath)) {
|
|
2761
2811
|
return null;
|
|
2762
2812
|
}
|
|
2763
2813
|
try {
|
|
2764
|
-
const content =
|
|
2814
|
+
const content = fs17.readFileSync(workspaceConfigPath, "utf8");
|
|
2765
2815
|
const workspaceConfig2 = JSON.parse(content);
|
|
2766
2816
|
const expiresAtEnv = envValue(process.env.EPISODA_ACCESS_TOKEN_EXPIRES_AT);
|
|
2767
2817
|
return {
|
|
@@ -2789,11 +2839,11 @@ var require_auth = __commonJS({
|
|
|
2789
2839
|
}
|
|
2790
2840
|
const homeDir = process.env.HOME || require("os").homedir();
|
|
2791
2841
|
const projectConfigPath = require("path").join(homeDir, "episoda", workspaceSlug, projectSlug, ".episoda", "config.json");
|
|
2792
|
-
if (!
|
|
2842
|
+
if (!fs17.existsSync(projectConfigPath)) {
|
|
2793
2843
|
return null;
|
|
2794
2844
|
}
|
|
2795
2845
|
try {
|
|
2796
|
-
const content =
|
|
2846
|
+
const content = fs17.readFileSync(projectConfigPath, "utf8");
|
|
2797
2847
|
const projectConfig2 = JSON.parse(content);
|
|
2798
2848
|
return {
|
|
2799
2849
|
project_id: projectConfig2.projectId || projectConfig2.project_id,
|
|
@@ -2861,7 +2911,7 @@ var require_auth = __commonJS({
|
|
|
2861
2911
|
ensureConfigDir(fullPath);
|
|
2862
2912
|
try {
|
|
2863
2913
|
const content = JSON.stringify(config, null, 2);
|
|
2864
|
-
|
|
2914
|
+
fs17.writeFileSync(fullPath, content, { mode: 384 });
|
|
2865
2915
|
} catch (error) {
|
|
2866
2916
|
throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
|
|
2867
2917
|
}
|
|
@@ -3020,6 +3070,7 @@ var status = {
|
|
|
3020
3070
|
// src/daemon/daemon-manager.ts
|
|
3021
3071
|
var fs = __toESM(require("fs"));
|
|
3022
3072
|
var path = __toESM(require("path"));
|
|
3073
|
+
var net = __toESM(require("net"));
|
|
3023
3074
|
var import_child_process = require("child_process");
|
|
3024
3075
|
var import_crypto = require("crypto");
|
|
3025
3076
|
var import_core = __toESM(require_dist());
|
|
@@ -3032,6 +3083,20 @@ function logSignalDispatch(input) {
|
|
|
3032
3083
|
};
|
|
3033
3084
|
console.log(`[DaemonManager] SignalDispatch ${JSON.stringify(payload)}`);
|
|
3034
3085
|
}
|
|
3086
|
+
function logReliabilityMetric(metric, fields) {
|
|
3087
|
+
const line = `[Daemon][Metric] ${JSON.stringify({
|
|
3088
|
+
metric,
|
|
3089
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3090
|
+
...fields
|
|
3091
|
+
})}`;
|
|
3092
|
+
console.log(line);
|
|
3093
|
+
try {
|
|
3094
|
+
const daemonLogPath = path.join((0, import_core.getConfigDir)(), "daemon.log");
|
|
3095
|
+
fs.appendFileSync(daemonLogPath, `${line}
|
|
3096
|
+
`, "utf-8");
|
|
3097
|
+
} catch {
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3035
3100
|
function killAllEpisodaProcesses() {
|
|
3036
3101
|
const currentPid = process.pid;
|
|
3037
3102
|
let killedCount = 0;
|
|
@@ -3084,6 +3149,111 @@ function killAllEpisodaProcesses() {
|
|
|
3084
3149
|
function getPidFilePath() {
|
|
3085
3150
|
return path.join((0, import_core.getConfigDir)(), "daemon.pid");
|
|
3086
3151
|
}
|
|
3152
|
+
function resolveDaemonEntryScript() {
|
|
3153
|
+
const packagedPath = path.join(__dirname, "daemon", "daemon-process.js");
|
|
3154
|
+
if (fs.existsSync(packagedPath)) {
|
|
3155
|
+
return packagedPath;
|
|
3156
|
+
}
|
|
3157
|
+
const sourceFallbackPath = path.resolve(__dirname, "../../dist/daemon/daemon-process.js");
|
|
3158
|
+
if (fs.existsSync(sourceFallbackPath)) {
|
|
3159
|
+
return sourceFallbackPath;
|
|
3160
|
+
}
|
|
3161
|
+
throw new Error(
|
|
3162
|
+
`Daemon script not found. Checked: ${packagedPath}, ${sourceFallbackPath}. Make sure CLI is built.`
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
async function pingDaemonSocket(socketPath, timeoutMs) {
|
|
3166
|
+
await new Promise((resolve6, reject) => {
|
|
3167
|
+
const socket = net.createConnection(socketPath);
|
|
3168
|
+
const requestId = (0, import_crypto.randomUUID)();
|
|
3169
|
+
let settled = false;
|
|
3170
|
+
let buffer = "";
|
|
3171
|
+
const finish = (fn) => {
|
|
3172
|
+
if (settled) return;
|
|
3173
|
+
settled = true;
|
|
3174
|
+
clearTimeout(timer);
|
|
3175
|
+
socket.removeAllListeners();
|
|
3176
|
+
socket.destroy();
|
|
3177
|
+
fn();
|
|
3178
|
+
};
|
|
3179
|
+
const timer = setTimeout(() => {
|
|
3180
|
+
finish(() => reject(new Error("IPC ping timeout")));
|
|
3181
|
+
}, timeoutMs);
|
|
3182
|
+
socket.on("connect", () => {
|
|
3183
|
+
socket.write(JSON.stringify({ id: requestId, command: "ping", params: {} }) + "\n");
|
|
3184
|
+
});
|
|
3185
|
+
socket.on("data", (chunk) => {
|
|
3186
|
+
buffer += chunk.toString("utf8");
|
|
3187
|
+
const parsed = consumeIpcPingBufferForRequest(buffer, requestId);
|
|
3188
|
+
buffer = parsed.remainingBuffer;
|
|
3189
|
+
if (parsed.result === "pending") return;
|
|
3190
|
+
if (parsed.result === "success") {
|
|
3191
|
+
finish(() => resolve6());
|
|
3192
|
+
return;
|
|
3193
|
+
}
|
|
3194
|
+
finish(() => reject(new Error(parsed.error || "IPC ping failed")));
|
|
3195
|
+
});
|
|
3196
|
+
socket.on("error", (error) => {
|
|
3197
|
+
finish(() => reject(error));
|
|
3198
|
+
});
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
function consumeIpcPingBufferForRequest(buffer, requestId) {
|
|
3202
|
+
let remaining = buffer;
|
|
3203
|
+
while (true) {
|
|
3204
|
+
const newlineIndex = remaining.indexOf("\n");
|
|
3205
|
+
if (newlineIndex === -1) {
|
|
3206
|
+
return { result: "pending", remainingBuffer: remaining };
|
|
3207
|
+
}
|
|
3208
|
+
const frame = remaining.slice(0, newlineIndex).trim();
|
|
3209
|
+
remaining = remaining.slice(newlineIndex + 1);
|
|
3210
|
+
if (!frame) continue;
|
|
3211
|
+
let parsed = null;
|
|
3212
|
+
try {
|
|
3213
|
+
parsed = JSON.parse(frame);
|
|
3214
|
+
} catch {
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
if (!parsed || parsed.id !== requestId) {
|
|
3218
|
+
continue;
|
|
3219
|
+
}
|
|
3220
|
+
if (parsed.success) {
|
|
3221
|
+
return { result: "success", remainingBuffer: remaining };
|
|
3222
|
+
}
|
|
3223
|
+
return {
|
|
3224
|
+
result: "error",
|
|
3225
|
+
remainingBuffer: remaining,
|
|
3226
|
+
error: parsed.error || "IPC ping failed"
|
|
3227
|
+
};
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
async function waitForDaemonIpcReady(timeoutMs = 15e3) {
|
|
3231
|
+
const socketPath = path.join((0, import_core.getConfigDir)(), "daemon.sock");
|
|
3232
|
+
const deadline = Date.now() + timeoutMs;
|
|
3233
|
+
let lastError = "unknown";
|
|
3234
|
+
while (Date.now() < deadline) {
|
|
3235
|
+
try {
|
|
3236
|
+
await pingDaemonSocket(socketPath, 1e3);
|
|
3237
|
+
return;
|
|
3238
|
+
} catch (error) {
|
|
3239
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
3240
|
+
await new Promise((resolve6) => setTimeout(resolve6, 200));
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
throw new Error(`Daemon IPC not ready within ${Math.round(timeoutMs / 1e3)}s (${lastError})`);
|
|
3244
|
+
}
|
|
3245
|
+
async function waitForProcessStart(pid, timeoutMs = 5e3) {
|
|
3246
|
+
const deadline = Date.now() + timeoutMs;
|
|
3247
|
+
while (Date.now() < deadline) {
|
|
3248
|
+
try {
|
|
3249
|
+
process.kill(pid, 0);
|
|
3250
|
+
return;
|
|
3251
|
+
} catch {
|
|
3252
|
+
await new Promise((resolve6) => setTimeout(resolve6, 100));
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
throw new Error(`Daemon process did not become alive within ${Math.round(timeoutMs / 1e3)}s`);
|
|
3256
|
+
}
|
|
3087
3257
|
function looksLikeEpisodaDaemonProcess(pid) {
|
|
3088
3258
|
if (process.platform === "win32") {
|
|
3089
3259
|
return true;
|
|
@@ -3128,6 +3298,7 @@ function isDaemonRunning() {
|
|
|
3128
3298
|
}
|
|
3129
3299
|
}
|
|
3130
3300
|
async function startDaemon() {
|
|
3301
|
+
const startTime = Date.now();
|
|
3131
3302
|
const existingPid = isDaemonRunning();
|
|
3132
3303
|
if (existingPid) {
|
|
3133
3304
|
throw new Error(`Daemon already running (PID: ${existingPid})`);
|
|
@@ -3136,10 +3307,7 @@ async function startDaemon() {
|
|
|
3136
3307
|
if (!fs.existsSync(configDir)) {
|
|
3137
3308
|
fs.mkdirSync(configDir, { recursive: true });
|
|
3138
3309
|
}
|
|
3139
|
-
const daemonScript =
|
|
3140
|
-
if (!fs.existsSync(daemonScript)) {
|
|
3141
|
-
throw new Error(`Daemon script not found: ${daemonScript}. Make sure CLI is built.`);
|
|
3142
|
-
}
|
|
3310
|
+
const daemonScript = resolveDaemonEntryScript();
|
|
3143
3311
|
const logPath = path.join(configDir, "daemon.log");
|
|
3144
3312
|
rotateDaemonLog(logPath);
|
|
3145
3313
|
const logFd = fs.openSync(logPath, "a");
|
|
@@ -3148,6 +3316,8 @@ async function startDaemon() {
|
|
|
3148
3316
|
// Run independently of parent
|
|
3149
3317
|
stdio: ["ignore", logFd, logFd],
|
|
3150
3318
|
// EP813: Redirect stdout/stderr to log file
|
|
3319
|
+
cwd: configDir,
|
|
3320
|
+
// Stable daemon context; never inherit transient shell cwd
|
|
3151
3321
|
env: {
|
|
3152
3322
|
...process.env,
|
|
3153
3323
|
EPISODA_DAEMON_MODE: "1"
|
|
@@ -3158,10 +3328,29 @@ async function startDaemon() {
|
|
|
3158
3328
|
const pid = child.pid;
|
|
3159
3329
|
const pidPath = getPidFilePath();
|
|
3160
3330
|
fs.writeFileSync(pidPath, pid.toString(), "utf-8");
|
|
3161
|
-
await
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3331
|
+
await waitForProcessStart(pid, 5e3);
|
|
3332
|
+
try {
|
|
3333
|
+
await waitForDaemonIpcReady(15e3);
|
|
3334
|
+
logReliabilityMetric("daemon_startup_ready", {
|
|
3335
|
+
pid,
|
|
3336
|
+
startupReadyLatencyMs: Date.now() - startTime
|
|
3337
|
+
});
|
|
3338
|
+
} catch (error) {
|
|
3339
|
+
try {
|
|
3340
|
+
logSignalDispatch({
|
|
3341
|
+
initiator: "startDaemon",
|
|
3342
|
+
pid,
|
|
3343
|
+
signal: "SIGTERM",
|
|
3344
|
+
reason: "ipc_readiness_failed"
|
|
3345
|
+
});
|
|
3346
|
+
process.kill(pid, "SIGTERM");
|
|
3347
|
+
} catch {
|
|
3348
|
+
}
|
|
3349
|
+
try {
|
|
3350
|
+
if (fs.existsSync(pidPath)) fs.unlinkSync(pidPath);
|
|
3351
|
+
} catch {
|
|
3352
|
+
}
|
|
3353
|
+
throw error;
|
|
3165
3354
|
}
|
|
3166
3355
|
return pid;
|
|
3167
3356
|
}
|
|
@@ -3186,7 +3375,7 @@ async function stopDaemon(timeout = 5e3) {
|
|
|
3186
3375
|
while (Date.now() - startTime < timeout) {
|
|
3187
3376
|
try {
|
|
3188
3377
|
process.kill(pid, 0);
|
|
3189
|
-
await new Promise((
|
|
3378
|
+
await new Promise((resolve6) => setTimeout(resolve6, 100));
|
|
3190
3379
|
} catch (error) {
|
|
3191
3380
|
const pidPath2 = getPidFilePath();
|
|
3192
3381
|
if (fs.existsSync(pidPath2)) {
|
|
@@ -3240,15 +3429,15 @@ function rotateDaemonLog(logPath) {
|
|
|
3240
3429
|
}
|
|
3241
3430
|
|
|
3242
3431
|
// src/ipc/ipc-client.ts
|
|
3243
|
-
var
|
|
3432
|
+
var net2 = __toESM(require("net"));
|
|
3244
3433
|
var path2 = __toESM(require("path"));
|
|
3245
3434
|
var crypto = __toESM(require("crypto"));
|
|
3246
3435
|
var import_core2 = __toESM(require_dist());
|
|
3247
3436
|
var getSocketPath = () => path2.join((0, import_core2.getConfigDir)(), "daemon.sock");
|
|
3248
3437
|
var DEFAULT_TIMEOUT = 15e3;
|
|
3249
3438
|
async function sendCommand(command, params, timeout = DEFAULT_TIMEOUT) {
|
|
3250
|
-
return new Promise((
|
|
3251
|
-
const socket =
|
|
3439
|
+
return new Promise((resolve6, reject) => {
|
|
3440
|
+
const socket = net2.createConnection(getSocketPath());
|
|
3252
3441
|
const requestId = crypto.randomUUID();
|
|
3253
3442
|
let buffer = "";
|
|
3254
3443
|
let timeoutHandle;
|
|
@@ -3278,7 +3467,7 @@ async function sendCommand(command, params, timeout = DEFAULT_TIMEOUT) {
|
|
|
3278
3467
|
clearTimeout(timeoutHandle);
|
|
3279
3468
|
socket.end();
|
|
3280
3469
|
if (response.success) {
|
|
3281
|
-
|
|
3470
|
+
resolve6(response.data);
|
|
3282
3471
|
} else {
|
|
3283
3472
|
reject(new Error(response.error || "Command failed"));
|
|
3284
3473
|
}
|
|
@@ -3305,7 +3494,7 @@ async function sendCommand(command, params, timeout = DEFAULT_TIMEOUT) {
|
|
|
3305
3494
|
}
|
|
3306
3495
|
async function isDaemonReachable() {
|
|
3307
3496
|
try {
|
|
3308
|
-
await sendCommand("ping", {},
|
|
3497
|
+
await sendCommand("ping", {}, 3e3);
|
|
3309
3498
|
return true;
|
|
3310
3499
|
} catch (error) {
|
|
3311
3500
|
return false;
|
|
@@ -3947,7 +4136,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
3947
4136
|
const lockContent = fs2.readFileSync(lockPath, "utf-8").trim();
|
|
3948
4137
|
const lockPid = parseInt(lockContent, 10);
|
|
3949
4138
|
if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
|
|
3950
|
-
await new Promise((
|
|
4139
|
+
await new Promise((resolve6) => setTimeout(resolve6, retryInterval));
|
|
3951
4140
|
continue;
|
|
3952
4141
|
}
|
|
3953
4142
|
} catch {
|
|
@@ -3961,7 +4150,7 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
3961
4150
|
} catch {
|
|
3962
4151
|
continue;
|
|
3963
4152
|
}
|
|
3964
|
-
await new Promise((
|
|
4153
|
+
await new Promise((resolve6) => setTimeout(resolve6, retryInterval));
|
|
3965
4154
|
continue;
|
|
3966
4155
|
}
|
|
3967
4156
|
throw err;
|
|
@@ -4298,7 +4487,7 @@ async function fetchProjectPath(config, projectId) {
|
|
|
4298
4487
|
return null;
|
|
4299
4488
|
}
|
|
4300
4489
|
}
|
|
4301
|
-
async function syncProjectPath(config, projectId,
|
|
4490
|
+
async function syncProjectPath(config, projectId, path20) {
|
|
4302
4491
|
const machineUuid = config.machine_uuid || config.device_id;
|
|
4303
4492
|
if (!machineUuid || !config.access_token) {
|
|
4304
4493
|
return false;
|
|
@@ -4312,7 +4501,7 @@ async function syncProjectPath(config, projectId, path19) {
|
|
|
4312
4501
|
"Authorization": `Bearer ${config.access_token}`,
|
|
4313
4502
|
"Content-Type": "application/json"
|
|
4314
4503
|
},
|
|
4315
|
-
body: JSON.stringify({ path:
|
|
4504
|
+
body: JSON.stringify({ path: path20 })
|
|
4316
4505
|
});
|
|
4317
4506
|
if (!response.ok) {
|
|
4318
4507
|
console.debug(`[MachineSettings] syncProjectPath failed: ${response.status} ${response.statusText}`);
|
|
@@ -4566,6 +4755,29 @@ async function ensureProtocolHandlerRegistered() {
|
|
|
4566
4755
|
}
|
|
4567
4756
|
}
|
|
4568
4757
|
|
|
4758
|
+
// src/utils/daemon-readiness.ts
|
|
4759
|
+
async function waitForDaemonReachable(isReachable, timeoutMs = 15e3, pollMs = 250, options = {}) {
|
|
4760
|
+
const deadline = Date.now() + timeoutMs;
|
|
4761
|
+
const backoffFactor = options.backoffFactor ?? 1.25;
|
|
4762
|
+
const maxPollMs = options.maxPollMs ?? 1e3;
|
|
4763
|
+
const random = options.random ?? Math.random;
|
|
4764
|
+
let currentPollMs = pollMs;
|
|
4765
|
+
while (Date.now() < deadline) {
|
|
4766
|
+
if (await isReachable()) {
|
|
4767
|
+
return true;
|
|
4768
|
+
}
|
|
4769
|
+
const remainingMs = Math.max(0, deadline - Date.now());
|
|
4770
|
+
const jitterMs = options.jitterMs ?? Math.min(50, Math.floor(currentPollMs * 0.2));
|
|
4771
|
+
const jitter = jitterMs > 0 ? Math.floor(random() * jitterMs) : 0;
|
|
4772
|
+
const sleepMs = Math.min(remainingMs, currentPollMs + jitter);
|
|
4773
|
+
if (sleepMs > 0) {
|
|
4774
|
+
await new Promise((resolve6) => setTimeout(resolve6, sleepMs));
|
|
4775
|
+
}
|
|
4776
|
+
currentPollMs = Math.min(maxPollMs, Math.max(1, Math.round(currentPollMs * backoffFactor)));
|
|
4777
|
+
}
|
|
4778
|
+
return false;
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4569
4781
|
// src/commands/daemon.ts
|
|
4570
4782
|
var CONNECTION_MAX_RETRIES = 3;
|
|
4571
4783
|
var FOREGROUND_DAEMON_MONITOR_INTERVAL_MS = 5e3;
|
|
@@ -4620,7 +4832,7 @@ async function daemonCommand(options = {}) {
|
|
|
4620
4832
|
const killedCount = killAllEpisodaProcesses();
|
|
4621
4833
|
if (killedCount > 0) {
|
|
4622
4834
|
status.info(`Cleaned up ${killedCount} stale process${killedCount > 1 ? "es" : ""}`);
|
|
4623
|
-
await new Promise((
|
|
4835
|
+
await new Promise((resolve6) => setTimeout(resolve6, 2e3));
|
|
4624
4836
|
}
|
|
4625
4837
|
}
|
|
4626
4838
|
let projectPath = null;
|
|
@@ -4685,7 +4897,7 @@ async function daemonCommand(options = {}) {
|
|
|
4685
4897
|
} else {
|
|
4686
4898
|
status.debug(`Daemon already running (PID: ${daemonPid})`);
|
|
4687
4899
|
}
|
|
4688
|
-
const reachable = await isDaemonReachable
|
|
4900
|
+
const reachable = await waitForDaemonReachable(isDaemonReachable, 15e3, 250);
|
|
4689
4901
|
if (!reachable) {
|
|
4690
4902
|
status.error("Daemon is running but not responding. Try: episoda stop && episoda dev");
|
|
4691
4903
|
process.exit(1);
|
|
@@ -4709,7 +4921,7 @@ async function daemonCommand(options = {}) {
|
|
|
4709
4921
|
for (let retry = 0; retry < CONNECTION_MAX_RETRIES && !connected; retry++) {
|
|
4710
4922
|
if (retry > 0) {
|
|
4711
4923
|
status.info(`Retrying connection (attempt ${retry + 1}/${CONNECTION_MAX_RETRIES})...`);
|
|
4712
|
-
await new Promise((
|
|
4924
|
+
await new Promise((resolve6) => setTimeout(resolve6, 1e3));
|
|
4713
4925
|
}
|
|
4714
4926
|
try {
|
|
4715
4927
|
const result = await addProject(connectId, connectPath);
|
|
@@ -5284,10 +5496,10 @@ async function initiateDeviceFlow(apiUrl, machineId) {
|
|
|
5284
5496
|
return await response.json();
|
|
5285
5497
|
}
|
|
5286
5498
|
async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
|
|
5287
|
-
return new Promise((
|
|
5499
|
+
return new Promise((resolve6) => {
|
|
5288
5500
|
const timeout = setTimeout(() => {
|
|
5289
5501
|
status.error("Authorization timed out");
|
|
5290
|
-
|
|
5502
|
+
resolve6(false);
|
|
5291
5503
|
}, expiresIn * 1e3);
|
|
5292
5504
|
const url = `${apiUrl}/api/oauth/authorize-stream?device_code=${deviceCode}`;
|
|
5293
5505
|
const curlProcess = (0, import_child_process7.spawn)("curl", ["-N", url]);
|
|
@@ -5313,26 +5525,26 @@ async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
|
|
|
5313
5525
|
if (eventType === "authorized") {
|
|
5314
5526
|
clearTimeout(timeout);
|
|
5315
5527
|
curlProcess.kill();
|
|
5316
|
-
|
|
5528
|
+
resolve6(true);
|
|
5317
5529
|
return;
|
|
5318
5530
|
} else if (eventType === "denied") {
|
|
5319
5531
|
clearTimeout(timeout);
|
|
5320
5532
|
curlProcess.kill();
|
|
5321
5533
|
status.error("Authorization denied by user");
|
|
5322
|
-
|
|
5534
|
+
resolve6(false);
|
|
5323
5535
|
return;
|
|
5324
5536
|
} else if (eventType === "expired") {
|
|
5325
5537
|
clearTimeout(timeout);
|
|
5326
5538
|
curlProcess.kill();
|
|
5327
5539
|
status.error("Authorization code expired");
|
|
5328
|
-
|
|
5540
|
+
resolve6(false);
|
|
5329
5541
|
return;
|
|
5330
5542
|
} else if (eventType === "error") {
|
|
5331
5543
|
const errorData = JSON.parse(data);
|
|
5332
5544
|
clearTimeout(timeout);
|
|
5333
5545
|
curlProcess.kill();
|
|
5334
5546
|
status.error(`Authorization error: ${errorData.error_description || errorData.error || "Unknown error"}`);
|
|
5335
|
-
|
|
5547
|
+
resolve6(false);
|
|
5336
5548
|
return;
|
|
5337
5549
|
}
|
|
5338
5550
|
} catch (error) {
|
|
@@ -5342,13 +5554,13 @@ async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
|
|
|
5342
5554
|
curlProcess.on("error", (error) => {
|
|
5343
5555
|
clearTimeout(timeout);
|
|
5344
5556
|
status.error(`Failed to monitor authorization: ${error.message}`);
|
|
5345
|
-
|
|
5557
|
+
resolve6(false);
|
|
5346
5558
|
});
|
|
5347
5559
|
curlProcess.on("close", (code) => {
|
|
5348
5560
|
clearTimeout(timeout);
|
|
5349
5561
|
if (code !== 0 && code !== null) {
|
|
5350
5562
|
status.error(`Authorization monitoring failed with code ${code}`);
|
|
5351
|
-
|
|
5563
|
+
resolve6(false);
|
|
5352
5564
|
}
|
|
5353
5565
|
});
|
|
5354
5566
|
});
|
|
@@ -5672,7 +5884,7 @@ async function restartDaemon() {
|
|
|
5672
5884
|
stdio: ["pipe", "pipe", "pipe"],
|
|
5673
5885
|
timeout: 1e4
|
|
5674
5886
|
});
|
|
5675
|
-
await new Promise((
|
|
5887
|
+
await new Promise((resolve6) => setTimeout(resolve6, 1e3));
|
|
5676
5888
|
const child = (0, import_child_process9.spawn)("episoda", ["dev"], {
|
|
5677
5889
|
detached: true,
|
|
5678
5890
|
stdio: "ignore",
|
|
@@ -5683,7 +5895,7 @@ async function restartDaemon() {
|
|
|
5683
5895
|
const pollIntervalMs = 500;
|
|
5684
5896
|
const startTime = Date.now();
|
|
5685
5897
|
while (Date.now() - startTime < maxWaitMs) {
|
|
5686
|
-
await new Promise((
|
|
5898
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollIntervalMs));
|
|
5687
5899
|
try {
|
|
5688
5900
|
const reachable = await isDaemonReachable();
|
|
5689
5901
|
if (reachable) {
|
|
@@ -5790,6 +6002,21 @@ function sanitizeGithubToken(token) {
|
|
|
5790
6002
|
}
|
|
5791
6003
|
|
|
5792
6004
|
// src/commands/status.ts
|
|
6005
|
+
var fs9 = __toESM(require("fs"));
|
|
6006
|
+
var path10 = __toESM(require("path"));
|
|
6007
|
+
function logCliReliabilityMetric(metric, fields) {
|
|
6008
|
+
const line = `[CLI][Metric] ${JSON.stringify({
|
|
6009
|
+
metric,
|
|
6010
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6011
|
+
...fields
|
|
6012
|
+
})}`;
|
|
6013
|
+
console.warn(line);
|
|
6014
|
+
try {
|
|
6015
|
+
fs9.appendFileSync(path10.join((0, import_core8.getConfigDir)(), "daemon.log"), `${line}
|
|
6016
|
+
`, "utf-8");
|
|
6017
|
+
} catch {
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
5793
6020
|
async function statusCommand(options = {}) {
|
|
5794
6021
|
status.info("Checking CLI status...");
|
|
5795
6022
|
status.info("");
|
|
@@ -5810,7 +6037,7 @@ async function statusCommand(options = {}) {
|
|
|
5810
6037
|
status.info(` API URL: ${config.api_url}`);
|
|
5811
6038
|
const effectiveCliVersion = resolveEffectiveCliVersion(import_core8.VERSION);
|
|
5812
6039
|
const installChannel = detectCliInstallChannel(import_core8.VERSION);
|
|
5813
|
-
if (
|
|
6040
|
+
if (options.update) {
|
|
5814
6041
|
const updateResult = await checkForUpdates(effectiveCliVersion);
|
|
5815
6042
|
if (updateResult.isLinked) {
|
|
5816
6043
|
status.info(` CLI Version: ${effectiveCliVersion} (linked)`);
|
|
@@ -5833,6 +6060,7 @@ async function statusCommand(options = {}) {
|
|
|
5833
6060
|
}
|
|
5834
6061
|
} else {
|
|
5835
6062
|
status.info(` CLI Version: ${effectiveCliVersion}`);
|
|
6063
|
+
status.info(' Run "episoda update" (or "episoda status --update") to apply updates.');
|
|
5836
6064
|
}
|
|
5837
6065
|
if (installChannel.legacyOnly) {
|
|
5838
6066
|
status.warning(` \u26A0 Legacy package channel detected (episoda@${installChannel.legacyVersion})`);
|
|
@@ -5853,6 +6081,10 @@ async function statusCommand(options = {}) {
|
|
|
5853
6081
|
if (!daemonStatus) {
|
|
5854
6082
|
const daemonPid = isDaemonRunning();
|
|
5855
6083
|
if (daemonPid) {
|
|
6084
|
+
logCliReliabilityMetric("manual_reconnect_needed", {
|
|
6085
|
+
reason: "daemon_running_ipc_unreachable",
|
|
6086
|
+
daemonPid
|
|
6087
|
+
});
|
|
5856
6088
|
status.error("\u2717 Daemon process is running but IPC is not reachable");
|
|
5857
6089
|
status.info(` PID: ${daemonPid}`);
|
|
5858
6090
|
status.info(' Run "episoda dev" to re-establish IPC/connection (or "episoda stop && episoda dev").');
|
|
@@ -5874,6 +6106,11 @@ async function statusCommand(options = {}) {
|
|
|
5874
6106
|
status.info(` Platform: ${daemonStatus.platform}/${daemonStatus.arch}`);
|
|
5875
6107
|
status.info(` Project: ${connectedProject.name}`);
|
|
5876
6108
|
} else if (serverCheck.localConnected && !serverCheck.serverConnected) {
|
|
6109
|
+
logCliReliabilityMetric("manual_reconnect_needed", {
|
|
6110
|
+
reason: "local_connected_server_disagrees",
|
|
6111
|
+
machineId: serverCheck.machineId,
|
|
6112
|
+
serverMachineId: serverCheck.serverMachineId || null
|
|
6113
|
+
});
|
|
5877
6114
|
status.error("\u2717 Not connected to server");
|
|
5878
6115
|
status.info(` Device: ${deviceDisplayName}`);
|
|
5879
6116
|
status.info(` Platform: ${daemonStatus.platform}/${daemonStatus.arch}`);
|
|
@@ -6028,24 +6265,24 @@ async function stopCommand(options = {}) {
|
|
|
6028
6265
|
}
|
|
6029
6266
|
|
|
6030
6267
|
// src/commands/clone.ts
|
|
6031
|
-
var
|
|
6032
|
-
var
|
|
6268
|
+
var fs11 = __toESM(require("fs"));
|
|
6269
|
+
var path12 = __toESM(require("path"));
|
|
6033
6270
|
var import_core10 = __toESM(require_dist());
|
|
6034
6271
|
|
|
6035
6272
|
// src/daemon/project-tracker.ts
|
|
6036
|
-
var
|
|
6037
|
-
var
|
|
6273
|
+
var fs10 = __toESM(require("fs"));
|
|
6274
|
+
var path11 = __toESM(require("path"));
|
|
6038
6275
|
var import_core9 = __toESM(require_dist());
|
|
6039
6276
|
function getProjectsFilePath() {
|
|
6040
|
-
return
|
|
6277
|
+
return path11.join((0, import_core9.getConfigDir)(), "projects.json");
|
|
6041
6278
|
}
|
|
6042
6279
|
function readProjects() {
|
|
6043
6280
|
const projectsPath = getProjectsFilePath();
|
|
6044
6281
|
try {
|
|
6045
|
-
if (!
|
|
6282
|
+
if (!fs10.existsSync(projectsPath)) {
|
|
6046
6283
|
return { projects: [] };
|
|
6047
6284
|
}
|
|
6048
|
-
const content =
|
|
6285
|
+
const content = fs10.readFileSync(projectsPath, "utf-8");
|
|
6049
6286
|
const data = JSON.parse(content);
|
|
6050
6287
|
if (!data.projects || !Array.isArray(data.projects)) {
|
|
6051
6288
|
console.warn("Invalid projects.json structure, resetting");
|
|
@@ -6060,11 +6297,11 @@ function readProjects() {
|
|
|
6060
6297
|
function writeProjects(data) {
|
|
6061
6298
|
const projectsPath = getProjectsFilePath();
|
|
6062
6299
|
try {
|
|
6063
|
-
const dir =
|
|
6064
|
-
if (!
|
|
6065
|
-
|
|
6300
|
+
const dir = path11.dirname(projectsPath);
|
|
6301
|
+
if (!fs10.existsSync(dir)) {
|
|
6302
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
6066
6303
|
}
|
|
6067
|
-
|
|
6304
|
+
fs10.writeFileSync(projectsPath, JSON.stringify(data, null, 2), "utf-8");
|
|
6068
6305
|
} catch (error) {
|
|
6069
6306
|
throw new Error(`Failed to write projects.json: ${error}`);
|
|
6070
6307
|
}
|
|
@@ -6088,7 +6325,7 @@ function addProject2(projectId, projectPath, options) {
|
|
|
6088
6325
|
console.log(`[ProjectTracker] Replacing project entry: ${existingById.path} -> ${projectPath}`);
|
|
6089
6326
|
data.projects.splice(existingByIdIndex, 1);
|
|
6090
6327
|
}
|
|
6091
|
-
const projectName =
|
|
6328
|
+
const projectName = path11.basename(projectPath);
|
|
6092
6329
|
const newProject = {
|
|
6093
6330
|
id: projectId,
|
|
6094
6331
|
path: projectPath,
|
|
@@ -6122,9 +6359,9 @@ async function cloneCommand(slugArg, options = {}) {
|
|
|
6122
6359
|
}
|
|
6123
6360
|
const apiUrl = options.apiUrl || config.api_url || "https://episoda.dev";
|
|
6124
6361
|
const projectPath = getProjectPath(workspaceSlug, projectSlug);
|
|
6125
|
-
if (
|
|
6126
|
-
const bareRepoPath =
|
|
6127
|
-
if (
|
|
6362
|
+
if (fs11.existsSync(projectPath)) {
|
|
6363
|
+
const bareRepoPath = path12.join(projectPath, ".bare");
|
|
6364
|
+
if (fs11.existsSync(bareRepoPath)) {
|
|
6128
6365
|
status.warning(`Project already cloned at ${projectPath}`);
|
|
6129
6366
|
status.info("");
|
|
6130
6367
|
status.info("Next steps:");
|
|
@@ -6156,7 +6393,7 @@ Please configure a repository in the project settings on episoda.dev.`
|
|
|
6156
6393
|
status.info("");
|
|
6157
6394
|
status.info("Creating project directory...");
|
|
6158
6395
|
const episodaRoot = getEpisodaRoot();
|
|
6159
|
-
|
|
6396
|
+
fs11.mkdirSync(projectPath, { recursive: true });
|
|
6160
6397
|
status.success(`\u2713 Created ${projectPath}`);
|
|
6161
6398
|
status.info("Cloning repository (bare)...");
|
|
6162
6399
|
try {
|
|
@@ -6194,9 +6431,9 @@ Please configure a repository in the project settings on episoda.dev.`
|
|
|
6194
6431
|
status.info(" 3. episoda checkout {moduleUid} # e.g., episoda checkout EP100");
|
|
6195
6432
|
status.info("");
|
|
6196
6433
|
} catch (error) {
|
|
6197
|
-
if (
|
|
6434
|
+
if (fs11.existsSync(projectPath)) {
|
|
6198
6435
|
try {
|
|
6199
|
-
|
|
6436
|
+
fs11.rmSync(projectPath, { recursive: true, force: true });
|
|
6200
6437
|
} catch {
|
|
6201
6438
|
}
|
|
6202
6439
|
}
|
|
@@ -6290,15 +6527,15 @@ async function fetchWithRetry(url, options, maxRetries = 3) {
|
|
|
6290
6527
|
lastError = error instanceof Error ? error : new Error("Network error");
|
|
6291
6528
|
}
|
|
6292
6529
|
if (attempt < maxRetries - 1) {
|
|
6293
|
-
await new Promise((
|
|
6530
|
+
await new Promise((resolve6) => setTimeout(resolve6, Math.pow(2, attempt) * 1e3));
|
|
6294
6531
|
}
|
|
6295
6532
|
}
|
|
6296
6533
|
throw lastError || new Error("Request failed after retries");
|
|
6297
6534
|
}
|
|
6298
6535
|
|
|
6299
6536
|
// src/utils/env-setup.ts
|
|
6300
|
-
var
|
|
6301
|
-
var
|
|
6537
|
+
var fs12 = __toESM(require("fs"));
|
|
6538
|
+
var path13 = __toESM(require("path"));
|
|
6302
6539
|
async function fetchEnvVars(apiUrl, accessToken) {
|
|
6303
6540
|
try {
|
|
6304
6541
|
const url = `${apiUrl}/api/cli/env-vars`;
|
|
@@ -6333,8 +6570,8 @@ function writeEnvFile(targetPath, envVars) {
|
|
|
6333
6570
|
}
|
|
6334
6571
|
return `${key}=${value}`;
|
|
6335
6572
|
}).join("\n") + "\n";
|
|
6336
|
-
const envPath =
|
|
6337
|
-
|
|
6573
|
+
const envPath = path13.join(targetPath, ".env");
|
|
6574
|
+
fs12.writeFileSync(envPath, envContent, { mode: 384 });
|
|
6338
6575
|
console.log(`[env-setup] Wrote ${Object.keys(envVars).length} env vars to ${envPath}`);
|
|
6339
6576
|
}
|
|
6340
6577
|
async function setupWorktreeEnv(worktreePath, apiUrl, accessToken) {
|
|
@@ -6552,7 +6789,7 @@ async function updateModuleCheckout(apiUrl, moduleId, accessToken, branchName, w
|
|
|
6552
6789
|
}
|
|
6553
6790
|
|
|
6554
6791
|
// src/commands/release.ts
|
|
6555
|
-
var
|
|
6792
|
+
var path14 = __toESM(require("path"));
|
|
6556
6793
|
var import_core12 = __toESM(require_dist());
|
|
6557
6794
|
async function releaseCommand(moduleUid, options = {}) {
|
|
6558
6795
|
if (!moduleUid || !moduleUid.match(/^EP\d+$/)) {
|
|
@@ -6599,7 +6836,7 @@ Commit or stash your changes first, or use --force to discard them.`
|
|
|
6599
6836
|
);
|
|
6600
6837
|
}
|
|
6601
6838
|
}
|
|
6602
|
-
const currentPath =
|
|
6839
|
+
const currentPath = path14.resolve(process.cwd());
|
|
6603
6840
|
if (currentPath.startsWith(existing.worktreePath)) {
|
|
6604
6841
|
status.warning("You are inside the worktree being released.");
|
|
6605
6842
|
status.info(`Please cd to ${projectRoot} first.`);
|
|
@@ -6673,8 +6910,8 @@ async function updateModuleRelease(apiUrl, projectId, workspaceSlug, moduleUid,
|
|
|
6673
6910
|
}
|
|
6674
6911
|
|
|
6675
6912
|
// src/commands/list.ts
|
|
6676
|
-
var
|
|
6677
|
-
var
|
|
6913
|
+
var fs13 = __toESM(require("fs"));
|
|
6914
|
+
var path15 = __toESM(require("path"));
|
|
6678
6915
|
var import_chalk2 = __toESM(require("chalk"));
|
|
6679
6916
|
var import_core13 = __toESM(require_dist());
|
|
6680
6917
|
async function listCommand(subcommand, options = {}) {
|
|
@@ -6686,7 +6923,7 @@ async function listCommand(subcommand, options = {}) {
|
|
|
6686
6923
|
}
|
|
6687
6924
|
async function listProjects(options) {
|
|
6688
6925
|
const episodaRoot = getEpisodaRoot();
|
|
6689
|
-
if (!
|
|
6926
|
+
if (!fs13.existsSync(episodaRoot)) {
|
|
6690
6927
|
status.info("No projects cloned yet.");
|
|
6691
6928
|
status.info("");
|
|
6692
6929
|
status.info("Clone a project with:");
|
|
@@ -6694,12 +6931,12 @@ async function listProjects(options) {
|
|
|
6694
6931
|
return;
|
|
6695
6932
|
}
|
|
6696
6933
|
const projects = [];
|
|
6697
|
-
const workspaces =
|
|
6934
|
+
const workspaces = fs13.readdirSync(episodaRoot, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
|
|
6698
6935
|
for (const workspace of workspaces) {
|
|
6699
|
-
const workspacePath =
|
|
6700
|
-
const projectDirs =
|
|
6936
|
+
const workspacePath = path15.join(episodaRoot, workspace.name);
|
|
6937
|
+
const projectDirs = fs13.readdirSync(workspacePath, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
|
|
6701
6938
|
for (const projectDir of projectDirs) {
|
|
6702
|
-
const projectPath =
|
|
6939
|
+
const projectPath = path15.join(workspacePath, projectDir.name);
|
|
6703
6940
|
if (await isWorktreeProject(projectPath)) {
|
|
6704
6941
|
const manager = new WorktreeManager(projectPath);
|
|
6705
6942
|
await manager.initialize();
|
|
@@ -6805,7 +7042,7 @@ async function listWorktrees(options) {
|
|
|
6805
7042
|
console.log("");
|
|
6806
7043
|
console.log(import_chalk2.default.red(" Orphaned worktrees (in git but not in config):"));
|
|
6807
7044
|
for (const wtPath of orphaned) {
|
|
6808
|
-
const moduleUid =
|
|
7045
|
+
const moduleUid = path15.basename(wtPath);
|
|
6809
7046
|
console.log(import_chalk2.default.red(` \u2717 ${moduleUid} - ${wtPath}`));
|
|
6810
7047
|
}
|
|
6811
7048
|
console.log(import_chalk2.default.gray(" These may be from crashed sessions. Run `episoda release <uid>` to clean up"));
|
|
@@ -6955,7 +7192,7 @@ async function updateCommand(options = {}) {
|
|
|
6955
7192
|
}
|
|
6956
7193
|
if (options.restart && daemonWasRunning) {
|
|
6957
7194
|
status.info("Restarting daemon...");
|
|
6958
|
-
await new Promise((
|
|
7195
|
+
await new Promise((resolve6) => setTimeout(resolve6, 1e3));
|
|
6959
7196
|
if (startDaemon2()) {
|
|
6960
7197
|
status.success("\u2713 Daemon restarted");
|
|
6961
7198
|
status.info("");
|
|
@@ -6999,26 +7236,26 @@ var import_child_process12 = require("child_process");
|
|
|
6999
7236
|
var import_core15 = __toESM(require_dist());
|
|
7000
7237
|
|
|
7001
7238
|
// src/utils/env-cache.ts
|
|
7002
|
-
var
|
|
7003
|
-
var
|
|
7239
|
+
var fs14 = __toESM(require("fs"));
|
|
7240
|
+
var path16 = __toESM(require("path"));
|
|
7004
7241
|
var os4 = __toESM(require("os"));
|
|
7005
7242
|
var DEFAULT_CACHE_TTL = 60;
|
|
7006
|
-
var CACHE_DIR =
|
|
7243
|
+
var CACHE_DIR = path16.join(os4.homedir(), ".episoda", "cache");
|
|
7007
7244
|
function getCacheFilePath(projectId) {
|
|
7008
|
-
return
|
|
7245
|
+
return path16.join(CACHE_DIR, `env-vars-${projectId}.json`);
|
|
7009
7246
|
}
|
|
7010
7247
|
function ensureCacheDir() {
|
|
7011
|
-
if (!
|
|
7012
|
-
|
|
7248
|
+
if (!fs14.existsSync(CACHE_DIR)) {
|
|
7249
|
+
fs14.mkdirSync(CACHE_DIR, { recursive: true, mode: 448 });
|
|
7013
7250
|
}
|
|
7014
7251
|
}
|
|
7015
7252
|
function readCache(projectId) {
|
|
7016
7253
|
try {
|
|
7017
7254
|
const cacheFile = getCacheFilePath(projectId);
|
|
7018
|
-
if (!
|
|
7255
|
+
if (!fs14.existsSync(cacheFile)) {
|
|
7019
7256
|
return null;
|
|
7020
7257
|
}
|
|
7021
|
-
const content =
|
|
7258
|
+
const content = fs14.readFileSync(cacheFile, "utf-8");
|
|
7022
7259
|
const data = JSON.parse(content);
|
|
7023
7260
|
if (!data.vars || typeof data.vars !== "object" || !data.fetchedAt) {
|
|
7024
7261
|
return null;
|
|
@@ -7037,7 +7274,7 @@ function writeCache(projectId, vars) {
|
|
|
7037
7274
|
fetchedAt: Date.now(),
|
|
7038
7275
|
projectId
|
|
7039
7276
|
};
|
|
7040
|
-
|
|
7277
|
+
fs14.writeFileSync(cacheFile, JSON.stringify(data, null, 2), { mode: 384 });
|
|
7041
7278
|
} catch (error) {
|
|
7042
7279
|
console.warn("[env-cache] Failed to write cache:", error instanceof Error ? error.message : error);
|
|
7043
7280
|
}
|
|
@@ -7273,8 +7510,8 @@ function printExports(envVars) {
|
|
|
7273
7510
|
}
|
|
7274
7511
|
|
|
7275
7512
|
// src/commands/env.ts
|
|
7276
|
-
var
|
|
7277
|
-
var
|
|
7513
|
+
var fs15 = __toESM(require("fs"));
|
|
7514
|
+
var path17 = __toESM(require("path"));
|
|
7278
7515
|
var readline = __toESM(require("readline"));
|
|
7279
7516
|
var import_core17 = __toESM(require_dist());
|
|
7280
7517
|
async function envListCommand(options = {}) {
|
|
@@ -7474,27 +7711,27 @@ async function envPullCommand(options = {}) {
|
|
|
7474
7711
|
return;
|
|
7475
7712
|
}
|
|
7476
7713
|
const filename = options.file || ".env";
|
|
7477
|
-
const filepath =
|
|
7478
|
-
|
|
7714
|
+
const filepath = path17.resolve(process.cwd(), filename);
|
|
7715
|
+
fs15.writeFileSync(filepath, fullContent, { mode: 384 });
|
|
7479
7716
|
status.success(`Wrote ${Object.keys(visibleEnvVars).length} env vars to ${filename}`);
|
|
7480
7717
|
}
|
|
7481
7718
|
async function promptForValue(key) {
|
|
7482
7719
|
if (!process.stdin.isTTY) {
|
|
7483
|
-
return new Promise((
|
|
7720
|
+
return new Promise((resolve6, reject) => {
|
|
7484
7721
|
const rl = readline.createInterface({
|
|
7485
7722
|
input: process.stdin,
|
|
7486
7723
|
output: process.stdout
|
|
7487
7724
|
});
|
|
7488
7725
|
rl.question(`Enter value for ${key}: `, (answer) => {
|
|
7489
7726
|
rl.close();
|
|
7490
|
-
|
|
7727
|
+
resolve6(answer);
|
|
7491
7728
|
});
|
|
7492
7729
|
rl.on("close", () => {
|
|
7493
7730
|
reject(new Error("Input closed"));
|
|
7494
7731
|
});
|
|
7495
7732
|
});
|
|
7496
7733
|
}
|
|
7497
|
-
return new Promise((
|
|
7734
|
+
return new Promise((resolve6, reject) => {
|
|
7498
7735
|
const rl = readline.createInterface({
|
|
7499
7736
|
input: process.stdin,
|
|
7500
7737
|
output: process.stdout
|
|
@@ -7514,7 +7751,7 @@ async function promptForValue(key) {
|
|
|
7514
7751
|
resolved = true;
|
|
7515
7752
|
cleanup();
|
|
7516
7753
|
console.log("");
|
|
7517
|
-
|
|
7754
|
+
resolve6(value);
|
|
7518
7755
|
} else if (str === "") {
|
|
7519
7756
|
cleanup();
|
|
7520
7757
|
reject(new Error("Cancelled"));
|
|
@@ -7743,8 +7980,8 @@ async function migrationsFixCommand(options) {
|
|
|
7743
7980
|
}
|
|
7744
7981
|
|
|
7745
7982
|
// src/utils/legacy-channel-notice.ts
|
|
7746
|
-
var
|
|
7747
|
-
var
|
|
7983
|
+
var fs16 = __toESM(require("fs"));
|
|
7984
|
+
var path19 = __toESM(require("path"));
|
|
7748
7985
|
var import_core18 = __toESM(require_dist());
|
|
7749
7986
|
var LEGACY_NOTICE_FILE = "legacy-channel-notice.json";
|
|
7750
7987
|
var LEGACY_NOTICE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -7757,7 +7994,7 @@ var SUPPRESSED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
7757
7994
|
"migrations"
|
|
7758
7995
|
]);
|
|
7759
7996
|
function getNoticeFilePath() {
|
|
7760
|
-
return
|
|
7997
|
+
return path19.join((0, import_core18.getConfigDir)(), LEGACY_NOTICE_FILE);
|
|
7761
7998
|
}
|
|
7762
7999
|
function getPrimaryCommand(argv) {
|
|
7763
8000
|
const args = argv.slice(2);
|
|
@@ -7765,7 +8002,7 @@ function getPrimaryCommand(argv) {
|
|
|
7765
8002
|
}
|
|
7766
8003
|
function readNoticeRecord() {
|
|
7767
8004
|
try {
|
|
7768
|
-
const raw =
|
|
8005
|
+
const raw = fs16.readFileSync(getNoticeFilePath(), "utf-8");
|
|
7769
8006
|
const parsed = JSON.parse(raw);
|
|
7770
8007
|
if (!parsed?.legacyVersion || typeof parsed.shownAt !== "number") {
|
|
7771
8008
|
return null;
|
|
@@ -7777,8 +8014,8 @@ function readNoticeRecord() {
|
|
|
7777
8014
|
}
|
|
7778
8015
|
function writeNoticeRecord(record) {
|
|
7779
8016
|
try {
|
|
7780
|
-
|
|
7781
|
-
|
|
8017
|
+
fs16.mkdirSync((0, import_core18.getConfigDir)(), { recursive: true });
|
|
8018
|
+
fs16.writeFileSync(getNoticeFilePath(), JSON.stringify(record), "utf-8");
|
|
7782
8019
|
} catch {
|
|
7783
8020
|
}
|
|
7784
8021
|
}
|
|
@@ -7874,12 +8111,12 @@ import_commander.program.command("setup").description("Register episoda:// proto
|
|
|
7874
8111
|
process.exit(1);
|
|
7875
8112
|
}
|
|
7876
8113
|
});
|
|
7877
|
-
import_commander.program.command("status").description("Show connection status").option("--verify", "Verify connection is healthy (not just connected)").option("--local", "Only check local daemon state (faster, but may be stale)").option("--
|
|
8114
|
+
import_commander.program.command("status").description("Show connection status").option("--verify", "Verify connection is healthy (not just connected)").option("--local", "Only check local daemon state (faster, but may be stale)").option("--update", "Check and apply CLI update if available (mutating)").action(async (options) => {
|
|
7878
8115
|
try {
|
|
7879
8116
|
await statusCommand({
|
|
7880
8117
|
verify: options.verify,
|
|
7881
8118
|
local: options.local,
|
|
7882
|
-
|
|
8119
|
+
update: options.update
|
|
7883
8120
|
});
|
|
7884
8121
|
} catch (error) {
|
|
7885
8122
|
status.error(`Status check failed: ${error instanceof Error ? error.message : String(error)}`);
|