@love-moon/conductor-cli 0.2.37 → 0.2.39
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-fire.js +289 -8
- package/bin/conductor-update.js +13 -2
- package/package.json +5 -5
- package/src/cli-update-notifier.js +21 -5
- package/src/daemon.js +46 -8
- package/src/runtime-backends.js +44 -1
- package/src/version-check.js +51 -1
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"));
|
|
@@ -81,6 +89,13 @@ export function shouldRunReconnectRecovery({
|
|
|
81
89
|
return !runner.shouldSuppressReconnectRecovery();
|
|
82
90
|
}
|
|
83
91
|
|
|
92
|
+
export function shouldFireReportTaskStatus({ launchedByDaemon = false, phase } = {}) {
|
|
93
|
+
if (phase === "final") {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return !launchedByDaemon;
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
// Load allow_cli_list from config file (no defaults - must be configured)
|
|
85
100
|
function loadFireConfigYaml(configFilePath) {
|
|
86
101
|
const home = os.homedir();
|
|
@@ -569,7 +584,41 @@ export class FireWatchdog {
|
|
|
569
584
|
}
|
|
570
585
|
}
|
|
571
586
|
|
|
587
|
+
export function createPendingRemoteInterruptQueue() {
|
|
588
|
+
const pending = [];
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
enqueue(event) {
|
|
592
|
+
return new Promise((resolve) => {
|
|
593
|
+
pending.push({ event, resolve });
|
|
594
|
+
});
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
async flushWith(dispatch) {
|
|
598
|
+
while (pending.length > 0) {
|
|
599
|
+
const next = pending.shift();
|
|
600
|
+
if (!next) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
next.resolve(await dispatch(next.event));
|
|
605
|
+
} catch {
|
|
606
|
+
next.resolve(false);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
rejectAll() {
|
|
612
|
+
while (pending.length > 0) {
|
|
613
|
+
const next = pending.shift();
|
|
614
|
+
next?.resolve(false);
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
572
620
|
async function main() {
|
|
621
|
+
syncPwdEnvWithProcessCwdForDaemonLaunch();
|
|
573
622
|
const cliArgs = await parseCliArgs();
|
|
574
623
|
let runtimeProjectPath = process.cwd();
|
|
575
624
|
let backendSession = null;
|
|
@@ -628,6 +677,7 @@ async function main() {
|
|
|
628
677
|
let reconnectRunner = null;
|
|
629
678
|
let reconnectTaskId = null;
|
|
630
679
|
let pendingRemoteStopEvent = null;
|
|
680
|
+
const pendingRemoteInterruptQueue = createPendingRemoteInterruptQueue();
|
|
631
681
|
let conductor = null;
|
|
632
682
|
let reconnectResumeInFlight = false;
|
|
633
683
|
let fireShuttingDown = false;
|
|
@@ -667,7 +717,7 @@ async function main() {
|
|
|
667
717
|
source: "conductor-fire",
|
|
668
718
|
metadata: { reconnect: true },
|
|
669
719
|
});
|
|
670
|
-
if (
|
|
720
|
+
if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "reconnect_running" })) {
|
|
671
721
|
await conductor.sendTaskStatus(reconnectTaskId, {
|
|
672
722
|
status: "RUNNING",
|
|
673
723
|
summary: "conductor fire reconnected",
|
|
@@ -697,6 +747,21 @@ async function main() {
|
|
|
697
747
|
pendingRemoteStopEvent = event;
|
|
698
748
|
};
|
|
699
749
|
|
|
750
|
+
const handleInterruptTurnCommand = async (event) => {
|
|
751
|
+
fireWatchdog.onInbound();
|
|
752
|
+
if (!event || typeof event !== "object") {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
const taskId = typeof event.taskId === "string" ? event.taskId : "";
|
|
756
|
+
if (reconnectTaskId && taskId && taskId !== reconnectTaskId) {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
if (reconnectRunner && typeof reconnectRunner.requestInterruptFromRemote === "function") {
|
|
760
|
+
return await reconnectRunner.requestInterruptFromRemote(event);
|
|
761
|
+
}
|
|
762
|
+
return await pendingRemoteInterruptQueue.enqueue(event);
|
|
763
|
+
};
|
|
764
|
+
|
|
700
765
|
if (cliArgs.configFile) {
|
|
701
766
|
env.CONDUCTOR_CONFIG = cliArgs.configFile;
|
|
702
767
|
}
|
|
@@ -747,6 +812,7 @@ async function main() {
|
|
|
747
812
|
fireWatchdog.onPong(event);
|
|
748
813
|
},
|
|
749
814
|
onStopTask: handleStopTaskCommand,
|
|
815
|
+
onInterruptTurn: handleInterruptTurnCommand,
|
|
750
816
|
});
|
|
751
817
|
|
|
752
818
|
const taskContext = await ensureTaskContext(conductor, {
|
|
@@ -844,6 +910,7 @@ async function main() {
|
|
|
844
910
|
await runner.requestStopFromRemote(pendingRemoteStopEvent);
|
|
845
911
|
pendingRemoteStopEvent = null;
|
|
846
912
|
}
|
|
913
|
+
await pendingRemoteInterruptQueue.flushWith((event) => runner.requestInterruptFromRemote(event));
|
|
847
914
|
|
|
848
915
|
const signals = new AbortController();
|
|
849
916
|
let shutdownSignal = null;
|
|
@@ -878,7 +945,7 @@ async function main() {
|
|
|
878
945
|
process.on("SIGINT", onSigint);
|
|
879
946
|
process.on("SIGTERM", onSigterm);
|
|
880
947
|
|
|
881
|
-
if (
|
|
948
|
+
if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "running" })) {
|
|
882
949
|
try {
|
|
883
950
|
await conductor.sendTaskStatus(taskContext.taskId, {
|
|
884
951
|
status: "RUNNING",
|
|
@@ -902,7 +969,7 @@ async function main() {
|
|
|
902
969
|
} finally {
|
|
903
970
|
process.off("SIGINT", onSigint);
|
|
904
971
|
process.off("SIGTERM", onSigterm);
|
|
905
|
-
if (
|
|
972
|
+
if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "final" })) {
|
|
906
973
|
const remoteStopReason = typeof runner.getRemoteStopReason === "function" ? runner.getRemoteStopReason() : null;
|
|
907
974
|
const remoteStopSummary = typeof runner.getRemoteStopSummary === "function" ? runner.getRemoteStopSummary() : null;
|
|
908
975
|
// When the task was deleted by the user, the DB record is already gone —
|
|
@@ -925,10 +992,10 @@ async function main() {
|
|
|
925
992
|
status: "KILLED",
|
|
926
993
|
summary: remoteStopSummary,
|
|
927
994
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
995
|
+
: {
|
|
996
|
+
status: "COMPLETED",
|
|
997
|
+
summary: "conductor fire exited",
|
|
998
|
+
};
|
|
932
999
|
if (!taskDeletedByUser) {
|
|
933
1000
|
try {
|
|
934
1001
|
const statusResult = await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
|
|
@@ -957,6 +1024,7 @@ async function main() {
|
|
|
957
1024
|
}
|
|
958
1025
|
}
|
|
959
1026
|
} finally {
|
|
1027
|
+
pendingRemoteInterruptQueue.rejectAll();
|
|
960
1028
|
fireShuttingDown = true;
|
|
961
1029
|
fireWatchdog.stop();
|
|
962
1030
|
if (backendSession && typeof backendSession.close === "function") {
|
|
@@ -1722,6 +1790,9 @@ export class BridgeRunner {
|
|
|
1722
1790
|
os.hostname();
|
|
1723
1791
|
this.needsReconnectRecovery = false;
|
|
1724
1792
|
this.remoteStopInfo = null;
|
|
1793
|
+
this.remoteInterruptsByReplyTo = new Map();
|
|
1794
|
+
this.pendingInterruptRetryTimers = new Map();
|
|
1795
|
+
this.activeTurnReplyTo = "";
|
|
1725
1796
|
this.sessionAnnouncementSent = false;
|
|
1726
1797
|
this.boundSessionId = "";
|
|
1727
1798
|
this.errorLoop = null;
|
|
@@ -1981,6 +2052,200 @@ export class BridgeRunner {
|
|
|
1981
2052
|
}
|
|
1982
2053
|
}
|
|
1983
2054
|
|
|
2055
|
+
normalizeReplyTarget(replyTo) {
|
|
2056
|
+
return typeof replyTo === "string" ? replyTo.trim() : "";
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
isTurnInterruptedError(error) {
|
|
2060
|
+
const reason = typeof error?.reason === "string" ? error.reason.trim().toLowerCase() : "";
|
|
2061
|
+
if (reason === "turn_interrupted" || reason === "turn_cancelled") {
|
|
2062
|
+
return true;
|
|
2063
|
+
}
|
|
2064
|
+
const turnStatus = typeof error?.turnStatus === "string" ? error.turnStatus.trim().toLowerCase() : "";
|
|
2065
|
+
if (
|
|
2066
|
+
turnStatus === "interrupted" ||
|
|
2067
|
+
turnStatus === "cancelled" ||
|
|
2068
|
+
turnStatus === "canceled" ||
|
|
2069
|
+
turnStatus === "aborted"
|
|
2070
|
+
) {
|
|
2071
|
+
return true;
|
|
2072
|
+
}
|
|
2073
|
+
const name = typeof error?.name === "string" ? error.name.trim().toLowerCase() : "";
|
|
2074
|
+
if (name === "aborterror") {
|
|
2075
|
+
return true;
|
|
2076
|
+
}
|
|
2077
|
+
const message = String(error?.message || error || "").toLowerCase();
|
|
2078
|
+
return (
|
|
2079
|
+
message.includes(" interrupted") ||
|
|
2080
|
+
message.includes("interrupt ") ||
|
|
2081
|
+
message.includes("turn interrupted") ||
|
|
2082
|
+
message.includes("cancelled") ||
|
|
2083
|
+
message.includes("canceled") ||
|
|
2084
|
+
message.includes("aborted")
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
async requestInterruptFromRemote(event = {}) {
|
|
2089
|
+
const taskId = typeof event.taskId === "string" ? event.taskId.trim() : "";
|
|
2090
|
+
if (taskId && taskId !== this.taskId) {
|
|
2091
|
+
return false;
|
|
2092
|
+
}
|
|
2093
|
+
const requestId = typeof event.requestId === "string" ? event.requestId.trim() : "";
|
|
2094
|
+
const reason = typeof event.reason === "string" ? event.reason.trim() : "";
|
|
2095
|
+
const targetReplyTo = this.normalizeReplyTarget(event.targetReplyTo);
|
|
2096
|
+
if (!targetReplyTo) {
|
|
2097
|
+
return false;
|
|
2098
|
+
}
|
|
2099
|
+
if (this.processedMessageIds.has(targetReplyTo)) {
|
|
2100
|
+
this.copilotLog(`ignore late interrupt_turn for processed replyTo=${targetReplyTo}`);
|
|
2101
|
+
return false;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
const existing = this.remoteInterruptsByReplyTo.get(targetReplyTo) || {};
|
|
2105
|
+
const interruptInfo = {
|
|
2106
|
+
requestId: requestId || existing.requestId || null,
|
|
2107
|
+
reason: reason || existing.reason || "user_interrupt",
|
|
2108
|
+
issued: Boolean(existing.issued),
|
|
2109
|
+
};
|
|
2110
|
+
this.remoteInterruptsByReplyTo.set(targetReplyTo, interruptInfo);
|
|
2111
|
+
log(
|
|
2112
|
+
`Received interrupt_turn for ${this.taskId} replyTo=${targetReplyTo}${
|
|
2113
|
+
interruptInfo.reason ? ` (${interruptInfo.reason})` : ""
|
|
2114
|
+
}`,
|
|
2115
|
+
);
|
|
2116
|
+
return await this.issueInterruptForReplyTarget(targetReplyTo);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
async issueInterruptForReplyTarget(replyTo) {
|
|
2120
|
+
const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
|
|
2121
|
+
if (!normalizedReplyTo) {
|
|
2122
|
+
return false;
|
|
2123
|
+
}
|
|
2124
|
+
const interruptInfo = this.remoteInterruptsByReplyTo.get(normalizedReplyTo);
|
|
2125
|
+
if (!interruptInfo) {
|
|
2126
|
+
return false;
|
|
2127
|
+
}
|
|
2128
|
+
if (interruptInfo.issued) {
|
|
2129
|
+
return true;
|
|
2130
|
+
}
|
|
2131
|
+
const supportsTurnInterrupt = typeof this.backendSession?.interruptCurrentTurn === "function";
|
|
2132
|
+
const isActiveTarget = this.runningTurn && normalizedReplyTo === this.activeTurnReplyTo;
|
|
2133
|
+
const isInFlightTarget = this.inFlightMessageIds.has(normalizedReplyTo);
|
|
2134
|
+
|
|
2135
|
+
if (!isActiveTarget && isInFlightTarget) {
|
|
2136
|
+
this.copilotLog(`interrupt arrived after replyTo=${normalizedReplyTo} stopped being interruptible`);
|
|
2137
|
+
return false;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
if (!isActiveTarget) {
|
|
2141
|
+
if (!supportsTurnInterrupt) {
|
|
2142
|
+
log(`Backend session for ${this.taskId} does not support turn interruption`);
|
|
2143
|
+
return false;
|
|
2144
|
+
}
|
|
2145
|
+
this.copilotLog(`queued interrupt request for future replyTo=${normalizedReplyTo}`);
|
|
2146
|
+
return true;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
if (!supportsTurnInterrupt) {
|
|
2150
|
+
log(`Backend session for ${this.taskId} does not support turn interruption`);
|
|
2151
|
+
return false;
|
|
2152
|
+
}
|
|
2153
|
+
try {
|
|
2154
|
+
const interrupted = await this.backendSession.interruptCurrentTurn();
|
|
2155
|
+
if (interrupted === false) {
|
|
2156
|
+
interruptInfo.issued = false;
|
|
2157
|
+
this.remoteInterruptsByReplyTo.set(normalizedReplyTo, interruptInfo);
|
|
2158
|
+
if (
|
|
2159
|
+
this.runningTurn &&
|
|
2160
|
+
this.activeTurnReplyTo === normalizedReplyTo &&
|
|
2161
|
+
this.inFlightMessageIds.has(normalizedReplyTo)
|
|
2162
|
+
) {
|
|
2163
|
+
this.copilotLog(`backend interrupt not ready replyTo=${normalizedReplyTo}; retrying`);
|
|
2164
|
+
this.scheduleInterruptRetryForReplyTarget(normalizedReplyTo);
|
|
2165
|
+
return true;
|
|
2166
|
+
}
|
|
2167
|
+
return false;
|
|
2168
|
+
}
|
|
2169
|
+
interruptInfo.issued = true;
|
|
2170
|
+
this.remoteInterruptsByReplyTo.set(normalizedReplyTo, interruptInfo);
|
|
2171
|
+
this.copilotLog(`requested backend interrupt replyTo=${normalizedReplyTo}`);
|
|
2172
|
+
return true;
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
interruptInfo.issued = false;
|
|
2175
|
+
this.remoteInterruptsByReplyTo.set(normalizedReplyTo, interruptInfo);
|
|
2176
|
+
log(`Failed to interrupt replyTo=${normalizedReplyTo} for ${this.taskId}: ${error?.message || error}`);
|
|
2177
|
+
return false;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
scheduleInterruptRetryForReplyTarget(replyTo) {
|
|
2182
|
+
const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
|
|
2183
|
+
if (!normalizedReplyTo || this.pendingInterruptRetryTimers.has(normalizedReplyTo)) {
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
const timer = setTimeout(() => {
|
|
2188
|
+
this.pendingInterruptRetryTimers.delete(normalizedReplyTo);
|
|
2189
|
+
const interruptInfo = this.remoteInterruptsByReplyTo.get(normalizedReplyTo);
|
|
2190
|
+
if (
|
|
2191
|
+
!interruptInfo ||
|
|
2192
|
+
interruptInfo.issued ||
|
|
2193
|
+
this.processedMessageIds.has(normalizedReplyTo) ||
|
|
2194
|
+
!this.runningTurn ||
|
|
2195
|
+
this.activeTurnReplyTo !== normalizedReplyTo ||
|
|
2196
|
+
!this.inFlightMessageIds.has(normalizedReplyTo)
|
|
2197
|
+
) {
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
void this.issueInterruptForReplyTarget(normalizedReplyTo);
|
|
2201
|
+
}, 50);
|
|
2202
|
+
if (typeof timer.unref === "function") {
|
|
2203
|
+
timer.unref();
|
|
2204
|
+
}
|
|
2205
|
+
this.pendingInterruptRetryTimers.set(normalizedReplyTo, timer);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
clearInterruptRetryForReplyTarget(replyTo) {
|
|
2209
|
+
const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
|
|
2210
|
+
const timer = this.pendingInterruptRetryTimers.get(normalizedReplyTo);
|
|
2211
|
+
if (!timer) {
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
clearTimeout(timer);
|
|
2215
|
+
this.pendingInterruptRetryTimers.delete(normalizedReplyTo);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
async handleInterruptedTurn(replyTo, interruptInfo) {
|
|
2219
|
+
const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
|
|
2220
|
+
this.clearInterruptRetryForReplyTarget(normalizedReplyTo);
|
|
2221
|
+
this.copilotLog(`turn interrupted replyTo=${normalizedReplyTo || "latest"}`);
|
|
2222
|
+
await this.reportRuntimeStatus(
|
|
2223
|
+
{
|
|
2224
|
+
phase: "interrupted",
|
|
2225
|
+
reply_in_progress: false,
|
|
2226
|
+
status_done_line: "Conversation interrupted",
|
|
2227
|
+
},
|
|
2228
|
+
normalizedReplyTo,
|
|
2229
|
+
);
|
|
2230
|
+
try {
|
|
2231
|
+
await this.conductor.sendMessage(this.taskId, "Conversation interrupted", {
|
|
2232
|
+
backend: this.backendName,
|
|
2233
|
+
reply_to: normalizedReplyTo || undefined,
|
|
2234
|
+
interrupted: true,
|
|
2235
|
+
interruption_request_id: interruptInfo?.requestId || undefined,
|
|
2236
|
+
reason: interruptInfo?.reason || undefined,
|
|
2237
|
+
cli_args: this.cliArgs,
|
|
2238
|
+
});
|
|
2239
|
+
} catch (error) {
|
|
2240
|
+
log(`Failed to send interrupt confirmation for ${this.taskId}: ${error?.message || error}`);
|
|
2241
|
+
}
|
|
2242
|
+
if (normalizedReplyTo) {
|
|
2243
|
+
this.processedMessageIds.add(normalizedReplyTo);
|
|
2244
|
+
this.remoteInterruptsByReplyTo.delete(normalizedReplyTo);
|
|
2245
|
+
}
|
|
2246
|
+
this.resetErrorLoop();
|
|
2247
|
+
}
|
|
2248
|
+
|
|
1984
2249
|
async recoverAfterReconnect() {
|
|
1985
2250
|
if (!this.needsReconnectRecovery) {
|
|
1986
2251
|
return;
|
|
@@ -2493,6 +2758,7 @@ export class BridgeRunner {
|
|
|
2493
2758
|
}
|
|
2494
2759
|
this.lastRuntimeStatusSignature = null;
|
|
2495
2760
|
this.runningTurn = true;
|
|
2761
|
+
this.activeTurnReplyTo = this.normalizeReplyTarget(replyTo);
|
|
2496
2762
|
const turnStartedAt = Date.now();
|
|
2497
2763
|
let turnWatchdog = null;
|
|
2498
2764
|
if (this.isCopilotBackend) {
|
|
@@ -2531,12 +2797,15 @@ export class BridgeRunner {
|
|
|
2531
2797
|
);
|
|
2532
2798
|
}
|
|
2533
2799
|
|
|
2534
|
-
const
|
|
2800
|
+
const turnPromise = this.backendSession.runTurn(content, {
|
|
2535
2801
|
useInitialImages,
|
|
2536
2802
|
onProgress: (payload) => {
|
|
2537
2803
|
void this.reportRuntimeStatus(payload, replyTo);
|
|
2538
2804
|
},
|
|
2539
2805
|
});
|
|
2806
|
+
await this.issueInterruptForReplyTarget(replyTo);
|
|
2807
|
+
const result = await turnPromise;
|
|
2808
|
+
this.activeTurnReplyTo = "";
|
|
2540
2809
|
this.copilotLog(
|
|
2541
2810
|
`runTurn completed replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} answerLen=${String(
|
|
2542
2811
|
result.text || "",
|
|
@@ -2573,6 +2842,10 @@ export class BridgeRunner {
|
|
|
2573
2842
|
});
|
|
2574
2843
|
}
|
|
2575
2844
|
await this.syncBackendSessionBinding();
|
|
2845
|
+
if (replyTo) {
|
|
2846
|
+
this.clearInterruptRetryForReplyTarget(replyTo);
|
|
2847
|
+
this.remoteInterruptsByReplyTo.delete(replyTo);
|
|
2848
|
+
}
|
|
2576
2849
|
if (replyTo) {
|
|
2577
2850
|
this.processedMessageIds.add(replyTo);
|
|
2578
2851
|
}
|
|
@@ -2591,6 +2864,7 @@ export class BridgeRunner {
|
|
|
2591
2864
|
this.copilotLog(`sdk_message sent replyTo=${replyTo || "latest"} responseLen=${responseText.length}`);
|
|
2592
2865
|
}
|
|
2593
2866
|
} catch (error) {
|
|
2867
|
+
this.activeTurnReplyTo = "";
|
|
2594
2868
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2595
2869
|
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
2596
2870
|
this.copilotLog(
|
|
@@ -2598,6 +2872,11 @@ export class BridgeRunner {
|
|
|
2598
2872
|
);
|
|
2599
2873
|
return;
|
|
2600
2874
|
}
|
|
2875
|
+
const interruptInfo = replyTo ? this.remoteInterruptsByReplyTo.get(replyTo) : null;
|
|
2876
|
+
if (interruptInfo && this.isTurnInterruptedError(error)) {
|
|
2877
|
+
await this.handleInterruptedTurn(replyTo, interruptInfo);
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2601
2880
|
if (await this.settleCodexCheckpointUnavailableAfterStream(replyTo, errorMessage)) {
|
|
2602
2881
|
return;
|
|
2603
2882
|
}
|
|
@@ -2654,7 +2933,9 @@ export class BridgeRunner {
|
|
|
2654
2933
|
}
|
|
2655
2934
|
if (replyTo) {
|
|
2656
2935
|
this.inFlightMessageIds.delete(replyTo);
|
|
2936
|
+
this.clearInterruptRetryForReplyTarget(replyTo);
|
|
2657
2937
|
}
|
|
2938
|
+
this.activeTurnReplyTo = "";
|
|
2658
2939
|
this.copilotLog(
|
|
2659
2940
|
`turn end replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} processedIds=${this.processedMessageIds.size}`,
|
|
2660
2941
|
);
|
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.39",
|
|
4
|
+
"gitCommitId": "30204c8",
|
|
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.39",
|
|
22
|
+
"@love-moon/ai-sdk": "0.2.39",
|
|
23
|
+
"@love-moon/conductor-sdk": "0.2.39",
|
|
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) ||
|
|
@@ -1570,18 +1582,34 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1570
1582
|
});
|
|
1571
1583
|
|
|
1572
1584
|
if (!AUTO_UPDATE_ENABLED && autoUpdateSupportedInstall === false) {
|
|
1573
|
-
|
|
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
|
+
}
|
|
1574
1590
|
}
|
|
1575
1591
|
|
|
1576
|
-
|
|
1592
|
+
const runMaintenanceTick = async () => {
|
|
1577
1593
|
void runDaemonWatchdog();
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1594
|
+
try {
|
|
1595
|
+
await checkForUpdate();
|
|
1596
|
+
} catch {
|
|
1597
|
+
// ignore non-critical version check failures
|
|
1598
|
+
}
|
|
1599
|
+
try {
|
|
1600
|
+
await tryAutoUpdate();
|
|
1601
|
+
} catch {
|
|
1602
|
+
// ignore non-critical auto-update failures
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
|
|
1606
|
+
watchdogTimer = setInterval(() => {
|
|
1607
|
+
void runMaintenanceTick();
|
|
1581
1608
|
}, DAEMON_WATCHDOG_INTERVAL_MS);
|
|
1582
1609
|
if (typeof watchdogTimer?.unref === "function") {
|
|
1583
1610
|
watchdogTimer.unref();
|
|
1584
1611
|
}
|
|
1612
|
+
void runMaintenanceTick();
|
|
1585
1613
|
})();
|
|
1586
1614
|
|
|
1587
1615
|
function markBackendHttpSuccess(at = Date.now()) {
|
|
@@ -3808,6 +3836,10 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3808
3836
|
return true;
|
|
3809
3837
|
}
|
|
3810
3838
|
|
|
3839
|
+
function shouldDaemonReportFireChildTerminalStatus(record) {
|
|
3840
|
+
return !Boolean(record?.managedByFireBridge);
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3811
3843
|
function handleStopTask(payload) {
|
|
3812
3844
|
const taskId = payload?.task_id;
|
|
3813
3845
|
if (!taskId) return;
|
|
@@ -4246,6 +4278,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
4246
4278
|
|
|
4247
4279
|
const env = {
|
|
4248
4280
|
...process.env,
|
|
4281
|
+
PWD: taskDir,
|
|
4249
4282
|
CONDUCTOR_PROJECT_ID: projectId,
|
|
4250
4283
|
CONDUCTOR_TASK_ID: taskId,
|
|
4251
4284
|
CONDUCTOR_LAUNCHED_BY_DAEMON: "1",
|
|
@@ -4309,6 +4342,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
4309
4342
|
projectId,
|
|
4310
4343
|
logPath,
|
|
4311
4344
|
stopForceKillTimer: null,
|
|
4345
|
+
managedByFireBridge: true,
|
|
4312
4346
|
});
|
|
4313
4347
|
|
|
4314
4348
|
client
|
|
@@ -4378,7 +4412,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
4378
4412
|
? "completed"
|
|
4379
4413
|
: `exited with code ${code}`;
|
|
4380
4414
|
|
|
4381
|
-
if (!suppressExitStatusReport) {
|
|
4415
|
+
if (!suppressExitStatusReport && shouldDaemonReportFireChildTerminalStatus(active)) {
|
|
4382
4416
|
client
|
|
4383
4417
|
.sendJson({
|
|
4384
4418
|
type: "task_status_update",
|
|
@@ -4733,6 +4767,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
4733
4767
|
projectId: normalizedProjectId,
|
|
4734
4768
|
logPath,
|
|
4735
4769
|
stopForceKillTimer: null,
|
|
4770
|
+
managedByFireBridge: true,
|
|
4736
4771
|
});
|
|
4737
4772
|
|
|
4738
4773
|
client
|
|
@@ -4795,7 +4830,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
4795
4830
|
? "completed"
|
|
4796
4831
|
: `exited with code ${code}`;
|
|
4797
4832
|
|
|
4798
|
-
if (!suppressExitStatusReport) {
|
|
4833
|
+
if (!suppressExitStatusReport && shouldDaemonReportFireChildTerminalStatus(active)) {
|
|
4799
4834
|
client
|
|
4800
4835
|
.sendJson({
|
|
4801
4836
|
type: "task_status_update",
|
|
@@ -4835,6 +4870,9 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
4835
4870
|
await Promise.allSettled(
|
|
4836
4871
|
activeEntries.map(async ([taskId, record]) => {
|
|
4837
4872
|
suppressedExitStatusReports.add(taskId);
|
|
4873
|
+
if (!shouldDaemonReportFireChildTerminalStatus(record)) {
|
|
4874
|
+
return;
|
|
4875
|
+
}
|
|
4838
4876
|
try {
|
|
4839
4877
|
await withTimeout(
|
|
4840
4878
|
client.sendJson({
|
package/src/runtime-backends.js
CHANGED
|
@@ -28,7 +28,7 @@ function appendProviderModulePaths(parts, value) {
|
|
|
28
28
|
if (!raw) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
-
for (const item of raw
|
|
31
|
+
for (const item of splitProviderModulePathString(raw)) {
|
|
32
32
|
const normalized = item.trim();
|
|
33
33
|
if (normalized) {
|
|
34
34
|
parts.push(normalized);
|
|
@@ -36,6 +36,49 @@ function appendProviderModulePaths(parts, value) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function looksLikeProviderModulePath(value) {
|
|
40
|
+
const normalized = String(value || "").trim();
|
|
41
|
+
if (!normalized) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return (
|
|
45
|
+
normalized.startsWith("/") ||
|
|
46
|
+
normalized.startsWith("./") ||
|
|
47
|
+
normalized.startsWith("../") ||
|
|
48
|
+
normalized.startsWith("~/") ||
|
|
49
|
+
normalized.startsWith("file:") ||
|
|
50
|
+
normalized.includes("/") ||
|
|
51
|
+
normalized.includes("\\") ||
|
|
52
|
+
/\.[cm]?[jt]sx?$/i.test(normalized) ||
|
|
53
|
+
/^[A-Za-z]:[\\/]/.test(normalized)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function splitProviderModulePathString(raw) {
|
|
58
|
+
const normalized = String(raw || "").trim();
|
|
59
|
+
if (!normalized) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const platformParts = normalized
|
|
64
|
+
.split(path.delimiter)
|
|
65
|
+
.map((item) => item.trim())
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
if (platformParts.length > 1 || !normalized.includes(",")) {
|
|
68
|
+
return platformParts;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const commaParts = normalized
|
|
72
|
+
.split(",")
|
|
73
|
+
.map((item) => item.trim())
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
if (commaParts.length > 1 && commaParts.every(looksLikeProviderModulePath)) {
|
|
76
|
+
return commaParts;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return platformParts;
|
|
80
|
+
}
|
|
81
|
+
|
|
39
82
|
function listProviderModulePaths(providerPathEnv) {
|
|
40
83
|
const parts = [];
|
|
41
84
|
appendProviderModulePaths(parts, providerPathEnv);
|
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
|
}
|