@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.
- package/bin/conductor-daemon.js +8 -8
- package/bin/conductor-fire.js +9 -0
- package/bin/conductor-update.js +13 -2
- package/package.json +5 -5
- package/src/cli-update-notifier.js +21 -5
- package/src/daemon.js +155 -28
- package/src/version-check.js +51 -1
package/bin/conductor-daemon.js
CHANGED
|
@@ -48,19 +48,19 @@ function resolveLauncherConfig() {
|
|
|
48
48
|
|
|
49
49
|
if (inheritedLauncherScript && inheritedSubcommand === "daemon" && inheritedSubcommandArgs) {
|
|
50
50
|
return {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
package/bin/conductor-fire.js
CHANGED
|
@@ -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;
|
package/bin/conductor-update.js
CHANGED
|
@@ -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.
|
|
4
|
-
"gitCommitId": "
|
|
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.
|
|
22
|
-
"@love-moon/ai-sdk": "0.2.
|
|
23
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(`[
|
|
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(`[
|
|
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 (
|
|
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
|
-
|
|
1972
|
-
await shutdownDaemon("auto-update");
|
|
1997
|
+
await shutdownDaemon(reason);
|
|
1973
1998
|
|
|
1974
|
-
|
|
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(`[
|
|
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]
|
|
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",
|
package/src/version-check.js
CHANGED
|
@@ -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
|
}
|