@love-moon/conductor-cli 0.2.36 → 0.2.38

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.
@@ -48,19 +48,19 @@ function resolveLauncherConfig() {
48
48
 
49
49
  if (inheritedLauncherScript && inheritedSubcommand === "daemon" && inheritedSubcommandArgs) {
50
50
  return {
51
- restartLauncherScript: inheritedLauncherScript,
52
- restartLauncherArgs: ["daemon", ...stripNohupArgs(inheritedSubcommandArgs)],
53
- versionCheckScript: inheritedLauncherScript,
54
- versionCheckArgs: ["--version"],
51
+ RESTART_LAUNCHER_SCRIPT: inheritedLauncherScript,
52
+ RESTART_LAUNCHER_ARGS: ["daemon", ...stripNohupArgs(inheritedSubcommandArgs)],
53
+ VERSION_CHECK_SCRIPT: inheritedLauncherScript,
54
+ VERSION_CHECK_ARGS: ["--version"],
55
55
  };
56
56
  }
57
57
 
58
58
  const daemonScript = path.resolve(process.argv[1]);
59
59
  return {
60
- restartLauncherScript: daemonScript,
61
- restartLauncherArgs: argv,
62
- versionCheckScript: inheritedLauncherScript,
63
- versionCheckArgs: ["--version"],
60
+ RESTART_LAUNCHER_SCRIPT: daemonScript,
61
+ RESTART_LAUNCHER_ARGS: argv,
62
+ VERSION_CHECK_SCRIPT: inheritedLauncherScript,
63
+ VERSION_CHECK_ARGS: ["--version"],
64
64
  };
65
65
  }
66
66
 
@@ -53,6 +53,14 @@ export function isLaunchedByDaemon(env = process.env) {
53
53
  );
54
54
  }
55
55
 
56
+ export function syncPwdEnvWithProcessCwdForDaemonLaunch(env = process.env, cwdFn = process.cwd) {
57
+ if (!isLaunchedByDaemon(env)) {
58
+ return false;
59
+ }
60
+ env.PWD = cwdFn();
61
+ return true;
62
+ }
63
+
56
64
  const ENABLE_FIRE_LOCAL_LOG = !isLaunchedByDaemon(process.env);
57
65
 
58
66
  const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
@@ -570,6 +578,7 @@ export class FireWatchdog {
570
578
  }
571
579
 
572
580
  async function main() {
581
+ syncPwdEnvWithProcessCwdForDaemonLaunch();
573
582
  const cliArgs = await parseCliArgs();
574
583
  let runtimeProjectPath = process.cwd();
575
584
  let backendSession = null;
@@ -6,13 +6,14 @@
6
6
 
7
7
  import { fileURLToPath } from "node:url";
8
8
  import path from "node:path";
9
- import { createRequire } from "node:module";
10
9
  import fs from "node:fs";
11
10
  import { spawn } from "node:child_process";
12
11
  import process from "node:process";
13
12
  import readline from "node:readline/promises";
14
13
  import {
15
14
  PACKAGE_NAME,
15
+ buildUpgradeCommand,
16
+ resolveInstallMethod,
16
17
  fetchLatestVersion,
17
18
  isNewerVersion,
18
19
  detectPackageManager,
@@ -24,11 +25,14 @@ import {
24
25
 
25
26
  const __filename = fileURLToPath(import.meta.url);
26
27
  const __dirname = path.dirname(__filename);
27
- const require = createRequire(import.meta.url);
28
28
  const PKG_ROOT = path.join(__dirname, "..");
29
29
 
30
30
  const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
31
31
  const CURRENT_VERSION = pkgJson.version;
32
+ const INSTALL_METHOD = resolveInstallMethod({
33
+ env: process.env,
34
+ packageRoot: PKG_ROOT,
35
+ });
32
36
 
33
37
  // ANSI 颜色代码
34
38
  const COLORS = {
@@ -56,6 +60,13 @@ async function main() {
56
60
  process.exit(0);
57
61
  }
58
62
 
63
+ if (INSTALL_METHOD === "homebrew") {
64
+ console.log(colorize("🍺 Homebrew-managed install detected", "cyan"));
65
+ console.log("");
66
+ console.log(` Use ${colorize(buildUpgradeCommand({ env: process.env }), "green")} to upgrade conductor.`);
67
+ process.exit(0);
68
+ }
69
+
59
70
  console.log(colorize(`📦 ${PACKAGE_NAME}`, "cyan"));
60
71
  console.log(` Current version: ${CURRENT_VERSION}`);
61
72
  console.log("");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.36",
4
- "gitCommitId": "54d9de4",
3
+ "version": "0.2.38",
4
+ "gitCommitId": "6bf5515",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "conductor": "bin/conductor.js"
@@ -18,9 +18,9 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@love-moon/ai-bridge": "0.1.4",
21
- "@love-moon/ai-manager": "0.2.36",
22
- "@love-moon/ai-sdk": "0.2.36",
23
- "@love-moon/conductor-sdk": "0.2.36",
21
+ "@love-moon/ai-manager": "0.2.38",
22
+ "@love-moon/ai-sdk": "0.2.38",
23
+ "@love-moon/conductor-sdk": "0.2.38",
24
24
  "chrome-launcher": "^1.2.1",
25
25
  "chrome-remote-interface": "^0.33.0",
26
26
  "dotenv": "^16.4.5",
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
 
5
- import { fetchLatestVersion, isNewerVersion } from "./version-check.js";
5
+ import { buildUpgradeCommand, fetchLatestVersion, isNewerVersion } from "./version-check.js";
6
6
 
7
7
  export const DEFAULT_VERSION_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
8
8
  export const DEFAULT_VERSION_NOTIFY_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -115,8 +115,13 @@ export function shouldSkipVersionCheck(options = {}) {
115
115
  return { skip: false, reason: null };
116
116
  }
117
117
 
118
- export function buildUpdateNotice({ currentVersion, latestVersion }) {
119
- return `New conductor version available: ${currentVersion} -> ${latestVersion}. Run: conductor update`;
118
+ export function buildUpdateNotice({ currentVersion, latestVersion, installMethod, env }) {
119
+ const noticeEnv =
120
+ installMethod && !env?.CONDUCTOR_INSTALL_METHOD
121
+ ? { ...env, CONDUCTOR_INSTALL_METHOD: installMethod }
122
+ : env;
123
+ const upgradeCommand = buildUpgradeCommand({ env: noticeEnv });
124
+ return `New conductor version available: ${currentVersion} -> ${latestVersion}. Run: ${upgradeCommand}`;
120
125
  }
121
126
 
122
127
  function shouldNotifyVersion({ latestVersion, currentVersion, cache, nowMs, notifyIntervalMs }) {
@@ -147,6 +152,7 @@ export async function maybeCheckForUpdates(options = {}) {
147
152
  const env = options.env || process.env;
148
153
  const currentVersion = normalizeOptionalString(options.currentVersion);
149
154
  const subcommand = normalizeOptionalString(options.subcommand);
155
+ const installMethod = normalizeOptionalString(env.CONDUCTOR_INSTALL_METHOD);
150
156
  const nowMs = options.nowMs ?? Date.now();
151
157
  const checkIntervalMs = options.checkIntervalMs ?? DEFAULT_VERSION_CHECK_INTERVAL_MS;
152
158
  const notifyIntervalMs = options.notifyIntervalMs ?? DEFAULT_VERSION_NOTIFY_INTERVAL_MS;
@@ -182,7 +188,12 @@ export async function maybeCheckForUpdates(options = {}) {
182
188
  nowMs,
183
189
  notifyIntervalMs,
184
190
  })) {
185
- writeNotice(buildUpdateNotice({ currentVersion, latestVersion: cache.latestVersion }));
191
+ writeNotice(buildUpdateNotice({
192
+ currentVersion,
193
+ latestVersion: cache.latestVersion,
194
+ installMethod,
195
+ env,
196
+ }));
186
197
  cache = createUpdatedCache(cache, {
187
198
  lastNotifiedVersion: cache.latestVersion,
188
199
  lastNotifiedAt: new Date(nowMs).toISOString(),
@@ -223,7 +234,12 @@ export async function maybeCheckForUpdates(options = {}) {
223
234
  nowMs,
224
235
  notifyIntervalMs,
225
236
  })) {
226
- writeNotice(buildUpdateNotice({ currentVersion, latestVersion: versionToNotify }));
237
+ writeNotice(buildUpdateNotice({
238
+ currentVersion,
239
+ latestVersion: versionToNotify,
240
+ installMethod,
241
+ env,
242
+ }));
227
243
  cache = createUpdatedCache(cache, {
228
244
  lastNotifiedVersion: versionToNotify,
229
245
  lastNotifiedAt: new Date(nowMs).toISOString(),
package/src/daemon.js CHANGED
@@ -29,12 +29,14 @@ import {
29
29
  } from "./runtime-backends.js";
30
30
  import {
31
31
  PACKAGE_NAME,
32
+ buildUpgradeCommand,
32
33
  fetchLatestVersion,
33
34
  isNewerVersion,
34
35
  detectPackageManager,
35
36
  parseUpdateWindow,
36
37
  isInUpdateWindow,
37
38
  isManagedInstallPath,
39
+ resolveInstallMethod,
38
40
  } from "./version-check.js";
39
41
  import {
40
42
  ensurePnpmOnlyBuiltDependencies,
@@ -662,12 +664,22 @@ export function startDaemon(config = {}, deps = {}) {
662
664
  const parseUpdateWindowFn = deps.parseUpdateWindow || parseUpdateWindow;
663
665
  const isInUpdateWindowFn = deps.isInUpdateWindow || isInUpdateWindow;
664
666
  const isManagedInstallPathFn = deps.isManagedInstallPath || isManagedInstallPath;
667
+ const resolveInstallMethodFn = deps.resolveInstallMethod || resolveInstallMethod;
665
668
  const installedPackageRoot = deps.packageRoot || PACKAGE_ROOT;
666
669
  const cliVersion = deps.cliVersion || CLI_VERSION;
667
670
  const isBackgroundProcess = deps.isBackgroundProcess ?? !process.stdout.isTTY;
668
671
  const autoUpdateForceLocal = parseBooleanEnv(process.env.CONDUCTOR_AUTO_UPDATE_FORCE_LOCAL);
672
+ const installMethod = resolveInstallMethodFn({
673
+ env: process.env,
674
+ packageRoot: installedPackageRoot,
675
+ readFileSync: deps.readFileSync || fs.readFileSync,
676
+ });
669
677
  const autoUpdateSupportedInstall =
670
- autoUpdateForceLocal || isManagedInstallPathFn(installedPackageRoot);
678
+ (installMethod !== "homebrew") &&
679
+ (autoUpdateForceLocal || isManagedInstallPathFn(installedPackageRoot, {
680
+ env: process.env,
681
+ readFileSync: deps.readFileSync || fs.readFileSync,
682
+ }));
671
683
  const skipPidLockCheck = parseBooleanEnv(process.env.CONDUCTOR_TUI_DEBUG);
672
684
  const lockHandoffToken =
673
685
  normalizeOptionalString(config.LOCK_HANDOFF_TOKEN) ||
@@ -691,6 +703,17 @@ export function startDaemon(config = {}, deps = {}) {
691
703
  autoUpdateSupportedInstall &&
692
704
  (process.env.CONDUCTOR_AUTO_UPDATE !== "false") &&
693
705
  (userConfig.auto_update !== false);
706
+ // Auto-update respawn was historically broken in prod (camelCase/UPPER_SNAKE
707
+ // config-key mismatch between conductor-daemon.js and daemon.js), so no fleet
708
+ // has actually restarted itself via this path. The key mismatch is now fixed,
709
+ // which means auto-update would start respawning daemons globally. To avoid a
710
+ // silent activation, keep respawn gated behind an explicit opt-in until it
711
+ // has been validated on a canary.
712
+ const AUTO_UPDATE_RESPAWN_ENABLED =
713
+ config.AUTO_UPDATE_RESPAWN === true ||
714
+ config.AUTO_UPDATE_RESPAWN === "true" ||
715
+ process.env.CONDUCTOR_AUTO_UPDATE_RESPAWN === "true" ||
716
+ userConfig.auto_update_respawn === true;
694
717
  const UPDATE_WINDOW = parseUpdateWindowFn(
695
718
  process.env.CONDUCTOR_UPDATE_WINDOW || userConfig.update_window || "02:00-04:00"
696
719
  );
@@ -1450,7 +1473,7 @@ export function startDaemon(config = {}, deps = {}) {
1450
1473
  "x-conductor-backends": SUPPORTED_BACKENDS.join(","),
1451
1474
  "x-conductor-version": cliVersion,
1452
1475
  };
1453
- const advertisedCapabilities = ["project_path_validation"];
1476
+ const advertisedCapabilities = ["project_path_validation", "restart_daemon"];
1454
1477
  if (ptyTaskCapabilityEnabled) {
1455
1478
  advertisedCapabilities.push("pty_task", "terminal_snapshot");
1456
1479
  }
@@ -1559,7 +1582,11 @@ export function startDaemon(config = {}, deps = {}) {
1559
1582
  });
1560
1583
 
1561
1584
  if (!AUTO_UPDATE_ENABLED && autoUpdateSupportedInstall === false) {
1562
- log("[auto-update] Disabled for local/dev install; set CONDUCTOR_AUTO_UPDATE_FORCE_LOCAL=true to override");
1585
+ if (installMethod === "homebrew") {
1586
+ log(`[auto-update] Disabled for Homebrew install; use ${buildUpgradeCommand({ env: process.env })}`);
1587
+ } else {
1588
+ log("[auto-update] Disabled for local/dev install; set CONDUCTOR_AUTO_UPDATE_FORCE_LOCAL=true to override");
1589
+ }
1563
1590
  }
1564
1591
 
1565
1592
  watchdogTimer = setInterval(() => {
@@ -1897,7 +1924,7 @@ export function startDaemon(config = {}, deps = {}) {
1897
1924
  return newPkg.version || null;
1898
1925
  }
1899
1926
 
1900
- async function performAutoUpdate(targetVersion) {
1927
+ async function installCliVersion(targetVersion, tag) {
1901
1928
  const pm = detectPackageManagerFn({
1902
1929
  launcherPath: restartLauncherScript || versionCheckScript,
1903
1930
  packageRoot: installedPackageRoot,
@@ -1905,7 +1932,7 @@ export function startDaemon(config = {}, deps = {}) {
1905
1932
  const pkgSpec = `${PACKAGE_NAME}@${targetVersion}`;
1906
1933
 
1907
1934
  if (pm === "pnpm") {
1908
- log("[auto-update] Preparing pnpm native dependency allowlist for node-pty");
1935
+ log(`[${tag}] Preparing pnpm native dependency allowlist for node-pty`);
1909
1936
  await ensurePnpmOnlyBuiltDependencies({
1910
1937
  runCommand: runBufferedCommand,
1911
1938
  dependencies: ["node-pty"],
@@ -1913,9 +1940,7 @@ export function startDaemon(config = {}, deps = {}) {
1913
1940
  });
1914
1941
  }
1915
1942
 
1916
- log(`[auto-update] Installing ${pkgSpec} via ${pm}...`);
1917
-
1918
- // Step 1: install
1943
+ log(`[${tag}] Installing ${pkgSpec} via ${pm}...`);
1919
1944
  const result = await runInstallCommand(pm, pkgSpec);
1920
1945
  if (!result.success) {
1921
1946
  throw new Error(
@@ -1923,13 +1948,6 @@ export function startDaemon(config = {}, deps = {}) {
1923
1948
  );
1924
1949
  }
1925
1950
 
1926
- // Step 2: re-check active tasks — a task may have arrived during the install
1927
- if (hasActiveTasks()) {
1928
- log("[auto-update] Active tasks appeared during install; aborting restart (will retry later)");
1929
- return;
1930
- }
1931
-
1932
- // Step 3: verify installed version using the globally resolved CLI entry point.
1933
1951
  try {
1934
1952
  const installedVersion = await readInstalledCliVersion();
1935
1953
  if (installedVersion !== targetVersion) {
@@ -1941,7 +1959,6 @@ export function startDaemon(config = {}, deps = {}) {
1941
1959
  throw new Error(`Version verification failed: ${verifyErr?.message || verifyErr}`);
1942
1960
  }
1943
1961
 
1944
- // Step 4: repair and verify native dependencies before shutting down the healthy daemon.
1945
1962
  try {
1946
1963
  await repairAndVerifyGlobalNodePty({
1947
1964
  packageManager: pm,
@@ -1953,26 +1970,33 @@ export function startDaemon(config = {}, deps = {}) {
1953
1970
  throw new Error(`Native dependency verification failed: ${verifyErr?.message || verifyErr}`);
1954
1971
  }
1955
1972
 
1956
- log(`[auto-update] Verified ${targetVersion} and node-pty. Restarting daemon...`);
1973
+ log(`[${tag}] Installed and verified ${targetVersion} (node-pty OK)`);
1974
+ }
1975
+
1976
+ async function restartDaemonProcess(reason, { allowForegroundRespawn = false } = {}) {
1977
+ const shouldRespawn = isBackgroundProcess || allowForegroundRespawn;
1978
+ if (shouldRespawn && !restartLauncherScript) {
1979
+ throw new Error("Missing daemon restart launcher script");
1980
+ }
1957
1981
 
1958
1982
  let logFd = null;
1959
- if (isBackgroundProcess) {
1960
- if (!restartLauncherScript) {
1961
- throw new Error("Missing daemon restart launcher script");
1962
- }
1983
+ if (shouldRespawn) {
1963
1984
  try {
1964
1985
  mkdirSyncFn(DAEMON_LOG_DIR, { recursive: true });
1965
1986
  } catch {
1966
1987
  /* ignore */
1967
1988
  }
1968
1989
  logFd = fs.openSync(DAEMON_LOG_PATH, "a");
1990
+ if (!isBackgroundProcess) {
1991
+ log(
1992
+ `[${reason}] Foreground daemon will be respawned in background. Logs: ${DAEMON_LOG_PATH}`
1993
+ );
1994
+ }
1969
1995
  }
1970
1996
 
1971
- // Step 5: graceful shutdown
1972
- await shutdownDaemon("auto-update");
1997
+ await shutdownDaemon(reason);
1973
1998
 
1974
- // Step 6: re-spawn (only in background/nohup mode)
1975
- if (isBackgroundProcess) {
1999
+ if (shouldRespawn) {
1976
2000
  const handoffToken = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
1977
2001
  const handoffExpiresAt = Date.now() + 15_000;
1978
2002
  try {
@@ -1992,7 +2016,7 @@ export function startDaemon(config = {}, deps = {}) {
1992
2016
  },
1993
2017
  });
1994
2018
  child.unref();
1995
- log(`[auto-update] New daemon spawned (PID ${child.pid})`);
2019
+ log(`[${reason}] New daemon spawned (PID ${child.pid})`);
1996
2020
  } catch (error) {
1997
2021
  cleanupLock();
1998
2022
  exitFn(1);
@@ -2003,11 +2027,108 @@ export function startDaemon(config = {}, deps = {}) {
2003
2027
  }
2004
2028
  }
2005
2029
  } else {
2030
+ log(`[${reason}] Foreground mode — please restart the daemon manually.`);
2031
+ }
2032
+ exitFn(0);
2033
+ }
2034
+
2035
+ async function performAutoUpdate(targetVersion) {
2036
+ await installCliVersion(targetVersion, "auto-update");
2037
+
2038
+ if (!AUTO_UPDATE_RESPAWN_ENABLED) {
2006
2039
  log(
2007
- `[auto-update] Updated to ${targetVersion}. Foreground mode please restart the daemon.`
2040
+ `[auto-update] Installed ${targetVersion}. Respawn is gated off (set CONDUCTOR_AUTO_UPDATE_RESPAWN=true to enable); new version will take effect on next manual restart.`
2008
2041
  );
2042
+ return;
2043
+ }
2044
+
2045
+ // Re-check active tasks — a task may have arrived during the install
2046
+ if (hasActiveTasks()) {
2047
+ log("[auto-update] Active tasks appeared during install; aborting restart (will retry later)");
2048
+ return;
2049
+ }
2050
+
2051
+ log(`[auto-update] Restarting daemon after upgrade to ${targetVersion}...`);
2052
+ await restartDaemonProcess("auto-update");
2053
+ }
2054
+
2055
+ async function handleRestartDaemon(payload) {
2056
+ const requestId = payload?.request_id ? String(payload.request_id) : "";
2057
+ const targetVersionRaw = payload?.target_version
2058
+ ? String(payload.target_version).trim()
2059
+ : "latest";
2060
+
2061
+ if (daemonShuttingDown) {
2062
+ log(`[restart_daemon] Ignored (${requestId}): daemon already shutting down`);
2063
+ sendAgentCommandAck({
2064
+ requestId,
2065
+ eventType: "restart_daemon",
2066
+ accepted: false,
2067
+ }).catch(() => {});
2068
+ return;
2069
+ }
2070
+ if (autoUpdateInProgress) {
2071
+ log(`[restart_daemon] Ignored (${requestId}): restart already in progress`);
2072
+ sendAgentCommandAck({
2073
+ requestId,
2074
+ eventType: "restart_daemon",
2075
+ accepted: false,
2076
+ }).catch(() => {});
2077
+ return;
2078
+ }
2079
+
2080
+ autoUpdateInProgress = true;
2081
+ try {
2082
+ log(
2083
+ `[restart_daemon] Received (request_id=${requestId}, target=${targetVersionRaw}, current=${cliVersion})`
2084
+ );
2085
+ // Ack receipt before blocking work so the server learns this daemon
2086
+ // accepted the command even if install/shutdown takes several seconds.
2087
+ await sendAgentCommandAck({
2088
+ requestId,
2089
+ eventType: "restart_daemon",
2090
+ accepted: true,
2091
+ }).catch(() => {});
2092
+
2093
+ let resolvedTarget = null;
2094
+ if (targetVersionRaw === "latest") {
2095
+ try {
2096
+ const latest = await fetchLatestVersionFn();
2097
+ if (latest && SEMVER_RE.test(latest) && isNewerVersionFn(latest, cliVersion)) {
2098
+ resolvedTarget = latest;
2099
+ } else if (latest) {
2100
+ log(`[restart_daemon] Already on latest (${cliVersion}); plain restart`);
2101
+ }
2102
+ } catch (err) {
2103
+ logError(`[restart_daemon] Failed to fetch latest version: ${err?.message || err}`);
2104
+ }
2105
+ } else if (SEMVER_RE.test(targetVersionRaw) && targetVersionRaw !== cliVersion) {
2106
+ resolvedTarget = targetVersionRaw;
2107
+ }
2108
+
2109
+ if (resolvedTarget) {
2110
+ try {
2111
+ await installCliVersion(resolvedTarget, "restart_daemon");
2112
+ } catch (err) {
2113
+ logError(
2114
+ `[restart_daemon] Install failed, falling back to plain restart: ${err?.message || err}`
2115
+ );
2116
+ }
2117
+ }
2118
+
2119
+ try {
2120
+ await restartDaemonProcess("restart_daemon", { allowForegroundRespawn: true });
2121
+ } catch (err) {
2122
+ logError(`[restart_daemon] Restart failed after shutdown; exiting: ${err?.message || err}`);
2123
+ cleanupLock();
2124
+ exitFn(1);
2125
+ }
2126
+ } finally {
2127
+ // Clear in case restartDaemonProcess never actually exited (e.g. in tests
2128
+ // where exitFn is mocked). In real runtime exitFn is process.exit, so
2129
+ // this line is unreachable on both success and failure paths.
2130
+ autoUpdateInProgress = false;
2009
2131
  }
2010
- exitFn(0);
2011
2132
  }
2012
2133
 
2013
2134
  const getActiveTaskIds = () => [
@@ -3439,6 +3560,11 @@ export function startDaemon(config = {}, deps = {}) {
3439
3560
  logError(`Unhandled ai_manager_request failure: ${error?.message || error}`);
3440
3561
  });
3441
3562
  }
3563
+ if (event.type === "restart_daemon") {
3564
+ void handleRestartDaemon(event.payload).catch((error) => {
3565
+ logError(`Unhandled restart_daemon failure: ${error?.message || error}`);
3566
+ });
3567
+ }
3442
3568
  }
3443
3569
 
3444
3570
  function markWatchdogHealthy(signal, at = Date.now()) {
@@ -4136,6 +4262,7 @@ export function startDaemon(config = {}, deps = {}) {
4136
4262
 
4137
4263
  const env = {
4138
4264
  ...process.env,
4265
+ PWD: taskDir,
4139
4266
  CONDUCTOR_PROJECT_ID: projectId,
4140
4267
  CONDUCTOR_TASK_ID: taskId,
4141
4268
  CONDUCTOR_LAUNCHED_BY_DAEMON: "1",
@@ -2,14 +2,17 @@
2
2
  * Shared version-check utilities used by both `conductor update` and the daemon auto-update flow.
3
3
  */
4
4
 
5
+ import fs from "node:fs";
5
6
  import http from "node:http";
6
7
  import https from "node:https";
7
8
  import path from "node:path";
8
9
  import { execFileSync, execSync } from "node:child_process";
9
10
 
10
11
  export const PACKAGE_NAME = "@love-moon/conductor-cli";
12
+ export const DEFAULT_HOMEBREW_FORMULA = "lovemoon-ai/tap/conductor";
11
13
  const DEFAULT_UPDATE_WINDOW = { startMinutes: 120, endMinutes: 240 };
12
14
  const REQUEST_TIMEOUT_MS = 10_000;
15
+ const INSTALL_METHOD_FILENAME = ".install-method";
13
16
 
14
17
  function resolveTimeoutMs(value) {
15
18
  const parsed = Number.parseInt(String(value ?? ""), 10);
@@ -100,6 +103,50 @@ function getRegistryBaseUrl(overrideRegistryUrl) {
100
103
  return candidate;
101
104
  }
102
105
 
106
+ function normalizeInstallMethod(value) {
107
+ if (typeof value !== "string") {
108
+ return null;
109
+ }
110
+ const normalized = value.trim().toLowerCase();
111
+ return normalized || null;
112
+ }
113
+
114
+ export function resolveHomebrewFormula(env = process.env) {
115
+ const configured = typeof env?.CONDUCTOR_HOMEBREW_FORMULA === "string"
116
+ ? env.CONDUCTOR_HOMEBREW_FORMULA.trim()
117
+ : "";
118
+ return configured || DEFAULT_HOMEBREW_FORMULA;
119
+ }
120
+
121
+ export function resolveInstallMethod(options = {}) {
122
+ const env = options.env || process.env;
123
+ const envMethod = normalizeInstallMethod(env?.CONDUCTOR_INSTALL_METHOD);
124
+ if (envMethod) {
125
+ return envMethod;
126
+ }
127
+
128
+ const packageRoot = typeof options.packageRoot === "string" ? options.packageRoot.trim() : "";
129
+ if (!packageRoot) {
130
+ return null;
131
+ }
132
+
133
+ const readFileSyncFn = options.readFileSync || fs.readFileSync;
134
+ const installMethodPath = path.join(path.resolve(packageRoot), INSTALL_METHOD_FILENAME);
135
+ try {
136
+ return normalizeInstallMethod(readFileSyncFn(installMethodPath, "utf-8"));
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ export function buildUpgradeCommand(options = {}) {
143
+ const installMethod = resolveInstallMethod(options);
144
+ if (installMethod === "homebrew") {
145
+ return `brew upgrade ${resolveHomebrewFormula(options.env || process.env)}`;
146
+ }
147
+ return "conductor update";
148
+ }
149
+
103
150
  /**
104
151
  * Compare two semver-like version strings.
105
152
  * Returns `true` when `latest` is strictly newer than `current`.
@@ -217,7 +264,10 @@ export function parseUpdateWindow(str) {
217
264
  * Auto-update should only mutate managed/global installs. Local repo runs and pnpm-linked
218
265
  * worktrees are treated as development installs and are skipped by default.
219
266
  */
220
- export function isManagedInstallPath(packageRoot) {
267
+ export function isManagedInstallPath(packageRoot, options = {}) {
268
+ if (resolveInstallMethod({ ...options, packageRoot }) === "homebrew") {
269
+ return false;
270
+ }
221
271
  if (typeof packageRoot !== "string" || !packageRoot.trim()) {
222
272
  return false;
223
273
  }