happy-imou-cloud 2.0.8 → 2.0.10

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.
@@ -1,34 +1,34 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
2
- import { l as logger, e as encodeBase64, c as configuration, h as buildAuthenticatedHeaders, S as SigningBootstrapRequiredError, j as SIGNING_BOOTSTRAP_REQUIRED_MESSAGE, k as encodeBase64Url, f as delay, m as buildClientHeaders, n as decodeBase64, H as HAPPY_CLOUD_DAEMON_PORT, p as packageJson, A as ApiClient, o as getLatestDaemonLog } from './api-D-uiH_TF.mjs';
3
- import { writeCredentialsLegacy, writeCredentialsDataKey, readCredentials, readSettings, updateSettings, readDaemonState, clearDaemonState, acquireDaemonLock, writeDaemonState, releaseDaemonLock, validateProfileForAgent, getProfileEnvironmentVariables, clearCredentials, clearMachineId } from './persistence-DiNg1DPF.mjs';
2
+ import { l as logger, e as encodeBase64, c as configuration, h as buildAuthenticatedHeaders, S as SigningBootstrapRequiredError, j as SIGNING_BOOTSTRAP_REQUIRED_MESSAGE, k as encodeBase64Url, f as delay, m as buildClientHeaders, n as decodeBase64, H as HAPPY_CLOUD_DAEMON_PORT, p as packageJson, A as ApiClient, o as getLatestDaemonLog } from './api-BjxmW-0W.mjs';
3
+ import { writeCredentialsLegacy, writeCredentialsDataKey, readCredentials, readSettings, updateSettings, readDaemonState, clearDaemonState, acquireDaemonLock, writeDaemonState, releaseDaemonLock, validateProfileForAgent, getProfileEnvironmentVariables, clearCredentials, clearMachineId } from './persistence-BxP6Jw1f.mjs';
4
4
  import { z } from 'zod';
5
- import fs from 'fs/promises';
5
+ import fs, { writeFile as writeFile$1, rename, unlink as unlink$1 } from 'fs/promises';
6
6
  import os, { homedir } from 'os';
7
7
  import * as tmp from 'tmp';
8
8
  import { randomUUID, randomBytes } from 'node:crypto';
9
9
  import tweetnacl from 'tweetnacl';
10
10
  import axios from 'axios';
11
11
  import qrcode from 'qrcode-terminal';
12
- import { writeFile, unlink } from 'node:fs/promises';
12
+ import { writeFile, unlink, readdir, readFile, mkdir } from 'node:fs/promises';
13
13
  import { createRequire } from 'node:module';
14
14
  import os$1, { tmpdir, homedir as homedir$1 } from 'node:os';
15
- import { join, resolve as resolve$1, isAbsolute, delimiter } from 'node:path';
15
+ import path, { join, resolve as resolve$1, isAbsolute, delimiter, normalize, dirname as dirname$1 } from 'node:path';
16
16
  import open from 'open';
17
17
  import React, { useState } from 'react';
18
18
  import { useInput, Box, Text, render } from 'ink';
19
19
  import { spawn, execSync, exec } from 'child_process';
20
20
  import { dirname, resolve, join as join$1 } from 'path';
21
21
  import { fileURLToPath } from 'url';
22
- import { readFileSync as readFileSync$1, existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync, mkdirSync } from 'fs';
22
+ import { readFileSync as readFileSync$1, existsSync as existsSync$1, writeFileSync, chmodSync, unlinkSync as unlinkSync$1, mkdirSync } from 'fs';
23
23
  import { execFileSync, spawn as spawn$2 } from 'node:child_process';
24
24
  import psList from 'ps-list';
25
25
  import spawn$1 from 'cross-spawn';
26
- import { existsSync, readFileSync, readdirSync, statSync, rmSync, mkdirSync as mkdirSync$1 } from 'node:fs';
26
+ import fs$1, { existsSync, readFileSync, readdirSync, statSync, unlinkSync, rmSync, mkdirSync as mkdirSync$1, realpathSync } from 'node:fs';
27
27
  import fastify from 'fastify';
28
28
  import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
29
+ import { randomUUID as randomUUID$1, randomBytes as randomBytes$1, createHash } from 'crypto';
29
30
  import { createInterface } from 'node:readline';
30
31
  import { createServer } from 'http';
31
- import { randomBytes as randomBytes$1, createHash } from 'crypto';
32
32
  import { promisify } from 'util';
33
33
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
34
34
 
@@ -677,6 +677,16 @@ function projectPath() {
677
677
  return path;
678
678
  }
679
679
 
680
+ function classifyHappyProcessLookup(proc) {
681
+ const match = classifyHappyProcess(proc);
682
+ if (match) {
683
+ return match;
684
+ }
685
+ if (process.platform === "win32" && isWindowsCliHostProcess(proc.name) && proc.cmd.trim().length === 0) {
686
+ return "indeterminate";
687
+ }
688
+ return null;
689
+ }
680
690
  function getDaemonPid() {
681
691
  try {
682
692
  if (!existsSync(configuration.daemonStateFile)) {
@@ -757,7 +767,7 @@ function findWindowsDaemonProcess(processes) {
757
767
  return {
758
768
  pid: daemonPid,
759
769
  command: `${daemonProcess.name || "unknown"} (PID ${daemonPid})`,
760
- type: "daemon"
770
+ type: "daemon-indeterminate"
761
771
  };
762
772
  }
763
773
  function findWindowsHappyProcesses(processes) {
@@ -774,6 +784,40 @@ function findWindowsHappyProcesses(processes) {
774
784
  }
775
785
  return Array.from(matches.values());
776
786
  }
787
+ function toProcessSnapshot(proc) {
788
+ return {
789
+ pid: proc.pid,
790
+ name: proc.name || "",
791
+ cmd: proc.cmd || ""
792
+ };
793
+ }
794
+ async function findHappyProcessByPid(pid) {
795
+ try {
796
+ if (process.platform === "win32") {
797
+ const windowsProcesses = getWindowsProcessSnapshots();
798
+ if (windowsProcesses.length > 0) {
799
+ const proc3 = windowsProcesses.find((candidate) => candidate.pid === pid);
800
+ if (!proc3) {
801
+ return null;
802
+ }
803
+ return classifyHappyProcessLookup(proc3);
804
+ }
805
+ const fallbackProcesses = (await psList()).map(toProcessSnapshot);
806
+ const proc2 = fallbackProcesses.find((candidate) => candidate.pid === pid);
807
+ if (!proc2) {
808
+ return null;
809
+ }
810
+ return classifyHappyProcessLookup(proc2);
811
+ }
812
+ const proc = (await psList()).map(toProcessSnapshot).find((candidate) => candidate.pid === pid);
813
+ if (!proc) {
814
+ return null;
815
+ }
816
+ return classifyHappyProcess(proc);
817
+ } catch (error) {
818
+ return "indeterminate";
819
+ }
820
+ }
777
821
  async function findAllHappyProcesses() {
778
822
  try {
779
823
  if (process.platform === "win32") {
@@ -781,15 +825,8 @@ async function findAllHappyProcesses() {
781
825
  if (windowsProcesses.length > 0) {
782
826
  return findWindowsHappyProcesses(windowsProcesses);
783
827
  }
784
- const fallbackProcesses = await psList();
785
- const fallbackDaemon = findWindowsDaemonProcess(
786
- fallbackProcesses.map((proc) => ({
787
- pid: proc.pid,
788
- name: proc.name || "",
789
- cmd: proc.cmd || ""
790
- }))
791
- );
792
- return fallbackDaemon ? [fallbackDaemon] : [];
828
+ const fallbackProcesses = (await psList()).map(toProcessSnapshot);
829
+ return findWindowsHappyProcesses(fallbackProcesses);
793
830
  }
794
831
  const processes = await psList();
795
832
  const allProcesses = [];
@@ -846,6 +883,46 @@ async function killRunawayHappyProcesses() {
846
883
  return { killed, errors };
847
884
  }
848
885
 
886
+ const CONFIRMED_DAEMON_PROCESS_TYPES = /* @__PURE__ */ new Set([
887
+ "daemon",
888
+ "dev-daemon"
889
+ ]);
890
+ function currentProcessLooksLikeDaemon() {
891
+ return process.argv.join(" ").includes("daemon start-sync");
892
+ }
893
+ async function inspectDaemonStateProcess(state) {
894
+ if (!state) {
895
+ return "stale";
896
+ }
897
+ try {
898
+ process.kill(state.pid, 0);
899
+ } catch {
900
+ logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
901
+ await cleanupDaemonState();
902
+ return "stale";
903
+ }
904
+ const processInfo = await findHappyProcessByPid(state.pid);
905
+ if (processInfo === "indeterminate") {
906
+ logger.debug(`[DAEMON RUN] Daemon PID ${state.pid} is alive but process identity is indeterminate`);
907
+ return "indeterminate";
908
+ }
909
+ if (!processInfo) {
910
+ logger.debug(`[DAEMON RUN] PID ${state.pid} is alive but is not a Happy daemon process, cleaning up state`);
911
+ await cleanupDaemonState();
912
+ return "stale";
913
+ }
914
+ if (processInfo.type === "current" && currentProcessLooksLikeDaemon()) {
915
+ return "confirmed";
916
+ }
917
+ if (!CONFIRMED_DAEMON_PROCESS_TYPES.has(processInfo.type)) {
918
+ logger.debug(
919
+ `[DAEMON RUN] PID ${state.pid} resolved to Happy process type ${processInfo.type}, not a daemon; cleaning up state`
920
+ );
921
+ await cleanupDaemonState();
922
+ return "stale";
923
+ }
924
+ return "confirmed";
925
+ }
849
926
  async function daemonPost(path, body) {
850
927
  const state = await readDaemonState();
851
928
  if (!state?.httpPort) {
@@ -855,9 +932,8 @@ async function daemonPost(path, body) {
855
932
  error: errorMessage
856
933
  };
857
934
  }
858
- try {
859
- process.kill(state.pid, 0);
860
- } catch (error) {
935
+ const processStatus = await inspectDaemonStateProcess(state);
936
+ if (processStatus === "stale") {
861
937
  const errorMessage = "Daemon is not running, file is stale";
862
938
  logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
863
939
  return {
@@ -956,18 +1032,22 @@ async function stopDaemonHttp() {
956
1032
  await daemonPost("/stop");
957
1033
  }
958
1034
  async function checkIfDaemonRunningAndCleanupStaleState() {
1035
+ const runtimeStatus = await getDaemonRuntimeStatus();
1036
+ return runtimeStatus !== "not-running";
1037
+ }
1038
+ async function getDaemonRuntimeStatus() {
959
1039
  const state = await readDaemonState();
960
1040
  if (!state) {
961
- return false;
1041
+ return "not-running";
962
1042
  }
963
- try {
964
- process.kill(state.pid, 0);
965
- return true;
966
- } catch {
967
- logger.debug("[DAEMON RUN] Daemon PID not running, cleaning up state");
968
- await cleanupDaemonState();
969
- return false;
1043
+ const processStatus = await inspectDaemonStateProcess(state);
1044
+ if (processStatus === "confirmed") {
1045
+ return "running";
970
1046
  }
1047
+ if (processStatus === "indeterminate") {
1048
+ return "indeterminate";
1049
+ }
1050
+ return "not-running";
971
1051
  }
972
1052
  async function isDaemonControlServerResponsive(timeoutMs = 1e3) {
973
1053
  const state = await readDaemonState();
@@ -975,11 +1055,9 @@ async function isDaemonControlServerResponsive(timeoutMs = 1e3) {
975
1055
  logger.debug("[DAEMON CONTROL] No daemon state or control port found for readiness check");
976
1056
  return false;
977
1057
  }
978
- try {
979
- process.kill(state.pid, 0);
980
- } catch {
981
- logger.debug("[DAEMON CONTROL] Daemon PID not running during readiness check, cleaning up state");
982
- await cleanupDaemonState();
1058
+ const processStatus = await inspectDaemonStateProcess(state);
1059
+ if (processStatus === "stale") {
1060
+ logger.debug("[DAEMON CONTROL] Daemon state became stale during readiness check");
983
1061
  return false;
984
1062
  }
985
1063
  try {
@@ -995,7 +1073,7 @@ async function isDaemonControlServerResponsive(timeoutMs = 1e3) {
995
1073
  return false;
996
1074
  }
997
1075
  }
998
- async function isDaemonRunningCurrentlyInstalledHappyVersion() {
1076
+ async function isDaemonRunningCurrentlyInstalledHappyVersion(readinessProbeTimeoutMs = 1e3) {
999
1077
  logger.debug("[DAEMON CONTROL] Checking if daemon is running same version");
1000
1078
  const runningDaemon = await checkIfDaemonRunningAndCleanupStaleState();
1001
1079
  if (!runningDaemon) {
@@ -1015,7 +1093,7 @@ async function isDaemonRunningCurrentlyInstalledHappyVersion() {
1015
1093
  if (currentCliVersion !== state.startedWithCliVersion) {
1016
1094
  return false;
1017
1095
  }
1018
- return await isDaemonControlServerResponsive();
1096
+ return await isDaemonControlServerResponsive(readinessProbeTimeoutMs);
1019
1097
  } catch (error) {
1020
1098
  logger.debug("[DAEMON CONTROL] Error checking daemon version", error);
1021
1099
  return false;
@@ -1048,6 +1126,17 @@ async function stopDaemon() {
1048
1126
  return;
1049
1127
  }
1050
1128
  logger.debug(`Stopping daemon with PID ${state.pid}`);
1129
+ const processStatus = await inspectDaemonStateProcess(state);
1130
+ if (processStatus === "stale") {
1131
+ logger.debug("Daemon state was stale while stopping, trying known control port/orphan cleanup");
1132
+ const stoppedByKnownPort = await stopDaemonOnKnownPort();
1133
+ if (stoppedByKnownPort) {
1134
+ logger.debug(`Requested daemon stop via known control port ${HAPPY_CLOUD_DAEMON_PORT}`);
1135
+ return;
1136
+ }
1137
+ await killOrphanDaemonProcesses();
1138
+ return;
1139
+ }
1051
1140
  try {
1052
1141
  await stopDaemonHttp();
1053
1142
  await waitForProcessDeath(state.pid, 2e3);
@@ -1057,6 +1146,10 @@ async function stopDaemon() {
1057
1146
  } catch (error) {
1058
1147
  logger.debug("HTTP stop failed, will force kill", error);
1059
1148
  }
1149
+ if (processStatus === "indeterminate") {
1150
+ logger.debug(`Skipping force kill for PID ${state.pid} because daemon identity is indeterminate`);
1151
+ return;
1152
+ }
1060
1153
  try {
1061
1154
  process.kill(state.pid, "SIGKILL");
1062
1155
  await waitForProcessDeath(state.pid, 2e3).catch(() => {
@@ -1108,6 +1201,21 @@ function getEnvironmentInfo() {
1108
1201
  terminal: process.env.TERM
1109
1202
  };
1110
1203
  }
1204
+ function resolveDaemonSpawnDiagnostics(projectRoot, fileExists = existsSync) {
1205
+ const wrapperCandidates = [
1206
+ join(projectRoot, "bin", "happy-cloud.mjs"),
1207
+ join(projectRoot, "bin", "happy.mjs")
1208
+ ];
1209
+ const cliEntrypoint = join(projectRoot, "dist", "index.mjs");
1210
+ const wrapperPath = wrapperCandidates.find((candidate) => fileExists(candidate)) ?? wrapperCandidates[0];
1211
+ return {
1212
+ projectRoot,
1213
+ wrapperPath,
1214
+ cliEntrypoint,
1215
+ wrapperExists: fileExists(wrapperPath),
1216
+ cliEntrypointExists: fileExists(cliEntrypoint)
1217
+ };
1218
+ }
1111
1219
  function getLogFiles(logDir) {
1112
1220
  if (!existsSync(logDir)) {
1113
1221
  return [];
@@ -1134,14 +1242,12 @@ async function runDoctorCommand(filter) {
1134
1242
  console.log(`Node.js Version: ${chalk.green(process.version)}`);
1135
1243
  console.log("");
1136
1244
  console.log(chalk.bold("\u{1F527} Daemon Spawn Diagnostics"));
1137
- const projectRoot = projectPath();
1138
- const wrapperPath = join(projectRoot, "bin", "happy.mjs");
1139
- const cliEntrypoint = join(projectRoot, "dist", "index.mjs");
1140
- console.log(`Project Root: ${chalk.blue(projectRoot)}`);
1141
- console.log(`Wrapper Script: ${chalk.blue(wrapperPath)}`);
1142
- console.log(`CLI Entrypoint: ${chalk.blue(cliEntrypoint)}`);
1143
- console.log(`Wrapper Exists: ${existsSync(wrapperPath) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
1144
- console.log(`CLI Exists: ${existsSync(cliEntrypoint) ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
1245
+ const diagnostics = resolveDaemonSpawnDiagnostics(projectPath());
1246
+ console.log(`Project Root: ${chalk.blue(diagnostics.projectRoot)}`);
1247
+ console.log(`Wrapper Script: ${chalk.blue(diagnostics.wrapperPath)}`);
1248
+ console.log(`CLI Entrypoint: ${chalk.blue(diagnostics.cliEntrypoint)}`);
1249
+ console.log(`Wrapper Exists: ${diagnostics.wrapperExists ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
1250
+ console.log(`CLI Exists: ${diagnostics.cliEntrypointExists ? chalk.green("\u2713 Yes") : chalk.red("\u274C No")}`);
1145
1251
  console.log("");
1146
1252
  console.log(chalk.bold("\u2699\uFE0F Configuration"));
1147
1253
  console.log(`Happy Home: ${chalk.blue(configuration.happyCloudHomeDir)}`);
@@ -1177,9 +1283,9 @@ async function runDoctorCommand(filter) {
1177
1283
  }
1178
1284
  console.log(chalk.bold("\n\u{1F916} Daemon Status"));
1179
1285
  try {
1180
- const isRunning = await checkIfDaemonRunningAndCleanupStaleState();
1286
+ const daemonStatus = await getDaemonRuntimeStatus();
1181
1287
  const state = await readDaemonState();
1182
- if (isRunning && state) {
1288
+ if (daemonStatus === "running" && state) {
1183
1289
  console.log(chalk.green("\u2713 Daemon is running"));
1184
1290
  console.log(` PID: ${state.pid}`);
1185
1291
  console.log(` Started: ${new Date(state.startTime).toLocaleString()}`);
@@ -1187,7 +1293,14 @@ async function runDoctorCommand(filter) {
1187
1293
  if (state.httpPort) {
1188
1294
  console.log(` HTTP Port: ${state.httpPort}`);
1189
1295
  }
1190
- } else if (state && !isRunning) {
1296
+ } else if (daemonStatus === "indeterminate" && state) {
1297
+ console.log(chalk.yellow("\u26A0\uFE0F Daemon state exists, but process identity could not be confirmed"));
1298
+ console.log(` PID: ${state.pid}`);
1299
+ console.log(` CLI Version: ${state.startedWithCliVersion}`);
1300
+ if (state.httpPort) {
1301
+ console.log(` HTTP Port: ${state.httpPort}`);
1302
+ }
1303
+ } else if (state) {
1191
1304
  console.log(chalk.yellow("\u26A0\uFE0F Daemon state exists but process not running (stale)"));
1192
1305
  } else {
1193
1306
  console.log(chalk.red("\u274C Daemon is not running"));
@@ -1209,6 +1322,7 @@ async function runDoctorCommand(filter) {
1209
1322
  const typeLabels = {
1210
1323
  "current": "\u{1F4CD} Current Process",
1211
1324
  "daemon": "\u{1F916} Daemon",
1325
+ "daemon-indeterminate": "\u26A0\uFE0F Possible Daemon (identity unconfirmed)",
1212
1326
  "daemon-version-check": "\u{1F50D} Daemon Version Check (stuck)",
1213
1327
  "daemon-spawned-session": "\u{1F517} Daemon-Spawned Sessions",
1214
1328
  "user-session": "\u{1F464} User Sessions",
@@ -1223,7 +1337,7 @@ async function runDoctorCommand(filter) {
1223
1337
  console.log(chalk.blue(`
1224
1338
  ${typeLabels[type] || type}:`));
1225
1339
  processes.forEach(({ pid, command }) => {
1226
- const color = type === "current" ? chalk.green : type.startsWith("dev") ? chalk.cyan : type.includes("daemon") ? chalk.blue : chalk.gray;
1340
+ const color = type === "current" ? chalk.green : type === "daemon-indeterminate" ? chalk.yellow : type.startsWith("dev") ? chalk.cyan : type.includes("daemon") ? chalk.blue : chalk.gray;
1227
1341
  console.log(` ${color(`PID ${pid}`)}: ${chalk.gray(command)}`);
1228
1342
  });
1229
1343
  });
@@ -2253,6 +2367,283 @@ function buildDaemonChildEnv(baseEnv, extraEnv) {
2253
2367
  return childEnv;
2254
2368
  }
2255
2369
 
2370
+ const DIFFERENT_DAEMON_RUNNING_MESSAGE = "A different daemon was started without killing us. We should kill ourselves.";
2371
+ function readProjectCliVersionFromDisk() {
2372
+ const packageJsonPath = join(projectPath(), "package.json");
2373
+ try {
2374
+ const parsedPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2375
+ if (typeof parsedPackageJson.version !== "string" || parsedPackageJson.version.trim().length === 0) {
2376
+ logger.warn(
2377
+ `[DAEMON RUN] Skipping daemon version check because ${packageJsonPath} does not contain a valid string version`
2378
+ );
2379
+ return null;
2380
+ }
2381
+ return parsedPackageJson.version;
2382
+ } catch (error) {
2383
+ logger.warn(
2384
+ `[DAEMON RUN] Skipping daemon version check because ${packageJsonPath} could not be read or parsed`,
2385
+ error instanceof Error ? error.message : String(error)
2386
+ );
2387
+ return null;
2388
+ }
2389
+ }
2390
+ async function runDaemonHealthCheck({
2391
+ trackedSessionPids,
2392
+ removeTrackedSession,
2393
+ currentCliVersion,
2394
+ readProjectCliVersion = readProjectCliVersionFromDisk,
2395
+ onDaemonOutdated,
2396
+ readDaemonState,
2397
+ daemonPid,
2398
+ requestShutdown,
2399
+ writeHeartbeat
2400
+ }) {
2401
+ for (const pid of trackedSessionPids) {
2402
+ try {
2403
+ process.kill(pid, 0);
2404
+ } catch {
2405
+ logger.debug(`[DAEMON RUN] Removing stale session with PID ${pid} (process no longer exists)`);
2406
+ removeTrackedSession(pid);
2407
+ }
2408
+ }
2409
+ const projectVersion = readProjectCliVersion();
2410
+ if (projectVersion && projectVersion !== currentCliVersion) {
2411
+ await onDaemonOutdated();
2412
+ return;
2413
+ }
2414
+ const daemonState = await readDaemonState();
2415
+ if (daemonState && daemonState.pid !== daemonPid) {
2416
+ logger.debug(`[DAEMON RUN] ${DIFFERENT_DAEMON_RUNNING_MESSAGE}`);
2417
+ requestShutdown("exception", DIFFERENT_DAEMON_RUNNING_MESSAGE);
2418
+ return;
2419
+ }
2420
+ await writeHeartbeat();
2421
+ }
2422
+
2423
+ async function atomicFileWrite(filePath, content) {
2424
+ const tmpFile = `${filePath}.${randomUUID$1()}.tmp`;
2425
+ try {
2426
+ await writeFile$1(tmpFile, content);
2427
+ await rename(tmpFile, filePath);
2428
+ } catch (error) {
2429
+ try {
2430
+ await unlink$1(tmpFile);
2431
+ } catch {
2432
+ }
2433
+ throw error;
2434
+ }
2435
+ }
2436
+
2437
+ const SESSION_REGISTRY_VERSION = 1;
2438
+ const SESSION_REGISTRY_DIRNAME = "session-registry";
2439
+ const NON_SESSION_PROCESS_TYPES = /* @__PURE__ */ new Set([
2440
+ "current",
2441
+ "daemon",
2442
+ "daemon-indeterminate",
2443
+ "dev-daemon",
2444
+ "daemon-launcher",
2445
+ "dev-daemon-launcher",
2446
+ "daemon-version-check",
2447
+ "dev-daemon-version-check",
2448
+ "doctor",
2449
+ "dev-doctor"
2450
+ ]);
2451
+ const registeredCleanupPids = /* @__PURE__ */ new Set();
2452
+ function getSessionRegistryDir() {
2453
+ return join(configuration.happyCloudHomeDir, SESSION_REGISTRY_DIRNAME);
2454
+ }
2455
+ function getSessionRegistryEntryPath(pid) {
2456
+ return join(getSessionRegistryDir(), `${pid}.json`);
2457
+ }
2458
+ function normalizeMetadataForPid(metadata, pid) {
2459
+ if (metadata.hostPid === pid) {
2460
+ return metadata;
2461
+ }
2462
+ return {
2463
+ ...metadata,
2464
+ hostPid: pid
2465
+ };
2466
+ }
2467
+ function createTrackedSessionFromRegistryEntry(entry) {
2468
+ const metadata = normalizeMetadataForPid(entry.metadata, entry.pid);
2469
+ return {
2470
+ startedBy: metadata.startedBy === "daemon" ? "daemon" : "happy directly - likely by user from terminal",
2471
+ happySessionId: entry.sessionId,
2472
+ happySessionMetadataFromLocalWebhook: metadata,
2473
+ pid: entry.pid
2474
+ };
2475
+ }
2476
+ async function ensureSessionRegistryDir() {
2477
+ await mkdir(getSessionRegistryDir(), { recursive: true });
2478
+ }
2479
+ async function removeRegistryEntryPath(path) {
2480
+ try {
2481
+ await unlink(path);
2482
+ return true;
2483
+ } catch (error) {
2484
+ if (error?.code === "ENOENT") {
2485
+ return false;
2486
+ }
2487
+ logger.debug(`[SESSION REGISTRY] Failed to remove ${path}`, error);
2488
+ return false;
2489
+ }
2490
+ }
2491
+ function removeRegistryEntryPathSync(path) {
2492
+ try {
2493
+ unlinkSync(path);
2494
+ } catch (error) {
2495
+ if (error?.code !== "ENOENT") {
2496
+ logger.debug(`[SESSION REGISTRY] Failed to remove ${path} during process cleanup`, error);
2497
+ }
2498
+ }
2499
+ }
2500
+ function installRegistryCleanup(pid) {
2501
+ if (registeredCleanupPids.has(pid)) {
2502
+ return;
2503
+ }
2504
+ registeredCleanupPids.add(pid);
2505
+ let cleaned = false;
2506
+ const cleanup = () => {
2507
+ if (cleaned) {
2508
+ return;
2509
+ }
2510
+ cleaned = true;
2511
+ removeRegistryEntryPathSync(getSessionRegistryEntryPath(pid));
2512
+ };
2513
+ process.once("exit", cleanup);
2514
+ }
2515
+ async function readRegistryEntry(path) {
2516
+ try {
2517
+ const raw = JSON.parse(await readFile(path, "utf8"));
2518
+ if (raw.version !== SESSION_REGISTRY_VERSION || typeof raw.pid !== "number" || typeof raw.sessionId !== "string" || !raw.sessionId || !raw.metadata || typeof raw.metadata !== "object") {
2519
+ logger.debug(`[SESSION REGISTRY] Invalid entry schema at ${path}, pruning`);
2520
+ await removeRegistryEntryPath(path);
2521
+ return null;
2522
+ }
2523
+ return {
2524
+ version: SESSION_REGISTRY_VERSION,
2525
+ pid: raw.pid,
2526
+ sessionId: raw.sessionId,
2527
+ metadata: normalizeMetadataForPid(raw.metadata, raw.pid),
2528
+ updatedAt: typeof raw.updatedAt === "number" ? raw.updatedAt : Date.now()
2529
+ };
2530
+ } catch (error) {
2531
+ logger.debug(`[SESSION REGISTRY] Failed to read ${path}, pruning`, error);
2532
+ await removeRegistryEntryPath(path);
2533
+ return null;
2534
+ }
2535
+ }
2536
+ async function resolveLiveHappySessionPids(listLiveHappyProcesses) {
2537
+ try {
2538
+ const liveHappyProcesses = await listLiveHappyProcesses();
2539
+ if (liveHappyProcesses.length === 0) {
2540
+ return null;
2541
+ }
2542
+ const liveSessionProcesses = liveHappyProcesses.filter((proc) => !NON_SESSION_PROCESS_TYPES.has(proc.type));
2543
+ if (liveSessionProcesses.length === 0) {
2544
+ logger.debug(
2545
+ "[SESSION REGISTRY] Process discovery did not report any session processes, falling back to PID checks"
2546
+ );
2547
+ return null;
2548
+ }
2549
+ return new Set(
2550
+ liveSessionProcesses.map((proc) => proc.pid)
2551
+ );
2552
+ } catch (error) {
2553
+ logger.debug("[SESSION REGISTRY] Failed to list live Happy processes, falling back to PID checks", error);
2554
+ return null;
2555
+ }
2556
+ }
2557
+ async function persistLocalSessionRegistration(sessionId, metadata) {
2558
+ const pid = metadata.hostPid ?? process.pid;
2559
+ const normalizedMetadata = normalizeMetadataForPid(metadata, pid);
2560
+ await ensureSessionRegistryDir();
2561
+ await atomicFileWrite(
2562
+ getSessionRegistryEntryPath(pid),
2563
+ JSON.stringify({
2564
+ version: SESSION_REGISTRY_VERSION,
2565
+ pid,
2566
+ sessionId,
2567
+ metadata: normalizedMetadata,
2568
+ updatedAt: Date.now()
2569
+ }, null, 2)
2570
+ );
2571
+ }
2572
+ async function publishSessionRegistration(sessionId, metadata) {
2573
+ const pid = metadata.hostPid ?? process.pid;
2574
+ try {
2575
+ await persistLocalSessionRegistration(sessionId, metadata);
2576
+ installRegistryCleanup(pid);
2577
+ } catch (error) {
2578
+ logger.debug(`[SESSION REGISTRY] Failed to persist local registration for session ${sessionId}`, error);
2579
+ }
2580
+ try {
2581
+ const result = await notifyDaemonSessionStarted(sessionId, normalizeMetadataForPid(metadata, pid));
2582
+ if (result?.error) {
2583
+ logger.debug(`[SESSION REGISTRY] Failed to report session ${sessionId} to daemon`, result.error);
2584
+ }
2585
+ } catch (error) {
2586
+ logger.debug(`[SESSION REGISTRY] Failed to report session ${sessionId} to daemon`, error);
2587
+ }
2588
+ }
2589
+ async function recoverTrackedSessionsFromLocalRegistry({
2590
+ trackedSessionPids,
2591
+ trackSession,
2592
+ listLiveHappyProcesses = findAllHappyProcesses,
2593
+ lookupHappyProcessByPid = findHappyProcessByPid
2594
+ }) {
2595
+ const registryDir = getSessionRegistryDir();
2596
+ if (!existsSync(registryDir)) {
2597
+ return { recoveredCount: 0, removedStaleCount: 0 };
2598
+ }
2599
+ const alreadyTracked = new Set(trackedSessionPids);
2600
+ const liveHappySessionPids = await resolveLiveHappySessionPids(listLiveHappyProcesses);
2601
+ let recoveredCount = 0;
2602
+ let removedStaleCount = 0;
2603
+ for (const entryFile of await readdir(registryDir)) {
2604
+ if (!entryFile.endsWith(".json")) {
2605
+ continue;
2606
+ }
2607
+ const entryPath = join(registryDir, entryFile);
2608
+ const entry = await readRegistryEntry(entryPath);
2609
+ if (!entry) {
2610
+ continue;
2611
+ }
2612
+ let lookupResult;
2613
+ try {
2614
+ lookupResult = liveHappySessionPids?.has(entry.pid) ? { pid: entry.pid, type: "known-session" } : await lookupHappyProcessByPid(entry.pid);
2615
+ } catch (error) {
2616
+ logger.debug(
2617
+ `[SESSION REGISTRY] Failed to inspect PID ${entry.pid}, keeping registry entry until next recovery tick`,
2618
+ error
2619
+ );
2620
+ lookupResult = "indeterminate";
2621
+ }
2622
+ const sessionPidIsAlive = lookupResult !== null && lookupResult !== "indeterminate" && !NON_SESSION_PROCESS_TYPES.has(lookupResult.type);
2623
+ const shouldKeepEntryWithoutRecovery = lookupResult === "indeterminate";
2624
+ if (shouldKeepEntryWithoutRecovery) {
2625
+ logger.debug(
2626
+ `[SESSION REGISTRY] Keeping registry entry for PID ${entry.pid} because process identity is indeterminate`
2627
+ );
2628
+ continue;
2629
+ }
2630
+ if (!sessionPidIsAlive) {
2631
+ if (await removeRegistryEntryPath(entryPath)) {
2632
+ removedStaleCount++;
2633
+ }
2634
+ continue;
2635
+ }
2636
+ if (alreadyTracked.has(entry.pid)) {
2637
+ continue;
2638
+ }
2639
+ trackSession(entry.pid, createTrackedSessionFromRegistryEntry(entry));
2640
+ alreadyTracked.add(entry.pid);
2641
+ recoveredCount++;
2642
+ logger.debug(`[SESSION REGISTRY] Recovered tracked session ${entry.sessionId} for PID ${entry.pid}`);
2643
+ }
2644
+ return { recoveredCount, removedStaleCount };
2645
+ }
2646
+
2256
2647
  const initialMachineMetadata = {
2257
2648
  host: os.hostname(),
2258
2649
  platform: os.platform(),
@@ -2476,14 +2867,12 @@ async function startDaemon() {
2476
2867
  errorMessage: spawnError.errorMessage
2477
2868
  };
2478
2869
  }
2479
- const tmuxAvailable = await isTmuxAvailable();
2480
- let useTmux = tmuxAvailable;
2481
2870
  let tmuxSessionName = extraEnv.TMUX_SESSION_NAME;
2482
- if (!tmuxAvailable || tmuxSessionName === void 0) {
2483
- useTmux = false;
2484
- if (tmuxSessionName !== void 0) {
2485
- logger.debug(`[DAEMON RUN] tmux session name specified but tmux not available, falling back to regular spawning`);
2486
- }
2871
+ const tmuxRequested = tmuxSessionName !== void 0;
2872
+ const tmuxAvailable = tmuxRequested ? await isTmuxAvailable() : false;
2873
+ let useTmux = tmuxRequested && tmuxAvailable;
2874
+ if (tmuxRequested && !tmuxAvailable) {
2875
+ logger.debug(`[DAEMON RUN] tmux session name specified but tmux not available, falling back to regular spawning`);
2487
2876
  }
2488
2877
  if (useTmux && tmuxSessionName !== void 0) {
2489
2878
  const sessionDesc = tmuxSessionName || "current/most recent session";
@@ -2688,6 +3077,17 @@ async function startDaemon() {
2688
3077
  onHappySessionWebhook,
2689
3078
  port: HAPPY_CLOUD_DAEMON_PORT
2690
3079
  });
3080
+ const recoveryResult = await recoverTrackedSessionsFromLocalRegistry({
3081
+ trackedSessionPids: pidToTrackedSession.keys(),
3082
+ trackSession: (pid, trackedSession) => {
3083
+ pidToTrackedSession.set(pid, trackedSession);
3084
+ }
3085
+ });
3086
+ if (recoveryResult.recoveredCount > 0 || recoveryResult.removedStaleCount > 0) {
3087
+ logger.debug(
3088
+ `[DAEMON RUN] Session registry recovery completed: recovered=${recoveryResult.recoveredCount}, removedStale=${recoveryResult.removedStaleCount}`
3089
+ );
3090
+ }
2691
3091
  const fileState = {
2692
3092
  pid: process.pid,
2693
3093
  httpPort: controlPort,
@@ -2727,52 +3127,69 @@ async function startDaemon() {
2727
3127
  if (process.env.DEBUG) {
2728
3128
  logger.debug(`[DAEMON RUN] Health check started at ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
2729
3129
  }
2730
- for (const [pid, _] of pidToTrackedSession.entries()) {
2731
- try {
2732
- process.kill(pid, 0);
2733
- } catch (error) {
2734
- logger.debug(`[DAEMON RUN] Removing stale session with PID ${pid} (process no longer exists)`);
2735
- pidToTrackedSession.delete(pid);
2736
- }
2737
- }
2738
- const projectVersion = JSON.parse(readFileSync$1(join$1(projectPath(), "package.json"), "utf-8")).version;
2739
- if (projectVersion !== configuration.currentCliVersion) {
2740
- logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
2741
- clearInterval(restartOnStaleVersionAndHeartbeat);
2742
- try {
2743
- spawnHappyCLI(["daemon", "start"], {
2744
- detached: true,
2745
- stdio: "ignore"
2746
- });
2747
- } catch (error) {
2748
- logger.debug("[DAEMON RUN] Failed to spawn new daemon, this is quite likely to happen during integration tests as we are cleaning out dist/ directory", error);
2749
- }
2750
- logger.debug("[DAEMON RUN] Hanging for a bit - waiting for CLI to kill us because we are running outdated version of the code");
2751
- await new Promise((resolve) => setTimeout(resolve, 1e4));
2752
- process.exit(0);
2753
- }
2754
- const daemonState = await readDaemonState();
2755
- if (daemonState && daemonState.pid !== process.pid) {
2756
- logger.debug("[DAEMON RUN] Somehow a different daemon was started without killing us. We should kill ourselves.");
2757
- requestShutdown("exception", "A different daemon was started without killing us. We should kill ourselves.");
2758
- }
2759
3130
  try {
2760
- const updatedState = {
2761
- pid: process.pid,
2762
- httpPort: controlPort,
2763
- startTime: fileState.startTime,
2764
- startedWithCliVersion: packageJson.version,
2765
- lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
2766
- daemonLogPath: fileState.daemonLogPath
2767
- };
2768
- writeDaemonState(updatedState);
2769
- if (process.env.DEBUG) {
2770
- logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
3131
+ const recoveryResult2 = await recoverTrackedSessionsFromLocalRegistry({
3132
+ trackedSessionPids: pidToTrackedSession.keys(),
3133
+ trackSession: (pid, trackedSession) => {
3134
+ pidToTrackedSession.set(pid, trackedSession);
3135
+ }
3136
+ });
3137
+ if (recoveryResult2.recoveredCount > 0 || recoveryResult2.removedStaleCount > 0) {
3138
+ logger.debug(
3139
+ `[DAEMON RUN] Session registry recovery tick completed: recovered=${recoveryResult2.recoveredCount}, removedStale=${recoveryResult2.removedStaleCount}`
3140
+ );
2771
3141
  }
3142
+ await runDaemonHealthCheck({
3143
+ trackedSessionPids: pidToTrackedSession.keys(),
3144
+ removeTrackedSession: (pid) => {
3145
+ pidToTrackedSession.delete(pid);
3146
+ },
3147
+ currentCliVersion: configuration.currentCliVersion,
3148
+ onDaemonOutdated: async () => {
3149
+ logger.debug("[DAEMON RUN] Daemon is outdated, triggering self-restart with latest version, clearing heartbeat interval");
3150
+ clearInterval(restartOnStaleVersionAndHeartbeat);
3151
+ try {
3152
+ spawnHappyCLI(["daemon", "start"], {
3153
+ detached: true,
3154
+ stdio: "ignore"
3155
+ });
3156
+ } catch (error) {
3157
+ logger.debug("[DAEMON RUN] Failed to spawn new daemon, this is quite likely to happen during integration tests as we are cleaning out dist/ directory", error);
3158
+ }
3159
+ logger.debug("[DAEMON RUN] Hanging for a bit - waiting for CLI to kill us because we are running outdated version of the code");
3160
+ await new Promise((resolve) => setTimeout(resolve, 1e4));
3161
+ process.exit(0);
3162
+ },
3163
+ readDaemonState,
3164
+ daemonPid: process.pid,
3165
+ requestShutdown,
3166
+ writeHeartbeat: async () => {
3167
+ try {
3168
+ const updatedState = {
3169
+ pid: process.pid,
3170
+ httpPort: controlPort,
3171
+ startTime: fileState.startTime,
3172
+ startedWithCliVersion: packageJson.version,
3173
+ lastHeartbeat: (/* @__PURE__ */ new Date()).toLocaleString(),
3174
+ daemonLogPath: fileState.daemonLogPath
3175
+ };
3176
+ writeDaemonState(updatedState);
3177
+ if (process.env.DEBUG) {
3178
+ logger.debug(`[DAEMON RUN] Health check completed at ${updatedState.lastHeartbeat}`);
3179
+ }
3180
+ } catch (error) {
3181
+ logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
3182
+ }
3183
+ }
3184
+ });
2772
3185
  } catch (error) {
2773
- logger.debug("[DAEMON RUN] Failed to write heartbeat", error);
3186
+ logger.warn(
3187
+ "[DAEMON RUN] Health check failed; keeping daemon alive until the next tick",
3188
+ error instanceof Error ? error.message : String(error)
3189
+ );
3190
+ } finally {
3191
+ heartbeatRunning = false;
2774
3192
  }
2775
- heartbeatRunning = false;
2776
3193
  }, heartbeatIntervalMs);
2777
3194
  const cleanupAndShutdown = async (source, errorMessage) => {
2778
3195
  logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
@@ -2909,7 +3326,7 @@ async function uninstall$1() {
2909
3326
  } catch (error) {
2910
3327
  logger.info("Failed to unload daemon (it might not be running)");
2911
3328
  }
2912
- unlinkSync(PLIST_FILE);
3329
+ unlinkSync$1(PLIST_FILE);
2913
3330
  logger.info(`Removed daemon plist from ${PLIST_FILE}`);
2914
3331
  logger.info("Daemon uninstalled successfully");
2915
3332
  } catch (error) {
@@ -3085,9 +3502,11 @@ async function handleAuthStatus() {
3085
3502
  console.log(chalk.gray(`
3086
3503
  Data directory: ${configuration.happyCloudHomeDir}`));
3087
3504
  try {
3088
- const running = await checkIfDaemonRunningAndCleanupStaleState();
3089
- if (running) {
3505
+ const daemonStatus = await getDaemonRuntimeStatus();
3506
+ if (daemonStatus === "running") {
3090
3507
  console.log(chalk.green("\u2713 Daemon running"));
3508
+ } else if (daemonStatus === "indeterminate") {
3509
+ console.log(chalk.yellow("\u26A0\uFE0F Daemon status indeterminate"));
3091
3510
  } else {
3092
3511
  console.log(chalk.gray("\u2717 Daemon not running"));
3093
3512
  }
@@ -6668,6 +7087,189 @@ function validateCodexAcpSpawn(options = {}) {
6668
7087
  return { ok: true, spawn };
6669
7088
  }
6670
7089
 
7090
+ function firstExistingPath(candidates) {
7091
+ for (const candidate of candidates) {
7092
+ try {
7093
+ if (fs$1.existsSync(candidate)) {
7094
+ return candidate;
7095
+ }
7096
+ } catch {
7097
+ }
7098
+ }
7099
+ return null;
7100
+ }
7101
+ function resolveCodexExecutable() {
7102
+ if (process.platform === "win32") {
7103
+ const appData = process.env.APPDATA || path.join(os$1.homedir(), "AppData", "Roaming");
7104
+ const npmGlobalBin = path.join(appData, "npm");
7105
+ const resolved = firstExistingPath([
7106
+ path.join(npmGlobalBin, "codex.cmd"),
7107
+ path.join(npmGlobalBin, "codex.ps1"),
7108
+ path.join(npmGlobalBin, "codex")
7109
+ ]);
7110
+ if (resolved) {
7111
+ return resolved;
7112
+ }
7113
+ }
7114
+ return "codex";
7115
+ }
7116
+
7117
+ function getCodexPlatformTarget(platform, arch) {
7118
+ if (platform === "win32" && arch === "x64") {
7119
+ return {
7120
+ packageName: "codex-win32-x64",
7121
+ targetTriple: "x86_64-pc-windows-msvc"
7122
+ };
7123
+ }
7124
+ if (platform === "win32" && arch === "arm64") {
7125
+ return {
7126
+ packageName: "codex-win32-arm64",
7127
+ targetTriple: "aarch64-pc-windows-msvc"
7128
+ };
7129
+ }
7130
+ if ((platform === "linux" || platform === "android") && arch === "x64") {
7131
+ return {
7132
+ packageName: "codex-linux-x64",
7133
+ targetTriple: "x86_64-unknown-linux-musl"
7134
+ };
7135
+ }
7136
+ if ((platform === "linux" || platform === "android") && arch === "arm64") {
7137
+ return {
7138
+ packageName: "codex-linux-arm64",
7139
+ targetTriple: "aarch64-unknown-linux-musl"
7140
+ };
7141
+ }
7142
+ if (platform === "darwin" && arch === "x64") {
7143
+ return {
7144
+ packageName: "codex-darwin-x64",
7145
+ targetTriple: "x86_64-apple-darwin"
7146
+ };
7147
+ }
7148
+ if (platform === "darwin" && arch === "arm64") {
7149
+ return {
7150
+ packageName: "codex-darwin-arm64",
7151
+ targetTriple: "aarch64-apple-darwin"
7152
+ };
7153
+ }
7154
+ return null;
7155
+ }
7156
+ function dedupePaths(paths, platform) {
7157
+ const seen = /* @__PURE__ */ new Set();
7158
+ const unique = [];
7159
+ for (const entry of paths) {
7160
+ const normalized = normalize(entry);
7161
+ const key = platform === "win32" ? normalized.toLowerCase() : normalized;
7162
+ if (seen.has(key)) {
7163
+ continue;
7164
+ }
7165
+ seen.add(key);
7166
+ unique.push(entry);
7167
+ }
7168
+ return unique;
7169
+ }
7170
+ function resolveCodexShimPath({
7171
+ platform = process.platform,
7172
+ exists = existsSync,
7173
+ resolveExecutable = resolveCodexExecutable,
7174
+ resolveOnPath = resolveCommandOnPath
7175
+ }) {
7176
+ const resolvedExecutable = resolveExecutable();
7177
+ if (isAbsolute(resolvedExecutable) && exists(resolvedExecutable)) {
7178
+ return resolvedExecutable;
7179
+ }
7180
+ const commandNames = platform === "win32" ? ["codex.cmd", "codex.ps1", "codex"] : ["codex"];
7181
+ for (const commandName of commandNames) {
7182
+ const resolved = resolveOnPath(commandName);
7183
+ if (resolved) {
7184
+ return resolved;
7185
+ }
7186
+ }
7187
+ return null;
7188
+ }
7189
+ function resolveCodexPackageRoots(executablePath, {
7190
+ platform = process.platform,
7191
+ exists = existsSync,
7192
+ realpath = realpathSync
7193
+ }) {
7194
+ const executableDir = dirname$1(executablePath);
7195
+ const candidates = [
7196
+ join(executableDir, "node_modules", "@openai", "codex"),
7197
+ join(executableDir, "..", "lib", "node_modules", "@openai", "codex")
7198
+ ];
7199
+ try {
7200
+ const realExecutablePath = realpath(executablePath);
7201
+ candidates.push(join(dirname$1(realExecutablePath), ".."));
7202
+ } catch {
7203
+ }
7204
+ return dedupePaths(
7205
+ candidates.filter((candidate) => exists(candidate)),
7206
+ platform
7207
+ );
7208
+ }
7209
+ function resolvePathEnvKey(env) {
7210
+ return Object.prototype.hasOwnProperty.call(env, "Path") && !Object.prototype.hasOwnProperty.call(env, "PATH") ? "Path" : "PATH";
7211
+ }
7212
+ function resolveBundledCodexToolPathDirs(options = {}) {
7213
+ const platform = options.platform ?? process.platform;
7214
+ const arch = options.arch ?? process.arch;
7215
+ const codexTarget = getCodexPlatformTarget(platform, arch);
7216
+ if (!codexTarget) {
7217
+ return [];
7218
+ }
7219
+ const codexShimPath = resolveCodexShimPath(options);
7220
+ if (!codexShimPath) {
7221
+ return [];
7222
+ }
7223
+ const packageRoots = resolveCodexPackageRoots(codexShimPath, options);
7224
+ if (packageRoots.length === 0) {
7225
+ return [];
7226
+ }
7227
+ const exists = options.exists ?? existsSync;
7228
+ const candidates = packageRoots.flatMap((packageRoot) => [
7229
+ join(packageRoot, "vendor", codexTarget.targetTriple, "path"),
7230
+ join(
7231
+ packageRoot,
7232
+ "node_modules",
7233
+ "@openai",
7234
+ codexTarget.packageName,
7235
+ "vendor",
7236
+ codexTarget.targetTriple,
7237
+ "path"
7238
+ )
7239
+ ]);
7240
+ return dedupePaths(
7241
+ candidates.filter((candidate) => exists(candidate)),
7242
+ platform
7243
+ );
7244
+ }
7245
+ function buildCodexAcpEnv(overrides = {}, options = {}) {
7246
+ const platform = options.platform ?? process.platform;
7247
+ const env = {
7248
+ ...process.env,
7249
+ ...overrides
7250
+ };
7251
+ const pathKey = resolvePathEnvKey(env);
7252
+ const alternatePathKey = pathKey === "PATH" ? "Path" : "PATH";
7253
+ const currentPathValue = env[pathKey] ?? env[alternatePathKey] ?? "";
7254
+ const currentPathEntries = currentPathValue.split(delimiter).filter(Boolean);
7255
+ const codexToolDirs = resolveBundledCodexToolPathDirs(options);
7256
+ const mergedPathEntries = dedupePaths(
7257
+ [...codexToolDirs, ...currentPathEntries],
7258
+ platform
7259
+ );
7260
+ if (mergedPathEntries.length > 0) {
7261
+ env[pathKey] = mergedPathEntries.join(delimiter);
7262
+ }
7263
+ delete env[alternatePathKey];
7264
+ const stringEnvEntries = [];
7265
+ for (const [key, value] of Object.entries(env)) {
7266
+ if (typeof value === "string") {
7267
+ stringEnvEntries.push([key, value]);
7268
+ }
7269
+ }
7270
+ return Object.fromEntries(stringEnvEntries);
7271
+ }
7272
+
6671
7273
  class CodexAcpTransport extends CodexTransport {
6672
7274
  constructor(initTimeoutMs) {
6673
7275
  super();
@@ -6697,10 +7299,10 @@ function createCodexBackend(options) {
6697
7299
  cwd: options.cwd,
6698
7300
  command: spawn.command,
6699
7301
  args: spawn.args,
6700
- env: {
7302
+ env: buildCodexAcpEnv({
6701
7303
  ...options.env,
6702
7304
  NODE_ENV: "production"
6703
- },
7305
+ }),
6704
7306
  permissionHandler: options.permissionHandler,
6705
7307
  selectionHandler: options.selectionHandler,
6706
7308
  transportHandler: resolveCodexTransport(spawn.command)
@@ -6850,6 +7452,24 @@ function createDefaultRuntimeShell() {
6850
7452
  return new RuntimeShell();
6851
7453
  }
6852
7454
 
7455
+ const DAEMON_STARTUP_TIMEOUT_MS = 3e4;
7456
+ const DAEMON_STARTUP_POLL_INTERVAL_MS = 250;
7457
+ async function waitForDaemonReady(timeoutMs = DAEMON_STARTUP_TIMEOUT_MS, pollIntervalMs = DAEMON_STARTUP_POLL_INTERVAL_MS) {
7458
+ const deadline = Date.now() + timeoutMs;
7459
+ while (Date.now() < deadline) {
7460
+ const remainingMs = deadline - Date.now();
7461
+ if (await isDaemonRunningCurrentlyInstalledHappyVersion(Math.max(1, remainingMs))) {
7462
+ return true;
7463
+ }
7464
+ const sleepMs = Math.min(pollIntervalMs, deadline - Date.now());
7465
+ if (sleepMs <= 0) {
7466
+ break;
7467
+ }
7468
+ await new Promise((resolve) => setTimeout(resolve, sleepMs));
7469
+ }
7470
+ return false;
7471
+ }
7472
+
6853
7473
  function isRuntimeProvider(value) {
6854
7474
  return value === "claude" || value === "codex" || value === "gemini" || value === "cursor";
6855
7475
  }
@@ -6894,26 +7514,20 @@ async function ensureUnifiedDaemonStarted() {
6894
7514
  env: process.env
6895
7515
  });
6896
7516
  daemonProcess.unref();
6897
- for (let i = 0; i < 100; i++) {
6898
- if (await isDaemonRunningCurrentlyInstalledHappyVersion()) {
6899
- return;
6900
- }
6901
- if (await isDaemonControlServerResponsive(500)) {
6902
- return;
6903
- }
6904
- await new Promise((resolve) => setTimeout(resolve, 100));
7517
+ if (await waitForDaemonReady()) {
7518
+ return;
6905
7519
  }
6906
7520
  throw new Error("Failed to start Happy background service.");
6907
7521
  }
6908
7522
  async function executeUnifiedProvider(opts) {
6909
7523
  const credentials = await ensureUnifiedRuntimePrerequisites(opts.credentials);
6910
7524
  if (opts.provider === "claude") {
6911
- const { runClaude } = await import('./runClaude-DAR_hw3C.mjs');
7525
+ const { runClaude } = await import('./runClaude-B_fTMxc4.mjs');
6912
7526
  await runClaude(credentials, opts.claudeOptions ?? {});
6913
7527
  return;
6914
7528
  }
6915
7529
  if (opts.provider === "codex") {
6916
- const { runCodex } = await import('./runCodex-B4QAb-Go.mjs');
7530
+ const { runCodex } = await import('./runCodex-CUgOiIEz.mjs');
6917
7531
  await runCodex({
6918
7532
  credentials,
6919
7533
  startedBy: opts.startedBy,
@@ -6923,7 +7537,7 @@ async function executeUnifiedProvider(opts) {
6923
7537
  return;
6924
7538
  }
6925
7539
  if (opts.provider === "gemini") {
6926
- const { runGemini } = await import('./runGemini-DqowSR2w.mjs');
7540
+ const { runGemini } = await import('./runGemini-2_FEtJYa.mjs');
6927
7541
  await runGemini({
6928
7542
  credentials,
6929
7543
  startedBy: opts.startedBy
@@ -6965,7 +7579,7 @@ function shouldRunMainClaudeFlow(opts) {
6965
7579
  return;
6966
7580
  } else if (subcommand === "runtime") {
6967
7581
  if (args[1] === "providers") {
6968
- const { renderRuntimeProviders } = await import('./command-DGFsZx58.mjs');
7582
+ const { renderRuntimeProviders } = await import('./command-ComOeFLY.mjs');
6969
7583
  console.log(renderRuntimeProviders());
6970
7584
  return;
6971
7585
  }
@@ -7143,8 +7757,8 @@ function shouldRunMainClaudeFlow(opts) {
7143
7757
  const projectId = args[3];
7144
7758
  try {
7145
7759
  const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return config; });
7146
- const { readCredentials: readCredentials2 } = await import('./persistence-DiNg1DPF.mjs');
7147
- const { ApiClient: ApiClient2 } = await import('./api-D-uiH_TF.mjs').then(function (n) { return n.q; });
7760
+ const { readCredentials: readCredentials2 } = await import('./persistence-BxP6Jw1f.mjs');
7761
+ const { ApiClient: ApiClient2 } = await import('./api-BjxmW-0W.mjs').then(function (n) { return n.q; });
7148
7762
  let userEmail = void 0;
7149
7763
  try {
7150
7764
  const credentials = await readCredentials2();
@@ -7291,14 +7905,7 @@ function shouldRunMainClaudeFlow(opts) {
7291
7905
  env: process.env
7292
7906
  });
7293
7907
  child.unref();
7294
- let started = false;
7295
- for (let i = 0; i < 100; i++) {
7296
- if (await checkIfDaemonRunningAndCleanupStaleState() && await isDaemonControlServerResponsive(500)) {
7297
- started = true;
7298
- break;
7299
- }
7300
- await new Promise((resolve) => setTimeout(resolve, 100));
7301
- }
7908
+ const started = await waitForDaemonReady();
7302
7909
  if (started) {
7303
7910
  console.log("Daemon started successfully");
7304
7911
  } else {
@@ -7567,4 +8174,4 @@ ${chalk.bold("Examples:")}
7567
8174
  }
7568
8175
  }
7569
8176
 
7570
- export { ExitCodeError as E, GEMINI_MODEL_ENV as G, createGeminiBackend as a, stopCaffeinate as b, createDefaultRuntimeShell as c, createCodexBackend as d, getProjectPath as e, formatDisplayMessage as f, getInitialGeminiModel as g, claudeLocal as h, initialMachineMetadata as i, isBun as j, trimIdent as k, claudeCheckSession as l, getEnvironmentInfo as m, notifyDaemonSessionStarted as n, startCaffeinate as o, projectPath as p, readGeminiLocalConfig as r, saveGeminiModelToConfig as s, truncateDisplayMessage as t, validateCodexAcpSpawn as v };
8177
+ export { ExitCodeError as E, GEMINI_MODEL_ENV as G, createGeminiBackend as a, stopCaffeinate as b, createDefaultRuntimeShell as c, projectPath as d, createCodexBackend as e, formatDisplayMessage as f, getInitialGeminiModel as g, getProjectPath as h, initialMachineMetadata as i, claudeLocal as j, isBun as k, trimIdent as l, claudeCheckSession as m, getEnvironmentInfo as n, startCaffeinate as o, publishSessionRegistration as p, readGeminiLocalConfig as r, saveGeminiModelToConfig as s, truncateDisplayMessage as t, validateCodexAcpSpawn as v };