local-operator-ui 0.26.3 → 0.26.4
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/out/main/index.js +834 -132
- package/out/preload/index.js +8 -1
- package/out/renderer/assets/{_basePickBy-wJDm0FQM.js → _basePickBy-CMDvPx06.js} +1 -1
- package/out/renderer/assets/{_baseUniq-CcKmT_7c.js → _baseUniq-CyTFSDKr.js} +1 -1
- package/out/renderer/assets/{agent-details-page-DPNTGNIQ.js → agent-details-page-kZMf8MHK.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-DDcxflg3.js → agent-hub-page-B0e-lhRQ.js} +1 -1
- package/out/renderer/assets/{agents-page-DDavoEDg.js → agents-page-Dbfv_f5H.js} +2 -2
- package/out/renderer/assets/{arc-TtqBtXpS.js → arc-lqRc3qif.js} +1 -1
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-Sn3dwx1M.js → architectureDiagram-IEHRJDOE-C2_O8el6.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-DmWRMx-8.js → blockDiagram-JOT3LUYC-DocM5IRU.js} +1 -1
- package/out/renderer/assets/browser-page-CP5lJ9uY.js +1 -0
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-kk1xWrtQ.js → c4Diagram-VJAJSXHY-DQWGIBdr.js} +1 -1
- package/out/renderer/assets/channel-Dr9HGzNK.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-C0l8JKva.js → chunk-4BMEZGHF-BxmzjOZh.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-BcX6OXd5.js → chunk-A2AXSNBT-NpShlg8c.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-Ea-04ABD.js → chunk-AEK57VVT-B0TZSkz7.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-CgZKUTL7.js → chunk-D6G4REZN-CXRQFY6k.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-CaYdXK61.js → chunk-RZ5BOZE2-Cmu9Ik6u.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-eINKeCA_.js → chunk-XZIHB7SX-BT3Mo2NV.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-CTprNpRM.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-CTprNpRM.js +1 -0
- package/out/renderer/assets/clone-DDcwKS67.js +1 -0
- package/out/renderer/assets/{compact-pagination-BGC_ltPs.js → compact-pagination-D01Rvfur.js} +1 -1
- package/out/renderer/assets/{dagre-OKDRZEBW-CRdErrN5.js → dagre-OKDRZEBW-UQnLr4NW.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-D1AJyKR9.js → diagram-SSKATNLV-3w1dMlg3.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-BjgCteQB.js → diagram-VNBRO52H-CCZaWhVp.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-11QyodNt.js → erDiagram-Q7BY3M3F-uZMQmX7_.js} +1 -1
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-ou_Lc3fB.js → flowDiagram-4HSFHLVR-BKourybF.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-CcVshyxS.js → ganttDiagram-APWFNJXF-BhYXEmQA.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-C0rtso3u.js → gitGraphDiagram-7IBYFJ6S-D_m6vlq6.js} +1 -1
- package/out/renderer/assets/{graph-Bc6JXDog.js → graph-Dq_3CFha.js} +1 -1
- package/out/renderer/assets/icon-0tEvAhQU.css +1 -0
- package/out/renderer/assets/{index-BhElRiu7.js → index-B5_r62an.js} +1 -1
- package/out/renderer/assets/{index-Bo2Wge3p.js → index-BOpDgAjv.js} +379 -372
- package/out/renderer/assets/{index-Cb6T4AFi.js → index-CNyA8Ppl.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-BYkNE3tJ.js → infoDiagram-PH2N3AL5-BaWnC0fK.js} +1 -1
- package/out/renderer/assets/{installer-DDldqUI5.js → installer-EGSglW9O.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-CUIyoA3C.js → journeyDiagram-U35MCT3I-CExc8Lvh.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-iPrfQ2oJ.js → kanban-definition-NDS4AKOZ-k2dTO3gs.js} +1 -1
- package/out/renderer/assets/{layout-CbD9heAw.js → layout-DsQh3awQ.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-D4ch8fHv.js → legacy-agents-page-6eRuLUCJ.js} +1 -1
- package/out/renderer/assets/{mermaid.core-B2WQhMve.js → mermaid.core-Cc73RCEu.js} +5 -5
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-B-zVnzUG.js → mindmap-definition-ALO5MXBD-CY1b7QnZ.js} +1 -1
- package/out/renderer/assets/{page-header-un3csrkZ.js → page-header-Cg5Oasi6.js} +1 -1
- package/out/renderer/assets/{parseISO-BFHy1myG.js → parseISO-xitnFuBA.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-D2Kfrawf.js → pieDiagram-IB7DONF6-CnJiQspi.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-KBSPvm-o.js → quadrantDiagram-7GDLP6J5-DlkqVGNi.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-CD0L2WL4.js → radar-MK3ICKWK-Dei_MUJO.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-CLf4igff.js → radient-auth-buttons-DyHyMHQM.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-C1-0khAP.js → requirementDiagram-KVF5MWMF-Dqo5gkes.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-DsGLUITt.js → sankeyDiagram-QLVOVGJD-By-NBmQd.js} +1 -1
- package/out/renderer/assets/schedules-page-CkK-8qjn.js +120 -0
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-Du4VNaA2.js → sequenceDiagram-X6HHIX6F-WYj_m0ij.js} +1 -1
- package/out/renderer/assets/{settings-page-Cy6oljBR.js → settings-page-ONfhyYmU.js} +1 -1
- package/out/renderer/assets/{stateDiagram-DGXRK772-DbUfrePQ.js → stateDiagram-DGXRK772-1_7lAHAU.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-DXjoneIR.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-BKMUc_3-.js → timeline-definition-BDJGKUSR-B7q4bXZF.js} +1 -1
- package/out/renderer/assets/{use-agent-like-mutation-CiYF_2uI.js → use-agent-like-mutation-ZT3Ct6fx.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-DFG0Vjf7.js → xychartDiagram-VJFVF3MP-TTQCkrx5.js} +1 -1
- package/out/renderer/index.html +3 -3
- package/out/renderer/installer.html +3 -3
- package/package.json +2 -2
- package/out/renderer/assets/browser-page-DgVk9bJD.js +0 -1
- package/out/renderer/assets/channel-n5Rajrsc.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-CoTV4VPG.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-CoTV4VPG.js +0 -1
- package/out/renderer/assets/clone-DE7Ya7xI.js +0 -1
- package/out/renderer/assets/icon-B8Li1YYS.css +0 -1
- package/out/renderer/assets/schedules-page-wm_dIpC7.js +0 -115
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-MuPTTa0H.js +0 -1
- /package/out/renderer/assets/{icon-Bny6j4NZ.js → icon--RqZR_iT.js} +0 -0
package/out/main/index.js
CHANGED
|
@@ -99,6 +99,9 @@ const variableKey = zod.z.string().min(1).max(128).refine(
|
|
|
99
99
|
).refine(isAddressableVariableKey);
|
|
100
100
|
const variableType = zod.z.enum(["str", "int", "float", "bool", "list", "dict"]);
|
|
101
101
|
const variableValue = zod.z.string().max(16384);
|
|
102
|
+
const WAKE_MESSAGE_MAX_CHARS = 2e3;
|
|
103
|
+
const wakeMessage = zod.z.string().min(1).max(WAKE_MESSAGE_MAX_CHARS);
|
|
104
|
+
const wakeId = zod.z.string().min(1).max(64);
|
|
102
105
|
const scheduleWrite = zod.z.object({
|
|
103
106
|
is_active: zod.z.boolean().nullish(),
|
|
104
107
|
one_time: zod.z.boolean().nullish(),
|
|
@@ -620,6 +623,91 @@ const desktopRequestSchema = zod.z.discriminatedUnion("op", [
|
|
|
620
623
|
sessionId,
|
|
621
624
|
asideId: requestId
|
|
622
625
|
}).strict(),
|
|
626
|
+
/*
|
|
627
|
+
* The wake surface: the Schedules page's machine-wide read and its writes.
|
|
628
|
+
*
|
|
629
|
+
* Deliberately NOT an extension of `sessions.*`. The session list is
|
|
630
|
+
* paginated by recency (`limit ≤ 500`), so a session armed once and never
|
|
631
|
+
* opened falls off it - and the page whose whole job is "every session that
|
|
632
|
+
* has wakes" would then list fewer sessions than exist. `wakes.list` reads
|
|
633
|
+
* the wake index instead, which is one small JSON file per wake-carrying
|
|
634
|
+
* session and is complete at any store size.
|
|
635
|
+
*
|
|
636
|
+
* The four ops sit behind the same router-level desktop bearer as their
|
|
637
|
+
* `sessions.*` siblings, and every one of them does its filesystem work off
|
|
638
|
+
* the event loop, so a listing over a cold store cannot stall the desktop
|
|
639
|
+
* plane's other readers.
|
|
640
|
+
*/
|
|
641
|
+
zod.z.object({
|
|
642
|
+
op: zod.z.literal("wakes.list"),
|
|
643
|
+
/* Bounded like `sessions.list`: the field exists so a pathological
|
|
644
|
+
store degrades visibly (through `truncated`) rather than silently. */
|
|
645
|
+
limit: zod.z.number().int().min(1).max(500).optional(),
|
|
646
|
+
/* Dormant rows are the ones whose session was stopped, which the page
|
|
647
|
+
SHOWS ("parked") rather than hides: stopping a conversation parks its
|
|
648
|
+
wakes, and a management surface that dropped them would report a
|
|
649
|
+
scheduled task as gone when it is only parked. */
|
|
650
|
+
includeDormant: zod.z.boolean().optional()
|
|
651
|
+
}).strict(),
|
|
652
|
+
zod.z.object({
|
|
653
|
+
op: zod.z.literal("wakes.create"),
|
|
654
|
+
requestId,
|
|
655
|
+
/*
|
|
656
|
+
* The session half of the request is EXACTLY ONE of two shapes: name an
|
|
657
|
+
* existing `sessionId`, or name a `cwd` (plus an optional `target`) and
|
|
658
|
+
* have the backend create the conversation and arm the wake in one
|
|
659
|
+
* call. The exclusivity cannot be stated with a `.refine()` on this
|
|
660
|
+
* object - that turns the member into a `ZodEffects`, which a
|
|
661
|
+
* discriminated union cannot take (see `mcp.credentials.store`, which
|
|
662
|
+
* records the same trap) - so the API client enforces it on the way out
|
|
663
|
+
* with `wakeCreateBody`, and the backend refuses the malformed shape
|
|
664
|
+
* with a 422 rather than a 500.
|
|
665
|
+
*/
|
|
666
|
+
sessionId: sessionId.optional(),
|
|
667
|
+
cwd: zod.z.string().min(1).max(4096).optional(),
|
|
668
|
+
target: target.optional(),
|
|
669
|
+
message: wakeMessage,
|
|
670
|
+
/*
|
|
671
|
+
* Timing, in the same spellings the wake tool and `lop wake create`
|
|
672
|
+
* take, parsed by the backend's own `parse_wake_duration` /
|
|
673
|
+
* `parse_wake_at`: a relative duration (`30m`), an ISO instant or the
|
|
674
|
+
* next `HH:MM`. The dialog's presets are `in`/`at` pairs so the common
|
|
675
|
+
* path never touches a calendar, and `Pick a time…` sends the ISO
|
|
676
|
+
* instant the picker yields.
|
|
677
|
+
*/
|
|
678
|
+
in: zod.z.string().min(1).max(64).optional(),
|
|
679
|
+
at: zod.z.string().min(1).max(64).optional(),
|
|
680
|
+
every: zod.z.string().min(1).max(64).optional(),
|
|
681
|
+
until: zod.z.string().min(1).max(64).optional(),
|
|
682
|
+
limit: zod.z.number().int().min(1).optional(),
|
|
683
|
+
/*
|
|
684
|
+
* The conversation's own title, when a caller wants to override the one
|
|
685
|
+
* derived from the prompt.
|
|
686
|
+
*
|
|
687
|
+
* Carried because the interface declares it, and never set by this
|
|
688
|
+
* page: the prompt IS the name here (the backend writes it into the
|
|
689
|
+
* session's stored-title sidecar, which `resume.session_name` consults
|
|
690
|
+
* first), which is what keeps the create flow free of a "name" field
|
|
691
|
+
* nobody would fill in.
|
|
692
|
+
*/
|
|
693
|
+
title: zod.z.string().min(1).max(200).optional()
|
|
694
|
+
}).strict(),
|
|
695
|
+
zod.z.object({
|
|
696
|
+
op: zod.z.literal("wakes.edit"),
|
|
697
|
+
sessionId,
|
|
698
|
+
wakeId,
|
|
699
|
+
/* Every field optional, and the body is sent with the absent ones
|
|
700
|
+
OMITTED rather than nulled: the backend applies the update against
|
|
701
|
+
the schedule it holds, so a `null` here would be a request to clear
|
|
702
|
+
a bound the user did not touch. */
|
|
703
|
+
message: wakeMessage.optional(),
|
|
704
|
+
in: zod.z.string().min(1).max(64).optional(),
|
|
705
|
+
at: zod.z.string().min(1).max(64).optional(),
|
|
706
|
+
every: zod.z.string().min(1).max(64).optional(),
|
|
707
|
+
until: zod.z.string().min(1).max(64).optional(),
|
|
708
|
+
limit: zod.z.number().int().min(1).optional()
|
|
709
|
+
}).strict(),
|
|
710
|
+
zod.z.object({ op: zod.z.literal("wakes.remove"), sessionId, wakeId }).strict(),
|
|
623
711
|
zod.z.object({ op: zod.z.literal("mcp.list"), sessionId }).strict(),
|
|
624
712
|
zod.z.object({
|
|
625
713
|
op: zod.z.literal("mcp.credentials.store"),
|
|
@@ -849,6 +937,22 @@ function desktopRequestTooLargeDetail(op) {
|
|
|
849
937
|
if (MESSAGE_OPS.has(op)) return DESKTOP_REQUEST_TOO_LARGE_DETAIL;
|
|
850
938
|
return "This request is too large to send. Shorten the text in this form.";
|
|
851
939
|
}
|
|
940
|
+
function wakeSessionHalf(request) {
|
|
941
|
+
if (request.sessionId) return { session_id: request.sessionId };
|
|
942
|
+
return {
|
|
943
|
+
...request.cwd ? { cwd: request.cwd } : {},
|
|
944
|
+
...request.target ? { target: request.target } : {}
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
function wakeTiming(request) {
|
|
948
|
+
return {
|
|
949
|
+
...request.in !== void 0 ? { in: request.in } : {},
|
|
950
|
+
...request.at !== void 0 ? { at: request.at } : {},
|
|
951
|
+
...request.every !== void 0 ? { every: request.every } : {},
|
|
952
|
+
...request.until !== void 0 ? { until: request.until } : {},
|
|
953
|
+
...request.limit !== void 0 ? { limit: request.limit } : {}
|
|
954
|
+
};
|
|
955
|
+
}
|
|
852
956
|
function desktopEndpoint(request) {
|
|
853
957
|
switch (request.op) {
|
|
854
958
|
case "capabilities":
|
|
@@ -1137,6 +1241,47 @@ function desktopEndpoint(request) {
|
|
|
1137
1241
|
method: "GET"
|
|
1138
1242
|
};
|
|
1139
1243
|
}
|
|
1244
|
+
/*
|
|
1245
|
+
* The wake routes, mapped beside their `sessions.*` siblings: one op per
|
|
1246
|
+
* method, snake_case on the wire (`include_dormant`, `request_id`), which is
|
|
1247
|
+
* what the backend's models declare.
|
|
1248
|
+
*/
|
|
1249
|
+
case "wakes.list": {
|
|
1250
|
+
const query = new URLSearchParams();
|
|
1251
|
+
if (request.limit !== void 0)
|
|
1252
|
+
query.set("limit", String(request.limit));
|
|
1253
|
+
if (request.includeDormant !== void 0)
|
|
1254
|
+
query.set("include_dormant", String(request.includeDormant));
|
|
1255
|
+
return {
|
|
1256
|
+
path: query.size > 0 ? `/v1/desktop/wakes?${query}` : "/v1/desktop/wakes",
|
|
1257
|
+
method: "GET"
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
case "wakes.create":
|
|
1261
|
+
return {
|
|
1262
|
+
path: "/v1/desktop/wakes",
|
|
1263
|
+
method: "POST",
|
|
1264
|
+
body: {
|
|
1265
|
+
request_id: request.requestId,
|
|
1266
|
+
...wakeSessionHalf(request),
|
|
1267
|
+
message: request.message,
|
|
1268
|
+
...wakeTiming(request)
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
case "wakes.edit":
|
|
1272
|
+
return {
|
|
1273
|
+
path: `/v1/desktop/wakes/${request.sessionId}/${request.wakeId}`,
|
|
1274
|
+
method: "PATCH",
|
|
1275
|
+
body: {
|
|
1276
|
+
...request.message !== void 0 ? { message: request.message } : {},
|
|
1277
|
+
...wakeTiming(request)
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
case "wakes.remove":
|
|
1281
|
+
return {
|
|
1282
|
+
path: `/v1/desktop/wakes/${request.sessionId}/${request.wakeId}`,
|
|
1283
|
+
method: "DELETE"
|
|
1284
|
+
};
|
|
1140
1285
|
case "legacy.jobs.list": {
|
|
1141
1286
|
const query = new URLSearchParams();
|
|
1142
1287
|
if (request.agentId) query.set("agent_id", request.agentId);
|
|
@@ -2735,6 +2880,49 @@ function clearPendingInstallMarker(dir) {
|
|
|
2735
2880
|
fs.rmSync(path2, { force: true });
|
|
2736
2881
|
return true;
|
|
2737
2882
|
}
|
|
2883
|
+
const PENDING_SERVER_UPDATE_MARKER_FILE = "pending-server-update.json";
|
|
2884
|
+
function pendingServerUpdateMarkerPath(dir) {
|
|
2885
|
+
return path$1.join(dir, PENDING_SERVER_UPDATE_MARKER_FILE);
|
|
2886
|
+
}
|
|
2887
|
+
function writePendingServerUpdateMarker(dir, marker) {
|
|
2888
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2889
|
+
const target2 = pendingServerUpdateMarkerPath(dir);
|
|
2890
|
+
const temp = `${target2}.tmp`;
|
|
2891
|
+
fs.writeFileSync(temp, `${JSON.stringify(marker, null, 2)}
|
|
2892
|
+
`, "utf8");
|
|
2893
|
+
fs.renameSync(temp, target2);
|
|
2894
|
+
return marker;
|
|
2895
|
+
}
|
|
2896
|
+
function parsePendingServerUpdateMarker(raw) {
|
|
2897
|
+
try {
|
|
2898
|
+
const parsed = JSON.parse(raw);
|
|
2899
|
+
if (typeof parsed?.startedAt !== "string" || parsed.startedAt.length === 0) {
|
|
2900
|
+
return null;
|
|
2901
|
+
}
|
|
2902
|
+
return {
|
|
2903
|
+
before: typeof parsed.before === "string" ? parsed.before : null,
|
|
2904
|
+
target: typeof parsed.target === "string" ? parsed.target : null,
|
|
2905
|
+
startedAt: parsed.startedAt
|
|
2906
|
+
};
|
|
2907
|
+
} catch {
|
|
2908
|
+
return null;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
function readPendingServerUpdateMarker(dir) {
|
|
2912
|
+
const path2 = pendingServerUpdateMarkerPath(dir);
|
|
2913
|
+
if (!fs.existsSync(path2)) return null;
|
|
2914
|
+
try {
|
|
2915
|
+
return parsePendingServerUpdateMarker(fs.readFileSync(path2, "utf8"));
|
|
2916
|
+
} catch {
|
|
2917
|
+
return null;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
function clearPendingServerUpdateMarker(dir) {
|
|
2921
|
+
const path2 = pendingServerUpdateMarkerPath(dir);
|
|
2922
|
+
if (!fs.existsSync(path2)) return false;
|
|
2923
|
+
fs.rmSync(path2, { force: true });
|
|
2924
|
+
return true;
|
|
2925
|
+
}
|
|
2738
2926
|
const LAST_INSTALL_ATTEMPT_FILE = "last-update-install.json";
|
|
2739
2927
|
function lastInstallAttemptPath(dir) {
|
|
2740
2928
|
return path$1.join(dir, LAST_INSTALL_ATTEMPT_FILE);
|
|
@@ -3314,6 +3502,33 @@ function pipxListNamesLocalOperator(output) {
|
|
|
3314
3502
|
}
|
|
3315
3503
|
const PYTHON_LIB_DIR = /^python\d/;
|
|
3316
3504
|
const LOCAL_OPERATOR_DIST_INFO = /^local[-_]operator[-_]/;
|
|
3505
|
+
const PYPI_SOURCE_TOKEN = "pypi";
|
|
3506
|
+
const SOURCE_MARKER_SEPARATOR = /\s+/;
|
|
3507
|
+
function isSourceBuildRef(token) {
|
|
3508
|
+
if (typeof token !== "string") return false;
|
|
3509
|
+
const trimmed = token.trim();
|
|
3510
|
+
if (trimmed.length < 7 || trimmed.length > 40) return false;
|
|
3511
|
+
for (const char of trimmed) {
|
|
3512
|
+
if (!"0123456789abcdefABCDEF".includes(char)) return false;
|
|
3513
|
+
}
|
|
3514
|
+
return trimmed.toLowerCase() !== PYPI_SOURCE_TOKEN;
|
|
3515
|
+
}
|
|
3516
|
+
function readSourceRef(prefix) {
|
|
3517
|
+
try {
|
|
3518
|
+
const raw = fs.readFileSync(path$1.join(prefix, ".lop-source"), "utf8");
|
|
3519
|
+
const token = raw.split(SOURCE_MARKER_SEPARATOR).filter(Boolean)[0];
|
|
3520
|
+
return token && token.length > 0 ? token : null;
|
|
3521
|
+
} catch {
|
|
3522
|
+
return null;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
const LOCAL_OPERATOR_DIST_INFO_VERSION = /^local[-_]operator[-_](.+)\.dist-info$/;
|
|
3526
|
+
function newerDistInfoVersion(candidate, current) {
|
|
3527
|
+
if (current === null) return true;
|
|
3528
|
+
const order = compareVersions$1(candidate, current);
|
|
3529
|
+
if (order !== null) return order > 0;
|
|
3530
|
+
return compareVersions$1(current, candidate) === null && candidate > current;
|
|
3531
|
+
}
|
|
3317
3532
|
function resolveDistributionMarkers(prefix) {
|
|
3318
3533
|
const sitePackages = [];
|
|
3319
3534
|
try {
|
|
@@ -3328,6 +3543,7 @@ function resolveDistributionMarkers(prefix) {
|
|
|
3328
3543
|
if (fs.existsSync(windowsSitePackages)) sitePackages.push(windowsSitePackages);
|
|
3329
3544
|
let installer = null;
|
|
3330
3545
|
let editable = false;
|
|
3546
|
+
let version = null;
|
|
3331
3547
|
for (const dir of sitePackages) {
|
|
3332
3548
|
let entries;
|
|
3333
3549
|
try {
|
|
@@ -3339,6 +3555,8 @@ function resolveDistributionMarkers(prefix) {
|
|
|
3339
3555
|
if (!LOCAL_OPERATOR_DIST_INFO.test(entry)) continue;
|
|
3340
3556
|
if (!entry.endsWith(".dist-info")) continue;
|
|
3341
3557
|
const distInfo = path$1.join(dir, entry);
|
|
3558
|
+
const parsed = LOCAL_OPERATOR_DIST_INFO_VERSION.exec(entry)?.[1]?.trim();
|
|
3559
|
+
if (parsed && newerDistInfoVersion(parsed, version)) version = parsed;
|
|
3342
3560
|
try {
|
|
3343
3561
|
const installerText = fs.readFileSync(path$1.join(distInfo, "INSTALLER"), "utf8");
|
|
3344
3562
|
installer = installerText.trim().toLowerCase() || installer;
|
|
@@ -3355,7 +3573,7 @@ function resolveDistributionMarkers(prefix) {
|
|
|
3355
3573
|
}
|
|
3356
3574
|
}
|
|
3357
3575
|
}
|
|
3358
|
-
return { installer, editable };
|
|
3576
|
+
return { installer, editable, version, sourceRef: readSourceRef(prefix) };
|
|
3359
3577
|
}
|
|
3360
3578
|
const TRAILING_SLASHES = /\/+$/;
|
|
3361
3579
|
function commandSearchDirs(env, home) {
|
|
@@ -3377,6 +3595,14 @@ function commandSearchDirs(env, home) {
|
|
|
3377
3595
|
add("/bin");
|
|
3378
3596
|
return dirs;
|
|
3379
3597
|
}
|
|
3598
|
+
function installerSearchPath(env = process.env, home = os.homedir()) {
|
|
3599
|
+
const inherited = (env.PATH ?? "").split(path$1.delimiter).map((dir) => dir.replace(TRAILING_SLASHES, "")).filter((dir) => dir.length > 0);
|
|
3600
|
+
const inheritedSet = new Set(inherited);
|
|
3601
|
+
const ours = commandSearchDirs(env, home).filter(
|
|
3602
|
+
(dir) => !inheritedSet.has(dir)
|
|
3603
|
+
);
|
|
3604
|
+
return [...ours, ...inherited].join(path$1.delimiter);
|
|
3605
|
+
}
|
|
3380
3606
|
function uvToolBinDirs(env, home, listDir, platform) {
|
|
3381
3607
|
const root = env.UV_TOOL_DIR ?? (env.XDG_DATA_HOME ? path$1.join(env.XDG_DATA_HOME, "uv", "tools") : path$1.join(home, ".local", "share", "uv", "tools"));
|
|
3382
3608
|
const scriptDir = platform === "win32" ? "Scripts" : "bin";
|
|
@@ -3435,6 +3661,23 @@ function resolveCommandPath(name, options = {}) {
|
|
|
3435
3661
|
}
|
|
3436
3662
|
return null;
|
|
3437
3663
|
}
|
|
3664
|
+
function resolveGlobalConsoleScript(options = {}) {
|
|
3665
|
+
if ((options.platform ?? process.platform) === "win32") {
|
|
3666
|
+
const first = (name) => {
|
|
3667
|
+
try {
|
|
3668
|
+
const resolved = node_child_process.execFileSync("where", [name], {
|
|
3669
|
+
encoding: "utf8",
|
|
3670
|
+
timeout: 5e3
|
|
3671
|
+
}).split("\n")[0]?.trim();
|
|
3672
|
+
return resolved && resolved.length > 0 ? resolved : null;
|
|
3673
|
+
} catch {
|
|
3674
|
+
return null;
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3677
|
+
return first("local-operator") ?? first("lop");
|
|
3678
|
+
}
|
|
3679
|
+
return resolveCommandPath("local-operator", options) ?? resolveCommandPath("lop", options);
|
|
3680
|
+
}
|
|
3438
3681
|
function readInstallIdentity(shimPath) {
|
|
3439
3682
|
if (!shimPath) return { path: null };
|
|
3440
3683
|
let realPath = null;
|
|
@@ -3461,9 +3704,30 @@ function readInstallIdentity(shimPath) {
|
|
|
3461
3704
|
uvReceipt: existing(path$1.join(prefix, "uv-receipt.toml")),
|
|
3462
3705
|
venvPrefix: fs.existsSync(path$1.join(prefix, "pyvenv.cfg")) ? prefix : null,
|
|
3463
3706
|
installer: markers.installer,
|
|
3464
|
-
editable: markers.editable
|
|
3707
|
+
editable: markers.editable,
|
|
3708
|
+
version: markers.version,
|
|
3709
|
+
sourceRef: markers.sourceRef
|
|
3465
3710
|
};
|
|
3466
3711
|
}
|
|
3712
|
+
function generationInstallRoot(identity) {
|
|
3713
|
+
const resolved = identity.realPath ?? identity.path;
|
|
3714
|
+
if (!resolved) return null;
|
|
3715
|
+
let executable;
|
|
3716
|
+
try {
|
|
3717
|
+
executable = fs.realpathSync(resolved);
|
|
3718
|
+
} catch {
|
|
3719
|
+
return null;
|
|
3720
|
+
}
|
|
3721
|
+
let dir = executable;
|
|
3722
|
+
for (; ; ) {
|
|
3723
|
+
const parent = path$1.dirname(dir);
|
|
3724
|
+
if (parent === dir) return null;
|
|
3725
|
+
if (path$1.basename(parent) === "generations") {
|
|
3726
|
+
return fs.existsSync(path$1.join(path$1.dirname(parent), "current")) ? dir : null;
|
|
3727
|
+
}
|
|
3728
|
+
dir = parent;
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3467
3731
|
function classifyGlobalInstall(identity) {
|
|
3468
3732
|
if (!identity.path) return "global-unknown";
|
|
3469
3733
|
if (identity.editable) return "editable";
|
|
@@ -3484,8 +3748,8 @@ function classifyGlobalInstall(identity) {
|
|
|
3484
3748
|
}
|
|
3485
3749
|
function resolveGlobalInstallPlan(input) {
|
|
3486
3750
|
const kind = classifyGlobalInstall(input.identity);
|
|
3487
|
-
const sourceBuild = kind === "editable" || kind === "uv-tool" &&
|
|
3488
|
-
const detail = input.identity.path ? `local-operator resolves to ${input.identity.path}${input.identity.realPath && input.identity.realPath !== input.identity.path ? ` (${input.identity.realPath})` : ""}, classified as ${kind}
|
|
3751
|
+
const sourceBuild = kind === "editable" || kind === "uv-tool" && isSourceBuildRef(input.identity.sourceRef);
|
|
3752
|
+
const detail = input.identity.path ? `local-operator resolves to ${input.identity.path}${input.identity.realPath && input.identity.realPath !== input.identity.path ? ` (${input.identity.realPath})` : ""}, classified as ${kind}.${kind === "editable" ? " Built from source on this machine, so it follows the checkout rather than the published release." : ""}` : "local-operator was not found on PATH.";
|
|
3489
3753
|
if (kind === "editable") {
|
|
3490
3754
|
return {
|
|
3491
3755
|
canManageUpdate: false,
|
|
@@ -3496,12 +3760,33 @@ function resolveGlobalInstallPlan(input) {
|
|
|
3496
3760
|
};
|
|
3497
3761
|
}
|
|
3498
3762
|
if (kind === "uv-tool") {
|
|
3499
|
-
const
|
|
3763
|
+
const managed = generationInstallRoot(input.identity) !== null;
|
|
3764
|
+
const provenance = sourceBuild ? " Built from source on this machine, so `lop update` installs the published release over it - the harness prints the same notice." : "";
|
|
3500
3765
|
return {
|
|
3501
|
-
canManageUpdate:
|
|
3502
|
-
updateCommand:
|
|
3503
|
-
|
|
3504
|
-
|
|
3766
|
+
canManageUpdate: managed,
|
|
3767
|
+
updateCommand: "lop update",
|
|
3768
|
+
/*
|
|
3769
|
+
* The remedy is the sentence above the command well in the by-hand panel
|
|
3770
|
+
* and the consequence line in the managed offer, and each arm says the
|
|
3771
|
+
* thing that decides the reader's next move:
|
|
3772
|
+
*
|
|
3773
|
+
* Managed - what the app is about to do TO THEM. This arm used to tell the
|
|
3774
|
+
* user to go to a terminal on the one path where the app runs the command
|
|
3775
|
+
* itself, which no surface rendered (reviews D7, U7), and the offer named
|
|
3776
|
+
* no cost at all before the click (review U3). The restart is the one
|
|
3777
|
+
* destructive thing this path does, so it is stated here rather than in
|
|
3778
|
+
* the in-flight panel the user reaches only after pressing.
|
|
3779
|
+
*
|
|
3780
|
+
* Legacy - WHY the app will not press the button, which is what the user
|
|
3781
|
+
* is choosing between (reviews U8, N1). It lived only in the mono Details
|
|
3782
|
+
* line, trailing a resolved path and a classification.
|
|
3783
|
+
*
|
|
3784
|
+
* NEITHER ENDS IN A COLON: the well below is visually distinct in both
|
|
3785
|
+
* panels, and in the by-hand panel the next line was the version sentence,
|
|
3786
|
+
* so the promise landed on the wrong line (review D5).
|
|
3787
|
+
*/
|
|
3788
|
+
remedy: managed ? "The app updates this install and then restarts the server it started, so a turn that is in flight is dropped while the server comes back. This can take a minute or two." : "This install predates the non-disruptive installer, so update it once from your terminal. This install's updater rewrites the shared environment in place, which can interrupt sessions mid-turn; the app manages updates after that.",
|
|
3789
|
+
detail: `${detail}${provenance}`,
|
|
3505
3790
|
sourceBuild
|
|
3506
3791
|
};
|
|
3507
3792
|
}
|
|
@@ -3509,7 +3794,7 @@ function resolveGlobalInstallPlan(input) {
|
|
|
3509
3794
|
return {
|
|
3510
3795
|
canManageUpdate: false,
|
|
3511
3796
|
updateCommand: "pipx upgrade local-operator",
|
|
3512
|
-
remedy: "The server is a pipx install, so update it from your terminal
|
|
3797
|
+
remedy: "The server is a pipx install, so update it from your terminal",
|
|
3513
3798
|
detail,
|
|
3514
3799
|
sourceBuild: false
|
|
3515
3800
|
};
|
|
@@ -3518,7 +3803,7 @@ function resolveGlobalInstallPlan(input) {
|
|
|
3518
3803
|
return {
|
|
3519
3804
|
canManageUpdate: false,
|
|
3520
3805
|
updateCommand: "pip install --upgrade local-operator",
|
|
3521
|
-
remedy: "The server is a pip install, so update it from your terminal
|
|
3806
|
+
remedy: "The server is a pip install, so update it from your terminal",
|
|
3522
3807
|
detail,
|
|
3523
3808
|
sourceBuild: false
|
|
3524
3809
|
};
|
|
@@ -3526,7 +3811,7 @@ function resolveGlobalInstallPlan(input) {
|
|
|
3526
3811
|
return {
|
|
3527
3812
|
canManageUpdate: false,
|
|
3528
3813
|
updateCommand: "",
|
|
3529
|
-
remedy: "The app could not tell how this server was installed, so update it with the tool you installed it with - uv, pipx or pip
|
|
3814
|
+
remedy: "The app could not tell how this server was installed, so update it with the tool you installed it with - uv, pipx or pip",
|
|
3530
3815
|
detail,
|
|
3531
3816
|
sourceBuild: false
|
|
3532
3817
|
};
|
|
@@ -3563,9 +3848,11 @@ function parsePipShowVersion(stdout) {
|
|
|
3563
3848
|
function didUpgradeLand(input) {
|
|
3564
3849
|
const { before, after, target: target2 } = input;
|
|
3565
3850
|
if (after == null) return false;
|
|
3566
|
-
if (
|
|
3567
|
-
|
|
3851
|
+
if (target2 != null) {
|
|
3852
|
+
const order = compareVersions$1(after, target2);
|
|
3853
|
+
return order !== null && order >= 0;
|
|
3568
3854
|
}
|
|
3855
|
+
if (before == null) return false;
|
|
3569
3856
|
return before.trim() !== after.trim();
|
|
3570
3857
|
}
|
|
3571
3858
|
const launchEnv = {
|
|
@@ -5491,8 +5778,6 @@ var LocalOperatorStartupMode = /* @__PURE__ */ ((LocalOperatorStartupMode2) => {
|
|
|
5491
5778
|
LocalOperatorStartupMode2["NOT_STARTED"] = "NOT_STARTED";
|
|
5492
5779
|
return LocalOperatorStartupMode2;
|
|
5493
5780
|
})(LocalOperatorStartupMode || {});
|
|
5494
|
-
const CONSOLE_DISCOVERY_TIMEOUT_MS = 5e3;
|
|
5495
|
-
const CONSOLE_RESOLUTION_WORST_MS = 2 * CONSOLE_DISCOVERY_TIMEOUT_MS;
|
|
5496
5781
|
const SHUTDOWN_TIMEOUT_DEFAULTS = {
|
|
5497
5782
|
restart: 1e4,
|
|
5498
5783
|
// 10 seconds for restart operations
|
|
@@ -5506,7 +5791,6 @@ const READINESS_POLL_INTERVAL_MS = 1e3;
|
|
|
5506
5791
|
const DESKTOP_TOKEN_FILENAME = "desktop-token";
|
|
5507
5792
|
const execPromise = node_util.promisify(node_child_process.exec);
|
|
5508
5793
|
const ENV_VAR_REGEX = /^([^=]+)=(.*)$/;
|
|
5509
|
-
const LINE_BREAK = /\r?\n/;
|
|
5510
5794
|
function classifyDesktopAnswer(status2) {
|
|
5511
5795
|
if (status2 >= 200 && status2 < 300) return "accepted";
|
|
5512
5796
|
if (status2 === 401 || status2 === 403) return "refused";
|
|
@@ -6191,59 +6475,75 @@ class BackendServiceManager {
|
|
|
6191
6475
|
}
|
|
6192
6476
|
this.notifyStatus();
|
|
6193
6477
|
}
|
|
6478
|
+
/**
|
|
6479
|
+
* The global launcher this app ranks its daemons by, spawns from, and reports.
|
|
6480
|
+
*
|
|
6481
|
+
* ONE resolution, read by `checkLocalOperatorExists`, `resolveGlobalConsole`
|
|
6482
|
+
* and `preferredInstallPrefix`, because the three have to name the same
|
|
6483
|
+
* install: the decision to run GLOBAL_INSTALL is only true if the spawn can
|
|
6484
|
+
* resolve that same launcher, and a second resolution through a different
|
|
6485
|
+
* mechanism is exactly how a shell rc file's prepended venv becomes the thing
|
|
6486
|
+
* that gets spawned while the ranking talked about a different one.
|
|
6487
|
+
*
|
|
6488
|
+
* `resolveCommandPath`, NOT `which`. WHY, measured on this host 2026-09-16: the
|
|
6489
|
+
* app is started by LaunchServices, whose PATH is `/usr/bin:/bin:/usr/sbin:/sbin`,
|
|
6490
|
+
* and an app launched from a session that carries its own minimal PATH inherits
|
|
6491
|
+
* that. `~/.local/bin` - where uv and pipx link their console scripts, and where
|
|
6492
|
+
* this machine's `lop-update` install lives - is on neither. So the shell probe
|
|
6493
|
+
* this replaced answered "not found globally" on every ordinary launch, and the
|
|
6494
|
+
* app fell through to its OWN bundled environment: it ran a backend the
|
|
6495
|
+
* operator never updates, watched it diverge from `lop --version` (0.55.9
|
|
6496
|
+
* bundled against 0.55.10 installed), and offered to `pip install` into a uv
|
|
6497
|
+
* tool. `resolveCommandPath` searches the installers' own locations
|
|
6498
|
+
* (`UV_TOOL_BIN_DIR`, `XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, `~/.local/bin`,
|
|
6499
|
+
* the Homebrew prefixes and every uv tool environment) as well as the inherited
|
|
6500
|
+
* PATH, so the same install is named with or without a shell - which is the
|
|
6501
|
+
* ranking rule `preferredInstallPrefix` states, and precisely what a
|
|
6502
|
+
* `which`-based check could not honour.
|
|
6503
|
+
*
|
|
6504
|
+
* `local-operator` before `lop`: both are console scripts of the same install
|
|
6505
|
+
* and normally sit in the same bin directory, but the spawn reads the resolved
|
|
6506
|
+
* script's shebang and refuses anything that is not a
|
|
6507
|
+
* `from local_operator.cli import main` launcher (`consoleInterpreter`), so the
|
|
6508
|
+
* name this app has always spawned is tried first and `lop` covers an install
|
|
6509
|
+
* whose older console script is gone.
|
|
6510
|
+
*/
|
|
6511
|
+
globalConsoleScript() {
|
|
6512
|
+
return resolveGlobalConsoleScript();
|
|
6513
|
+
}
|
|
6194
6514
|
/**
|
|
6195
6515
|
* Check if the local-operator command exists globally
|
|
6196
6516
|
* @returns Promise resolving to true if the command exists, false otherwise
|
|
6197
6517
|
*/
|
|
6198
6518
|
async checkLocalOperatorExists() {
|
|
6199
|
-
|
|
6200
|
-
|
|
6519
|
+
const command = this.globalConsoleScript();
|
|
6520
|
+
if (!command) {
|
|
6201
6521
|
logger.info(
|
|
6202
|
-
|
|
6522
|
+
"local-operator command not found globally",
|
|
6203
6523
|
LogFileType.BACKEND
|
|
6204
6524
|
);
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
"Library",
|
|
6217
|
-
"Application Support",
|
|
6218
|
-
"Local Operator"
|
|
6219
|
-
)
|
|
6220
|
-
)) {
|
|
6221
|
-
logger.info(
|
|
6222
|
-
"The PATH command belongs to a legacy managed environment; preparing a separate backend instead",
|
|
6223
|
-
LogFileType.BACKEND
|
|
6224
|
-
);
|
|
6225
|
-
return false;
|
|
6226
|
-
}
|
|
6227
|
-
logger.info(
|
|
6228
|
-
`local-operator command found at: ${stdout.trim()}`,
|
|
6229
|
-
LogFileType.BACKEND
|
|
6230
|
-
);
|
|
6231
|
-
return true;
|
|
6232
|
-
}
|
|
6233
|
-
} catch (_error) {
|
|
6525
|
+
return false;
|
|
6526
|
+
}
|
|
6527
|
+
if (process.platform === "darwin" && isLegacyManagedCommand(
|
|
6528
|
+
command,
|
|
6529
|
+
path$1.join(
|
|
6530
|
+
electron.app.getPath("home"),
|
|
6531
|
+
"Library",
|
|
6532
|
+
"Application Support",
|
|
6533
|
+
"Local Operator"
|
|
6534
|
+
)
|
|
6535
|
+
)) {
|
|
6234
6536
|
logger.info(
|
|
6235
|
-
|
|
6537
|
+
`The resolved command belongs to a legacy managed environment (${command}); preparing a separate backend instead`,
|
|
6236
6538
|
LogFileType.BACKEND
|
|
6237
6539
|
);
|
|
6540
|
+
return false;
|
|
6238
6541
|
}
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
*/
|
|
6245
|
-
canonicalLauncher() {
|
|
6246
|
-
return resolveCommandPath("lop") ?? resolveCommandPath("local-operator");
|
|
6542
|
+
logger.info(
|
|
6543
|
+
`local-operator command found at: ${command}`,
|
|
6544
|
+
LogFileType.BACKEND
|
|
6545
|
+
);
|
|
6546
|
+
return true;
|
|
6247
6547
|
}
|
|
6248
6548
|
/**
|
|
6249
6549
|
* `sys.prefix` of the install the user's own `lop` runs, when it can be named.
|
|
@@ -6258,7 +6558,7 @@ class BackendServiceManager {
|
|
|
6258
6558
|
*/
|
|
6259
6559
|
preferredInstallPrefix() {
|
|
6260
6560
|
try {
|
|
6261
|
-
const identity = readInstallIdentity(this.
|
|
6561
|
+
const identity = readInstallIdentity(this.globalConsoleScript());
|
|
6262
6562
|
if (identity.uvReceipt) return path$1.dirname(identity.uvReceipt);
|
|
6263
6563
|
if (identity.venvPrefix) return identity.venvPrefix;
|
|
6264
6564
|
return null;
|
|
@@ -6637,14 +6937,32 @@ class BackendServiceManager {
|
|
|
6637
6937
|
* Start the backend service
|
|
6638
6938
|
* @returns Promise resolving to true if the backend was started successfully, false otherwise
|
|
6639
6939
|
*/
|
|
6640
|
-
/**
|
|
6641
|
-
*
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6940
|
+
/** The global console to launch, resolved the one shell-free way.
|
|
6941
|
+
*
|
|
6942
|
+
* `ownedServeLaunch` still proves the identity of what it is handed rather than
|
|
6943
|
+
* trusting the name it was resolved under - that part is unchanged. What
|
|
6944
|
+
* changed is WHERE the name comes from: this method used to re-resolve a bare
|
|
6945
|
+
* `local-operator` through a shell, in the spawn environment that carries the
|
|
6946
|
+
* login shell's PATH. That is the second resolution `globalConsoleScript`
|
|
6947
|
+
* warns about, and it could name a venv a shell rc file prepends while the
|
|
6948
|
+
* decision above had ranked the operator's real install - two answers to one
|
|
6949
|
+
* question, with the spawn using the one the ranking never saw. Reading the
|
|
6950
|
+
* same helper removes the possibility instead of documenting it.
|
|
6951
|
+
*
|
|
6952
|
+
* The throw is reachable only when the resolution succeeded a moment ago and
|
|
6953
|
+
* the install disappeared since. It is deliberately not swallowed into an
|
|
6954
|
+
* empty string: `consoleInterpreter("")` would report that as an ENOENT on a
|
|
6955
|
+
* path nobody printed, and the caller's own failure report is the honest face
|
|
6956
|
+
* of this.
|
|
6957
|
+
*/
|
|
6958
|
+
async resolveGlobalConsole() {
|
|
6959
|
+
const command = this.globalConsoleScript();
|
|
6960
|
+
if (!command) {
|
|
6961
|
+
throw new Error(
|
|
6962
|
+
"No global local-operator command could be resolved, though one was found a moment ago"
|
|
6963
|
+
);
|
|
6964
|
+
}
|
|
6965
|
+
return command;
|
|
6648
6966
|
}
|
|
6649
6967
|
/**
|
|
6650
6968
|
* Start a daemon this app owns, or attach to one that already exists.
|
|
@@ -6717,7 +7035,7 @@ class BackendServiceManager {
|
|
|
6717
7035
|
let interpreters;
|
|
6718
7036
|
if (globalInstall) {
|
|
6719
7037
|
this.startupMode = "GLOBAL_INSTALL";
|
|
6720
|
-
const executable = await this.resolveGlobalConsole(
|
|
7038
|
+
const executable = await this.resolveGlobalConsole();
|
|
6721
7039
|
interpreters = process.platform === "win32" ? await windowsInterpreterCandidates(executable, env) : [consoleInterpreter(executable)];
|
|
6722
7040
|
} else {
|
|
6723
7041
|
this.startupMode = "APP_BUNDLED_VENV";
|
|
@@ -7481,6 +7799,20 @@ class BackendServiceManager {
|
|
|
7481
7799
|
getOwnedPid() {
|
|
7482
7800
|
return this.process?.pid ?? null;
|
|
7483
7801
|
}
|
|
7802
|
+
/**
|
|
7803
|
+
* The address this app is talking to RIGHT NOW.
|
|
7804
|
+
*
|
|
7805
|
+
* Not the configured target: `attachTo` rotates `backendUrl` onto the daemon
|
|
7806
|
+
* discovery adopted, and everything that queries the backend resolves against
|
|
7807
|
+
* it at call time. A consumer that instead read the configured URL described a
|
|
7808
|
+
* daemon the app is not talking to - the update service's `/health` read did
|
|
7809
|
+
* exactly that, so for an adopted daemon every version read failed and the
|
|
7810
|
+
* panel could neither name the build being served nor report the skew after an
|
|
7811
|
+
* install moved (QA Q-2, UX U1).
|
|
7812
|
+
*/
|
|
7813
|
+
getBackendUrl() {
|
|
7814
|
+
return this.backendUrl;
|
|
7815
|
+
}
|
|
7484
7816
|
/**
|
|
7485
7817
|
* Check if the backend service is using an external backend
|
|
7486
7818
|
* @returns True if using an external backend, false if we started our own
|
|
@@ -15508,6 +15840,9 @@ function runCommand(command, args, options = {}) {
|
|
|
15508
15840
|
);
|
|
15509
15841
|
});
|
|
15510
15842
|
}
|
|
15843
|
+
function serverUpdateLogPath() {
|
|
15844
|
+
return path$1.join(logger.getLogPath(), LogFileType.UPDATE_SERVICE);
|
|
15845
|
+
}
|
|
15511
15846
|
function readCommandOutput(command, args) {
|
|
15512
15847
|
try {
|
|
15513
15848
|
const output = node_child_process.execFileSync(command, args, {
|
|
@@ -15519,6 +15854,11 @@ function readCommandOutput(command, args) {
|
|
|
15519
15854
|
return null;
|
|
15520
15855
|
}
|
|
15521
15856
|
}
|
|
15857
|
+
const INSTALLER_PROBE_TIMEOUT_MS = 5e3;
|
|
15858
|
+
function stderrTail(stderr, maxLines = 4) {
|
|
15859
|
+
const lines = stderr.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
15860
|
+
return lines.slice(-maxLines).join("\n").slice(-600);
|
|
15861
|
+
}
|
|
15522
15862
|
class UpdateService {
|
|
15523
15863
|
mainWindow = null;
|
|
15524
15864
|
isDevMode;
|
|
@@ -17364,19 +17704,39 @@ ${heal.removed.join("\n")}`,
|
|
|
17364
17704
|
if (v1PreRelease && !v2PreRelease) return false;
|
|
17365
17705
|
return v1PreRelease > v2PreRelease;
|
|
17366
17706
|
}
|
|
17707
|
+
/**
|
|
17708
|
+
* The address every backend read in this service goes to.
|
|
17709
|
+
*
|
|
17710
|
+
* The manager's LIVE address rather than the configured one: an adopted daemon
|
|
17711
|
+
* is discovered at a port the configuration never named, and the manager rotates
|
|
17712
|
+
* its own `backendUrl` onto it when it attaches. Reading the configured URL
|
|
17713
|
+
* instead made every health read fail for exactly the shape this branch adds -
|
|
17714
|
+
* a daemon the app did not start - so the skew after an install moved degraded
|
|
17715
|
+
* to "no reading" on the one path where that sentence is the only report the
|
|
17716
|
+
* user gets, while Settings could name the same daemon's version a few clicks
|
|
17717
|
+
* away (QA Q-2, UX U1).
|
|
17718
|
+
*
|
|
17719
|
+
* The configured URL stays as the fallback for the frames where no manager is
|
|
17720
|
+
* attached yet - the update service is constructed before it in the app's start
|
|
17721
|
+
* path.
|
|
17722
|
+
*/
|
|
17723
|
+
liveBackendUrl() {
|
|
17724
|
+
return this.backendService?.getBackendUrl() ?? this.backendUrl;
|
|
17725
|
+
}
|
|
17367
17726
|
/**
|
|
17368
17727
|
* Get the installed backend version using the health API
|
|
17369
17728
|
* @returns Promise resolving to the installed version or null if not found
|
|
17370
17729
|
*/
|
|
17371
17730
|
async getInstalledBackendVersion() {
|
|
17372
17731
|
try {
|
|
17732
|
+
const backendUrl = this.liveBackendUrl();
|
|
17373
17733
|
logger.info(
|
|
17374
|
-
`Checking backend version from health API at ${
|
|
17734
|
+
`Checking backend version from health API at ${backendUrl}/health`,
|
|
17375
17735
|
LogFileType.UPDATE_SERVICE
|
|
17376
17736
|
);
|
|
17377
17737
|
const controller = new AbortController();
|
|
17378
17738
|
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
17379
|
-
const response = await fetch(`${
|
|
17739
|
+
const response = await fetch(`${backendUrl}/health`, {
|
|
17380
17740
|
method: "GET",
|
|
17381
17741
|
headers: { Accept: "application/json" },
|
|
17382
17742
|
signal: controller.signal
|
|
@@ -17408,9 +17768,10 @@ ${heal.removed.join("\n")}`,
|
|
|
17408
17768
|
LogFileType.UPDATE_SERVICE,
|
|
17409
17769
|
error
|
|
17410
17770
|
);
|
|
17411
|
-
|
|
17771
|
+
const retryUrl = this.liveBackendUrl();
|
|
17772
|
+
if (retryUrl.includes("127.0.0.1")) {
|
|
17412
17773
|
try {
|
|
17413
|
-
const altUrl =
|
|
17774
|
+
const altUrl = retryUrl.replace("127.0.0.1", "localhost");
|
|
17414
17775
|
logger.info(
|
|
17415
17776
|
`Trying alternative URL: ${altUrl}/health`,
|
|
17416
17777
|
LogFileType.UPDATE_SERVICE
|
|
@@ -17531,12 +17892,10 @@ ${heal.removed.join("\n")}`,
|
|
|
17531
17892
|
*/
|
|
17532
17893
|
async resolveBackendUpdatePlan(startupMode) {
|
|
17533
17894
|
if (startupMode === LocalOperatorStartupMode.EXISTING_SERVER || startupMode === LocalOperatorStartupMode.GLOBAL_INSTALL) {
|
|
17534
|
-
const
|
|
17535
|
-
|
|
17536
|
-
lopUpdatePath: this.resolveLopUpdatePath()
|
|
17537
|
-
});
|
|
17895
|
+
const identity = await this.resolveInstallIdentity();
|
|
17896
|
+
const plan = resolveGlobalInstallPlan({ identity });
|
|
17538
17897
|
logger.info(
|
|
17539
|
-
`External backend install (${startupMode}): ${plan.detail}; remedy is \`${plan.updateCommand || "nothing the app can name"}
|
|
17898
|
+
`External backend install (${startupMode}): ${plan.detail}; remedy is \`${plan.updateCommand || "nothing the app can name"}\`; install version ${identity.version ?? "unreadable"}`,
|
|
17540
17899
|
LogFileType.UPDATE_SERVICE
|
|
17541
17900
|
);
|
|
17542
17901
|
return {
|
|
@@ -17544,7 +17903,8 @@ ${heal.removed.join("\n")}`,
|
|
|
17544
17903
|
updateCommand: plan.updateCommand,
|
|
17545
17904
|
remedy: plan.remedy,
|
|
17546
17905
|
detail: plan.detail,
|
|
17547
|
-
sourceBuild: plan.sourceBuild
|
|
17906
|
+
sourceBuild: plan.sourceBuild,
|
|
17907
|
+
installedInstallVersion: identity.version ?? null
|
|
17548
17908
|
};
|
|
17549
17909
|
}
|
|
17550
17910
|
return {
|
|
@@ -17552,7 +17912,8 @@ ${heal.removed.join("\n")}`,
|
|
|
17552
17912
|
updateCommand: "pip install --upgrade local-operator",
|
|
17553
17913
|
remedy: "Updating the server will improve AI functionality.",
|
|
17554
17914
|
detail: `The app started this server itself (${startupMode}).`,
|
|
17555
|
-
sourceBuild: false
|
|
17915
|
+
sourceBuild: false,
|
|
17916
|
+
installedInstallVersion: null
|
|
17556
17917
|
};
|
|
17557
17918
|
}
|
|
17558
17919
|
/**
|
|
@@ -17571,25 +17932,7 @@ ${heal.removed.join("\n")}`,
|
|
|
17571
17932
|
* searches the machine's and the user's own install locations.
|
|
17572
17933
|
*/
|
|
17573
17934
|
resolveLocalOperatorPath() {
|
|
17574
|
-
|
|
17575
|
-
const first = readCommandOutput("where", ["local-operator"])?.split("\n")[0]?.trim();
|
|
17576
|
-
return first && first.length > 0 ? first : null;
|
|
17577
|
-
}
|
|
17578
|
-
return resolveCommandPath("local-operator");
|
|
17579
|
-
}
|
|
17580
|
-
/**
|
|
17581
|
-
* Path the `lop-update` shim resolves to, or null when this machine has none.
|
|
17582
|
-
*
|
|
17583
|
-
* The same problem as the shim above, for the remedy rather than the
|
|
17584
|
-
* classification: `lop-update` is what the source-build wording tells the user
|
|
17585
|
-
* to run, and it lives in `~/.local/bin`, which a Finder launch cannot see
|
|
17586
|
-
* through PATH alone - so the plan fell back to the pip/pipx wording for an
|
|
17587
|
-
* install that is neither. It is a shell script, so there is no Windows
|
|
17588
|
-
* counterpart to look for.
|
|
17589
|
-
*/
|
|
17590
|
-
resolveLopUpdatePath() {
|
|
17591
|
-
if (process.platform === "win32") return null;
|
|
17592
|
-
return resolveCommandPath("lop-update");
|
|
17935
|
+
return resolveGlobalConsoleScript();
|
|
17593
17936
|
}
|
|
17594
17937
|
/**
|
|
17595
17938
|
* Everything `classifyGlobalInstall` needs to tell a uv tool install from a
|
|
@@ -17601,17 +17944,44 @@ ${heal.removed.join("\n")}`,
|
|
|
17601
17944
|
* answered: spawning `uv` and `pipx` on every backend check for a question
|
|
17602
17945
|
* three string tests settled is work nobody asked for.
|
|
17603
17946
|
*/
|
|
17604
|
-
resolveInstallIdentity() {
|
|
17947
|
+
async resolveInstallIdentity() {
|
|
17605
17948
|
const identity = readInstallIdentity(this.resolveLocalOperatorPath());
|
|
17606
17949
|
if (identity.path && classifyGlobalInstall(identity) !== "global-unknown") {
|
|
17607
17950
|
return identity;
|
|
17608
17951
|
}
|
|
17609
17952
|
const uv = resolveCommandPath("uv");
|
|
17610
17953
|
const pipx = resolveCommandPath("pipx");
|
|
17611
|
-
|
|
17612
|
-
|
|
17954
|
+
const [uvToolList, pipxList] = await Promise.all([
|
|
17955
|
+
uv ? this.probeInstallerList(uv, ["tool", "list"]) : null,
|
|
17956
|
+
pipx ? this.probeInstallerList(pipx, ["list"]) : null
|
|
17957
|
+
]);
|
|
17958
|
+
identity.uvToolList = uvToolList;
|
|
17959
|
+
identity.pipxList = pipxList;
|
|
17613
17960
|
return identity;
|
|
17614
17961
|
}
|
|
17962
|
+
/**
|
|
17963
|
+
* What an installer's own list command answers, or null.
|
|
17964
|
+
*
|
|
17965
|
+
* ASYNC, and that is the R1-4 fix: this runs on every backend check for an
|
|
17966
|
+
* install the three string tests cannot classify, and the synchronous form of it
|
|
17967
|
+
* blocked the main thread for up to 5 s per probe - 10 s for the pair, with the
|
|
17968
|
+
* window and every IPC channel frozen behind it. Review R1-4 measured it; the
|
|
17969
|
+
* answer is identical, only the waiting is not done on the event loop.
|
|
17970
|
+
*
|
|
17971
|
+
* `runCommand` rather than a second runner, and with `pythonSpawnEnv()`: `uv` and
|
|
17972
|
+
* `pipx` are python programs, so they get the same guarded environment every
|
|
17973
|
+
* other python spawn here passes - which is what keeps bytecode they write out
|
|
17974
|
+
* of the installed app.
|
|
17975
|
+
*/
|
|
17976
|
+
async probeInstallerList(command, args) {
|
|
17977
|
+
const result = await runCommand(command, args, {
|
|
17978
|
+
timeoutMs: INSTALLER_PROBE_TIMEOUT_MS,
|
|
17979
|
+
env: this.pythonSpawnEnv()
|
|
17980
|
+
});
|
|
17981
|
+
if (!result.ran) return null;
|
|
17982
|
+
const output = result.stdout.trim();
|
|
17983
|
+
return output.length > 0 ? output : null;
|
|
17984
|
+
}
|
|
17615
17985
|
/**
|
|
17616
17986
|
* Check the installed server against the published release.
|
|
17617
17987
|
*
|
|
@@ -17626,6 +17996,7 @@ ${heal.removed.join("\n")}`,
|
|
|
17626
17996
|
`Checking for backend updates... (silent mode: ${silent})`,
|
|
17627
17997
|
LogFileType.UPDATE_SERVICE
|
|
17628
17998
|
);
|
|
17999
|
+
await this.reportUnattendedServerUpdate();
|
|
17629
18000
|
try {
|
|
17630
18001
|
if (this.isDevMode) {
|
|
17631
18002
|
logger.info(
|
|
@@ -17655,9 +18026,12 @@ ${heal.removed.join("\n")}`,
|
|
|
17655
18026
|
);
|
|
17656
18027
|
return { status: "unavailable", info: null };
|
|
17657
18028
|
}
|
|
17658
|
-
const
|
|
18029
|
+
const plan = await this.resolveBackendUpdatePlan(startupMode);
|
|
18030
|
+
const runningVersion = await this.getInstalledBackendVersion();
|
|
17659
18031
|
const latestVersion = await this.getLatestPypiVersion();
|
|
17660
18032
|
if (latestVersion) this.lastPublishedBackendVersion = latestVersion;
|
|
18033
|
+
const installVersion = plan.installedInstallVersion;
|
|
18034
|
+
const installedVersion = installVersion && isReadableVersion(installVersion) ? installVersion : runningVersion;
|
|
17661
18035
|
if (!installedVersion || !latestVersion) {
|
|
17662
18036
|
logger.error(
|
|
17663
18037
|
"Unable to determine backend versions.",
|
|
@@ -17666,7 +18040,8 @@ ${heal.removed.join("\n")}`,
|
|
|
17666
18040
|
if (reportFailure) {
|
|
17667
18041
|
this.sendToRenderer("backend-update-error", {
|
|
17668
18042
|
message: "Unable to determine backend version.",
|
|
17669
|
-
phase: "check"
|
|
18043
|
+
phase: "check",
|
|
18044
|
+
logPath: serverUpdateLogPath()
|
|
17670
18045
|
});
|
|
17671
18046
|
}
|
|
17672
18047
|
return { status: "unavailable", info: null };
|
|
@@ -17679,7 +18054,8 @@ ${heal.removed.join("\n")}`,
|
|
|
17679
18054
|
if (reportFailure) {
|
|
17680
18055
|
this.sendToRenderer("backend-update-error", {
|
|
17681
18056
|
message: "The installed server version could not be determined, so no update was offered. Restart the app to try again.",
|
|
17682
|
-
phase: "check"
|
|
18057
|
+
phase: "check",
|
|
18058
|
+
logPath: serverUpdateLogPath()
|
|
17683
18059
|
});
|
|
17684
18060
|
}
|
|
17685
18061
|
return { status: "unavailable", info: null };
|
|
@@ -17698,12 +18074,17 @@ ${heal.removed.join("\n")}`,
|
|
|
17698
18074
|
if (reportFailure) {
|
|
17699
18075
|
this.sendToRenderer("backend-update-error", {
|
|
17700
18076
|
message: "Unable to determine backend version.",
|
|
17701
|
-
phase: "check"
|
|
18077
|
+
phase: "check",
|
|
18078
|
+
logPath: serverUpdateLogPath()
|
|
17702
18079
|
});
|
|
17703
18080
|
}
|
|
17704
18081
|
return { status: "unavailable", info: null };
|
|
17705
18082
|
}
|
|
17706
18083
|
const shouldUpdate = this.isNewerVersion(latestVersion, installedVersion);
|
|
18084
|
+
logger.info(
|
|
18085
|
+
`Install on disk reports: ${installVersion ?? "no reading"}, running backend reports: ${runningVersion ?? "no reading"}, Latest: ${latestVersion}, Update needed: ${shouldUpdate}, Startup mode: ${startupMode}`,
|
|
18086
|
+
LogFileType.UPDATE_SERVICE
|
|
18087
|
+
);
|
|
17707
18088
|
logger.info(
|
|
17708
18089
|
`Installed backend version: ${installedVersion}, Latest: ${latestVersion}, Update needed: ${shouldUpdate}, Startup mode: ${startupMode}`,
|
|
17709
18090
|
LogFileType.UPDATE_SERVICE
|
|
@@ -17713,16 +18094,29 @@ ${heal.removed.join("\n")}`,
|
|
|
17713
18094
|
`New backend version available: ${latestVersion} (installed: ${installedVersion})`,
|
|
17714
18095
|
LogFileType.UPDATE_SERVICE
|
|
17715
18096
|
);
|
|
17716
|
-
const { canManageUpdate, updateCommand, remedy, detail, sourceBuild } = await this.resolveBackendUpdatePlan(startupMode);
|
|
17717
18097
|
const updateInfo = {
|
|
17718
18098
|
currentVersion: installedVersion,
|
|
17719
18099
|
latestVersion,
|
|
17720
|
-
|
|
17721
|
-
|
|
18100
|
+
runningVersion,
|
|
18101
|
+
updateCommand: plan.updateCommand,
|
|
18102
|
+
canManageUpdate: plan.canManageUpdate,
|
|
17722
18103
|
startupMode,
|
|
17723
|
-
remedy,
|
|
17724
|
-
detail,
|
|
17725
|
-
sourceBuild,
|
|
18104
|
+
remedy: plan.remedy,
|
|
18105
|
+
detail: plan.detail,
|
|
18106
|
+
sourceBuild: plan.sourceBuild,
|
|
18107
|
+
/*
|
|
18108
|
+
* WHO THE APP WOULD RESTART, on the panel where the user decides (UX U9).
|
|
18109
|
+
*
|
|
18110
|
+
* The managed arm's consequence sentence comes from the plan, and the plan is
|
|
18111
|
+
* an INSTALL classification - it cannot know whether the daemon serving this
|
|
18112
|
+
* app is one the app started. On a machine where discovery ADOPTED a server,
|
|
18113
|
+
* the offer therefore promised a restart that cannot happen, and the app's
|
|
18114
|
+
* own completion notice then said the opposite ("Local Operator does not
|
|
18115
|
+
* restart a server it did not start"). The sentence is right for the arm it
|
|
18116
|
+
* was written for; what was missing is the reading that decides it, which the
|
|
18117
|
+
* other two events already carry. It travels here too.
|
|
18118
|
+
*/
|
|
18119
|
+
restartable: this.backendIsAppOwned(),
|
|
17726
18120
|
/* `silent` is the whole difference between the two callers: the periodic
|
|
17727
18121
|
and start-up checks pass `true`, the IPC handlers behind the buttons
|
|
17728
18122
|
pass `false`. The renderer needs to know which one it is answering,
|
|
@@ -17742,9 +18136,23 @@ ${heal.removed.join("\n")}`,
|
|
|
17742
18136
|
`No new backend version available. (installed: ${installedVersion}, latest: ${latestVersion})`,
|
|
17743
18137
|
LogFileType.UPDATE_SERVICE
|
|
17744
18138
|
);
|
|
17745
|
-
|
|
18139
|
+
const readingsDiffer = installedVersion !== null && runningVersion !== null && isReadableVersion(installedVersion) && isReadableVersion(runningVersion) && installedVersion.trim() !== runningVersion.trim();
|
|
18140
|
+
if ((!silent || readingsDiffer) && this.mainWindow && !this.mainWindow.isDestroyed() && this.mainWindow.webContents && !this.mainWindow.webContents.isDestroyed()) {
|
|
17746
18141
|
this.mainWindow.webContents.send("backend-update-not-available", {
|
|
17747
|
-
version: installedVersion
|
|
18142
|
+
version: installedVersion,
|
|
18143
|
+
/*
|
|
18144
|
+
* The second reading travels on THIS state too, which is the one QA
|
|
18145
|
+
* Q-1 found: an install already at the published version whose daemon
|
|
18146
|
+
* still serves the old build offers nothing, so the offer's detail was
|
|
18147
|
+
* never rendered and the user was told nothing at all - while the log
|
|
18148
|
+
* carried both readings and Settings could name the daemon. It is the
|
|
18149
|
+
* state design §5 exists for (right after a landed install, before the
|
|
18150
|
+
* restart), and no later check re-offers it, because the install itself
|
|
18151
|
+
* is up to date.
|
|
18152
|
+
*/
|
|
18153
|
+
runningVersion,
|
|
18154
|
+
/** Whether the app may restart the daemon that is behind. */
|
|
18155
|
+
restartable: this.backendIsAppOwned()
|
|
17748
18156
|
});
|
|
17749
18157
|
}
|
|
17750
18158
|
return { status: "current", info: null };
|
|
@@ -17764,7 +18172,8 @@ ${heal.removed.join("\n")}`,
|
|
|
17764
18172
|
* keeps that sentence's machine line readable.
|
|
17765
18173
|
*/
|
|
17766
18174
|
message: stripErrorPrefixes(error.message),
|
|
17767
|
-
phase: "check"
|
|
18175
|
+
phase: "check",
|
|
18176
|
+
logPath: serverUpdateLogPath()
|
|
17768
18177
|
});
|
|
17769
18178
|
}
|
|
17770
18179
|
return { status: "unavailable", info: null };
|
|
@@ -17833,7 +18242,11 @@ ${heal.removed.join("\n")}`,
|
|
|
17833
18242
|
const version = await this.getInstalledBackendVersion();
|
|
17834
18243
|
if (version && version !== "Unknown") {
|
|
17835
18244
|
last = version;
|
|
17836
|
-
if (target2 == null
|
|
18245
|
+
if (target2 == null) return version;
|
|
18246
|
+
const order = compareVersions$1(version, target2);
|
|
18247
|
+
if (order === null ? version.trim() === target2.trim() : order >= 0) {
|
|
18248
|
+
return version;
|
|
18249
|
+
}
|
|
17837
18250
|
}
|
|
17838
18251
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
17839
18252
|
}
|
|
@@ -17843,6 +18256,281 @@ ${heal.removed.join("\n")}`,
|
|
|
17843
18256
|
);
|
|
17844
18257
|
return null;
|
|
17845
18258
|
}
|
|
18259
|
+
/**
|
|
18260
|
+
* Whether the daemon serving this app is one the APP may restart.
|
|
18261
|
+
*
|
|
18262
|
+
* The answer decides what the skew notice can tell the user to do about a server
|
|
18263
|
+
* still on the old build: an owned daemon is the app's to bounce (so the sentence
|
|
18264
|
+
* names restarting Local Operator, which is an action), while an adopted one is
|
|
18265
|
+
* not (so the sentence says the app will not, which is a fact about what the
|
|
18266
|
+
* reader should expect). Getting this wrong is not cosmetic - the notice would
|
|
18267
|
+
* tell a user of an app-owned daemon that nothing will ever move it.
|
|
18268
|
+
*/
|
|
18269
|
+
backendIsAppOwned() {
|
|
18270
|
+
return this.backendService !== null && !this.backendService.isUsingExternalBackend();
|
|
18271
|
+
}
|
|
18272
|
+
/**
|
|
18273
|
+
* The version the resolved global install reports on disk, or null.
|
|
18274
|
+
*
|
|
18275
|
+
* Shell-free, and deliberately a different read from the running backend's
|
|
18276
|
+
* `/health`: the install is what an update moves, and immediately after one it
|
|
18277
|
+
* is the only reading that has moved - the daemon lags until it is restarted.
|
|
18278
|
+
* Read through the same resolution the plan classified, because it is half of
|
|
18279
|
+
* the evidence that the install changed at all.
|
|
18280
|
+
*/
|
|
18281
|
+
readGlobalInstallVersion() {
|
|
18282
|
+
return readInstallIdentity(this.resolveLocalOperatorPath())?.version ?? null;
|
|
18283
|
+
}
|
|
18284
|
+
/**
|
|
18285
|
+
* Run the install's own front end: `<resolved shim> update`.
|
|
18286
|
+
*
|
|
18287
|
+
* WHY THIS AND NOT AN INSTALLER. The mechanism that owns a uv tool install is
|
|
18288
|
+
* `lop update`: it detects the install kind, honours the `editable`/`unknown`
|
|
18289
|
+
* refusals, writes the `.lop-source` marker runtimes converge on, prunes the
|
|
18290
|
+
* generations nothing refers to, and refreshes the daemons it supervises. An
|
|
18291
|
+
* app that ran `uv` itself would bypass every one of those, and the two
|
|
18292
|
+
* commands this path used to name are the ones the harness documents as
|
|
18293
|
+
* failing (`uv tool upgrade` on a pinned receipt or a git snapshot) or as
|
|
18294
|
+
* belonging to another audience (`lop-update`, the release owner's
|
|
18295
|
+
* build-from-checkout script, which refuses when its ref has diverged).
|
|
18296
|
+
*
|
|
18297
|
+
* THE PATH IS BUILT, NOT INHERITED. `lop update` reaches `uv` by bare name
|
|
18298
|
+
* through the child's environment, and this app is normally launched by Finder
|
|
18299
|
+
* with `/usr/bin:/bin:/usr/sbin:/sbin` - on this machine `uv` is
|
|
18300
|
+
* `/opt/homebrew/bin/uv`. Without `installerSearchPath` the feature fails with
|
|
18301
|
+
* an `installer exited 127` that reads as an installer bug.
|
|
18302
|
+
*
|
|
18303
|
+
* The cwd is the user's home: a neutral directory, never the app's resources
|
|
18304
|
+
* dir, so an installer that resolves a relative path from where it stands
|
|
18305
|
+
* cannot land inside the sealed bundle. `shell: true` is never used - this is a
|
|
18306
|
+
* resolved shim path, not a command line.
|
|
18307
|
+
*/
|
|
18308
|
+
async runGlobalUpdate(consolePath) {
|
|
18309
|
+
const updatePath = installerSearchPath(process.env, os.homedir());
|
|
18310
|
+
logger.info(
|
|
18311
|
+
`Executing the install's own updater: ${consolePath} update (PATH: ${updatePath})`,
|
|
18312
|
+
LogFileType.UPDATE_SERVICE
|
|
18313
|
+
);
|
|
18314
|
+
const run = await runCommand(consolePath, ["update"], {
|
|
18315
|
+
timeoutMs: 15 * 60 * 1e3,
|
|
18316
|
+
env: { ...this.pythonSpawnEnv(), PATH: updatePath }
|
|
18317
|
+
});
|
|
18318
|
+
logger.info(
|
|
18319
|
+
`lop update stdout: ${run.stdout.trim() || "(empty)"}`,
|
|
18320
|
+
LogFileType.UPDATE_SERVICE
|
|
18321
|
+
);
|
|
18322
|
+
if (run.stderr.trim().length > 0) {
|
|
18323
|
+
logger.warn(
|
|
18324
|
+
`lop update stderr: ${run.stderr.trim()}`,
|
|
18325
|
+
LogFileType.UPDATE_SERVICE
|
|
18326
|
+
);
|
|
18327
|
+
}
|
|
18328
|
+
return run;
|
|
18329
|
+
}
|
|
18330
|
+
/**
|
|
18331
|
+
* True while an app-driven update of the global install is running.
|
|
18332
|
+
*
|
|
18333
|
+
* The guard is here and not on the surfaces that ask for the update, because
|
|
18334
|
+
* their flags are per-surface: the offer holds `checking` and the run panel holds
|
|
18335
|
+
* `updatingBackend`, so a press on each of them reached `updateGlobalInstall`
|
|
18336
|
+
* twice and the second `lop update` ran beside the first against one install root
|
|
18337
|
+
* (review R1-8). The bundled path never needed one because it stops the backend
|
|
18338
|
+
* first, which serialises it by accident; nothing on this path stops anything.
|
|
18339
|
+
*/
|
|
18340
|
+
globalUpdateInFlight = false;
|
|
18341
|
+
/**
|
|
18342
|
+
* Update the resolved global install, then restart the daemon this app owns.
|
|
18343
|
+
*
|
|
18344
|
+
* ORDERING IS THE POINT, and it is the opposite of the bundled path: that one
|
|
18345
|
+
* stops the backend BEFORE pip because pip replaces the environment the daemon
|
|
18346
|
+
* runs from. Under generations the install lands in a new tree, the pointer
|
|
18347
|
+
* flips, and no running process's tree is touched - so the install cannot fail
|
|
18348
|
+
* the daemon, and stopping first would throw away in-flight work for the whole
|
|
18349
|
+
* install to buy nothing. Install, verify, restart. A failed install therefore
|
|
18350
|
+
* leaves a daemon that never stopped serving.
|
|
18351
|
+
*
|
|
18352
|
+
* `landed` is a READING, not an exit code: `lop update` exits 0 in its "already
|
|
18353
|
+
* latest" case too, so the app holds the install's own version to the target
|
|
18354
|
+
* exactly as the pip path does (`didUpgradeLand`).
|
|
18355
|
+
*/
|
|
18356
|
+
async updateGlobalInstall(backend, plan, target2) {
|
|
18357
|
+
if (this.globalUpdateInFlight) {
|
|
18358
|
+
logger.warn(
|
|
18359
|
+
"A global install update is already running; refusing to start a second beside it",
|
|
18360
|
+
LogFileType.UPDATE_SERVICE
|
|
18361
|
+
);
|
|
18362
|
+
this.sendToRenderer("backend-update-error", {
|
|
18363
|
+
message: "A server update is already running. Let it finish before starting another.",
|
|
18364
|
+
phase: "update",
|
|
18365
|
+
logPath: serverUpdateLogPath()
|
|
18366
|
+
});
|
|
18367
|
+
return false;
|
|
18368
|
+
}
|
|
18369
|
+
this.globalUpdateInFlight = true;
|
|
18370
|
+
try {
|
|
18371
|
+
return await this.runGlobalUpdateAttempt(backend, plan, target2);
|
|
18372
|
+
} finally {
|
|
18373
|
+
this.globalUpdateInFlight = false;
|
|
18374
|
+
}
|
|
18375
|
+
}
|
|
18376
|
+
/** The attempt itself, under the guard above. */
|
|
18377
|
+
async runGlobalUpdateAttempt(backend, plan, target2) {
|
|
18378
|
+
const consolePath = this.resolveLocalOperatorPath();
|
|
18379
|
+
if (!consolePath) {
|
|
18380
|
+
this.sendToRenderer("backend-update-error", {
|
|
18381
|
+
message: "The global install could not be found on this machine, so the update did not run. See the update service log.",
|
|
18382
|
+
phase: "update",
|
|
18383
|
+
logPath: serverUpdateLogPath()
|
|
18384
|
+
});
|
|
18385
|
+
return false;
|
|
18386
|
+
}
|
|
18387
|
+
const before = this.readGlobalInstallVersion() ?? plan.installedInstallVersion;
|
|
18388
|
+
this.sendToRenderer("backend-update-progress", { phase: "installing" });
|
|
18389
|
+
writePendingServerUpdateMarker(this.markerDir(), {
|
|
18390
|
+
before,
|
|
18391
|
+
target: target2,
|
|
18392
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
18393
|
+
});
|
|
18394
|
+
const run = await this.runGlobalUpdate(consolePath);
|
|
18395
|
+
const after = this.readGlobalInstallVersion();
|
|
18396
|
+
clearPendingServerUpdateMarker(this.markerDir());
|
|
18397
|
+
const landed = run.exitCode === 0 && didUpgradeLand({ before, after, target: target2 });
|
|
18398
|
+
if (!landed) {
|
|
18399
|
+
const running = await this.getInstalledBackendVersion();
|
|
18400
|
+
const tail = stderrTail(run.stderr);
|
|
18401
|
+
logger.error(
|
|
18402
|
+
`Global install update did not land (exited ${run.exitCode}, ran ${run.ran}): ${before ?? "unknown"} -> ${after ?? "unknown"}, running backend reports ${running ?? "no reading"}`,
|
|
18403
|
+
LogFileType.UPDATE_SERVICE
|
|
18404
|
+
);
|
|
18405
|
+
this.sendToRenderer("backend-update-error", {
|
|
18406
|
+
message: `${!run.ran ? "The server update did not finish: the installer could not be run to a verdict or timed out." : run.exitCode !== 0 ? `The server update did not install: \`lop update\` exited ${run.exitCode}.` : `The server update to ${target2 ?? "the new release"} did not take effect: the install still reports ${after ?? before ?? "its previous version"}.`} The running backend is still serving${running ? ` version ${running}` : ""}; see the update service log for the installer's output.${tail ? `
|
|
18407
|
+
|
|
18408
|
+
${tail}` : ""}`,
|
|
18409
|
+
phase: "update",
|
|
18410
|
+
logPath: serverUpdateLogPath()
|
|
18411
|
+
});
|
|
18412
|
+
return false;
|
|
18413
|
+
}
|
|
18414
|
+
logger.info(
|
|
18415
|
+
`Global install updated: ${before ?? "unknown"} -> ${after ?? "unknown"}`,
|
|
18416
|
+
LogFileType.UPDATE_SERVICE
|
|
18417
|
+
);
|
|
18418
|
+
if (backend.isUsingExternalBackend()) {
|
|
18419
|
+
const running = await this.getInstalledBackendVersion();
|
|
18420
|
+
logger.info(
|
|
18421
|
+
`Install moved to ${after ?? "unknown"}; the attached backend still reports ${running ?? "no reading"}`,
|
|
18422
|
+
LogFileType.UPDATE_SERVICE
|
|
18423
|
+
);
|
|
18424
|
+
this.sendToRenderer("backend-update-completed", {
|
|
18425
|
+
installVersion: after,
|
|
18426
|
+
runningVersion: running,
|
|
18427
|
+
restarted: false,
|
|
18428
|
+
restartable: this.backendIsAppOwned()
|
|
18429
|
+
});
|
|
18430
|
+
return true;
|
|
18431
|
+
}
|
|
18432
|
+
logger.info(
|
|
18433
|
+
"Restarting backend service onto the updated install...",
|
|
18434
|
+
LogFileType.UPDATE_SERVICE
|
|
18435
|
+
);
|
|
18436
|
+
this.sendToRenderer("backend-update-progress", { phase: "restarting" });
|
|
18437
|
+
backend.setAutoUpdating(true);
|
|
18438
|
+
const restartSuccess = await backend.restart();
|
|
18439
|
+
backend.setAutoUpdating(false);
|
|
18440
|
+
if (!restartSuccess) {
|
|
18441
|
+
logger.error(
|
|
18442
|
+
"Backend service restart failed after the global install update",
|
|
18443
|
+
LogFileType.UPDATE_SERVICE
|
|
18444
|
+
);
|
|
18445
|
+
this.sendToRenderer("backend-update-error", {
|
|
18446
|
+
message: "The server was updated but the backend did not restart. Restart Local Operator to run the new version.",
|
|
18447
|
+
phase: "update",
|
|
18448
|
+
logPath: serverUpdateLogPath()
|
|
18449
|
+
});
|
|
18450
|
+
return false;
|
|
18451
|
+
}
|
|
18452
|
+
const healthy = await this.checkBackendHealth();
|
|
18453
|
+
if (!healthy) {
|
|
18454
|
+
logger.error(
|
|
18455
|
+
"Backend service restarted but the health check failed; retrying once",
|
|
18456
|
+
LogFileType.UPDATE_SERVICE
|
|
18457
|
+
);
|
|
18458
|
+
await backend.start();
|
|
18459
|
+
if (!await this.checkBackendHealth()) {
|
|
18460
|
+
this.sendToRenderer("backend-update-error", {
|
|
18461
|
+
message: "The server was updated but the backend did not restart properly. Restart Local Operator to run the new version.",
|
|
18462
|
+
phase: "update",
|
|
18463
|
+
logPath: serverUpdateLogPath()
|
|
18464
|
+
});
|
|
18465
|
+
return false;
|
|
18466
|
+
}
|
|
18467
|
+
}
|
|
18468
|
+
const reported = await this.waitForBackendVersion(target2);
|
|
18469
|
+
if (reported === null) {
|
|
18470
|
+
logger.error(
|
|
18471
|
+
`Backend restarted onto the updated install but did not report ${target2 ?? "the target version"}`,
|
|
18472
|
+
LogFileType.UPDATE_SERVICE
|
|
18473
|
+
);
|
|
18474
|
+
this.sendToRenderer("backend-update-error", {
|
|
18475
|
+
message: `The server restarted but did not report ${target2 ? `version ${target2}` : "the updated version"}. See the update service log, then restart Local Operator.`,
|
|
18476
|
+
phase: "update",
|
|
18477
|
+
logPath: serverUpdateLogPath()
|
|
18478
|
+
});
|
|
18479
|
+
return false;
|
|
18480
|
+
}
|
|
18481
|
+
logger.info(
|
|
18482
|
+
`Backend reports version ${reported} after the global install update`,
|
|
18483
|
+
LogFileType.UPDATE_SERVICE
|
|
18484
|
+
);
|
|
18485
|
+
this.sendToRenderer("backend-update-completed", {
|
|
18486
|
+
installVersion: after,
|
|
18487
|
+
runningVersion: reported,
|
|
18488
|
+
restarted: true
|
|
18489
|
+
});
|
|
18490
|
+
return true;
|
|
18491
|
+
}
|
|
18492
|
+
/**
|
|
18493
|
+
* Report a server update that landed while no app was supervising it.
|
|
18494
|
+
*
|
|
18495
|
+
* The other half of UX U6: the marker above survives exactly when the app died
|
|
18496
|
+
* mid-attempt, and the installer is a child that outlives its parent, so the
|
|
18497
|
+
* update can land with nothing watching. The end state is coherent on its own -
|
|
18498
|
+
* the app comes up on the new build and reports it - which is why this reports
|
|
18499
|
+
* the fact ONCE rather than treating a stale marker as a failure: there is no
|
|
18500
|
+
* remedy to offer and no version to hold anyone to, only a sentence the user has
|
|
18501
|
+
* never seen about an update they started.
|
|
18502
|
+
*
|
|
18503
|
+
* Cleared either way: a marker whose install did NOT move describes an
|
|
18504
|
+
* interrupted attempt whose reason is in the log, and re-reporting it on every
|
|
18505
|
+
* launch would be a permanent warning about a state the app has since read
|
|
18506
|
+
* correctly (and the failure path keeps no record for the app to re-read).
|
|
18507
|
+
*/
|
|
18508
|
+
async reportUnattendedServerUpdate() {
|
|
18509
|
+
const marker = readPendingServerUpdateMarker(this.markerDir());
|
|
18510
|
+
if (!marker) return;
|
|
18511
|
+
const after = this.readGlobalInstallVersion();
|
|
18512
|
+
clearPendingServerUpdateMarker(this.markerDir());
|
|
18513
|
+
if (!after || after === marker.before) {
|
|
18514
|
+
logger.info(
|
|
18515
|
+
`A server update started at ${marker.startedAt} did not move the install (still ${after ?? "no reading"}); nothing to report on this launch`,
|
|
18516
|
+
LogFileType.UPDATE_SERVICE
|
|
18517
|
+
);
|
|
18518
|
+
return;
|
|
18519
|
+
}
|
|
18520
|
+
const running = await this.getInstalledBackendVersion();
|
|
18521
|
+
logger.info(
|
|
18522
|
+
`A server update that no app was supervising landed: ${marker.before ?? "unknown"} -> ${after}; the backend serving this launch reports ${running ?? "no reading"}`,
|
|
18523
|
+
LogFileType.UPDATE_SERVICE
|
|
18524
|
+
);
|
|
18525
|
+
this.sendToRenderer("backend-update-completed", {
|
|
18526
|
+
installVersion: after,
|
|
18527
|
+
runningVersion: running,
|
|
18528
|
+
restarted: false,
|
|
18529
|
+
unattended: true,
|
|
18530
|
+
restartable: this.backendIsAppOwned(),
|
|
18531
|
+
before: marker.before
|
|
18532
|
+
});
|
|
18533
|
+
}
|
|
17846
18534
|
async updateBackend(targetVersion) {
|
|
17847
18535
|
logger.info("Updating backend...", LogFileType.UPDATE_SERVICE);
|
|
17848
18536
|
try {
|
|
@@ -17854,7 +18542,8 @@ ${heal.removed.join("\n")}`,
|
|
|
17854
18542
|
if (this.mainWindow && !this.mainWindow.isDestroyed() && this.mainWindow.webContents && !this.mainWindow.webContents.isDestroyed()) {
|
|
17855
18543
|
this.mainWindow.webContents.send("backend-update-error", {
|
|
17856
18544
|
message: "No backend service reference available, cannot update.",
|
|
17857
|
-
phase: "update"
|
|
18545
|
+
phase: "update",
|
|
18546
|
+
logPath: serverUpdateLogPath()
|
|
17858
18547
|
});
|
|
17859
18548
|
}
|
|
17860
18549
|
return false;
|
|
@@ -17869,13 +18558,22 @@ ${heal.removed.join("\n")}`,
|
|
|
17869
18558
|
if (this.mainWindow && !this.mainWindow.isDestroyed() && this.mainWindow.webContents && !this.mainWindow.webContents.isDestroyed()) {
|
|
17870
18559
|
this.mainWindow.webContents.send("backend-update-error", {
|
|
17871
18560
|
message: "No python server is available, nothing to update.",
|
|
17872
|
-
phase: "update"
|
|
18561
|
+
phase: "update",
|
|
18562
|
+
logPath: serverUpdateLogPath()
|
|
17873
18563
|
});
|
|
17874
18564
|
}
|
|
17875
18565
|
return false;
|
|
17876
18566
|
case LocalOperatorStartupMode.EXISTING_SERVER:
|
|
17877
18567
|
case LocalOperatorStartupMode.GLOBAL_INSTALL: {
|
|
17878
18568
|
const plan = await this.resolveBackendUpdatePlan(startupMode);
|
|
18569
|
+
const target2 = targetVersion ?? this.lastPublishedBackendVersion ?? null;
|
|
18570
|
+
if (plan.canManageUpdate) {
|
|
18571
|
+
return await this.updateGlobalInstall(
|
|
18572
|
+
this.backendService,
|
|
18573
|
+
plan,
|
|
18574
|
+
target2
|
|
18575
|
+
);
|
|
18576
|
+
}
|
|
17879
18577
|
logger.info(
|
|
17880
18578
|
`Cannot manage the update for ${startupMode}: ${plan.detail}`,
|
|
17881
18579
|
LogFileType.UPDATE_SERVICE
|
|
@@ -17885,13 +18583,10 @@ ${heal.removed.join("\n")}`,
|
|
|
17885
18583
|
message: plan.remedy,
|
|
17886
18584
|
command: plan.updateCommand,
|
|
17887
18585
|
detail: plan.detail,
|
|
17888
|
-
|
|
17889
|
-
// button does), otherwise the published release the last check
|
|
17890
|
-
// read: the compatibility banner calls this with no target at
|
|
17891
|
-
// all, and the panel's version sentence then had nothing to
|
|
17892
|
-
// render on the path that actually produces it (review U17).
|
|
17893
|
-
latestVersion: targetVersion ?? this.lastPublishedBackendVersion ?? null,
|
|
18586
|
+
latestVersion: target2,
|
|
17894
18587
|
currentVersion: installed && installed !== "Unknown" ? installed : null,
|
|
18588
|
+
installVersion: plan.installedInstallVersion,
|
|
18589
|
+
runningVersion: installed,
|
|
17895
18590
|
sourceBuild: plan.sourceBuild
|
|
17896
18591
|
});
|
|
17897
18592
|
return false;
|
|
@@ -17905,7 +18600,8 @@ ${heal.removed.join("\n")}`,
|
|
|
17905
18600
|
);
|
|
17906
18601
|
this.sendToRenderer("backend-update-error", {
|
|
17907
18602
|
message: "The app could not tell how this server was started, so it did not update it. See the update service log.",
|
|
17908
|
-
phase: "update"
|
|
18603
|
+
phase: "update",
|
|
18604
|
+
logPath: serverUpdateLogPath()
|
|
17909
18605
|
});
|
|
17910
18606
|
return false;
|
|
17911
18607
|
}
|
|
@@ -17939,7 +18635,8 @@ ${heal.removed.join("\n")}`,
|
|
|
17939
18635
|
await this.restartBackendAfterFailedUpgrade();
|
|
17940
18636
|
this.sendToRenderer("backend-update-error", {
|
|
17941
18637
|
message: "The bundled server's Python environment could not be found, so the update did not run. Please reinstall the application.",
|
|
17942
|
-
phase: "update"
|
|
18638
|
+
phase: "update",
|
|
18639
|
+
logPath: serverUpdateLogPath()
|
|
17943
18640
|
});
|
|
17944
18641
|
return false;
|
|
17945
18642
|
}
|
|
@@ -17979,7 +18676,8 @@ ${pipRun.stderr.trim()}`,
|
|
|
17979
18676
|
const serverIsBack = await this.restartBackendAfterFailedUpgrade();
|
|
17980
18677
|
this.sendToRenderer("backend-update-error", {
|
|
17981
18678
|
message: pipRun.exitCode !== 0 ? serverIsBack ? "The server update failed to install. The previously installed server is running again; see the update service log for pip's output." : "The server update failed to install and the server did not come back up. Restart Local Operator, and see the update service log for pip's output." : `The server update to ${targetVersion ?? "the new release"} did not take effect: the server is still on ${versionAfter ?? "its previous version"}. See the update service log for pip's output, then try again.`,
|
|
17982
|
-
phase: "update"
|
|
18679
|
+
phase: "update",
|
|
18680
|
+
logPath: serverUpdateLogPath()
|
|
17983
18681
|
});
|
|
17984
18682
|
return false;
|
|
17985
18683
|
}
|
|
@@ -18018,7 +18716,8 @@ ${pipRun.stderr.trim()}`,
|
|
|
18018
18716
|
if (this.mainWindow) {
|
|
18019
18717
|
this.mainWindow.webContents.send("backend-update-error", {
|
|
18020
18718
|
message: "Backend was updated but failed to restart properly. Please restart the application.",
|
|
18021
|
-
phase: "update"
|
|
18719
|
+
phase: "update",
|
|
18720
|
+
logPath: serverUpdateLogPath()
|
|
18022
18721
|
});
|
|
18023
18722
|
}
|
|
18024
18723
|
return false;
|
|
@@ -18033,7 +18732,8 @@ ${pipRun.stderr.trim()}`,
|
|
|
18033
18732
|
if (this.mainWindow) {
|
|
18034
18733
|
this.mainWindow.webContents.send("backend-update-error", {
|
|
18035
18734
|
message: "Backend was updated but failed to restart. Please restart the application.",
|
|
18036
|
-
phase: "update"
|
|
18735
|
+
phase: "update",
|
|
18736
|
+
logPath: serverUpdateLogPath()
|
|
18037
18737
|
});
|
|
18038
18738
|
}
|
|
18039
18739
|
return false;
|
|
@@ -18048,7 +18748,8 @@ ${pipRun.stderr.trim()}`,
|
|
|
18048
18748
|
);
|
|
18049
18749
|
this.sendToRenderer("backend-update-error", {
|
|
18050
18750
|
message: `The server restarted but did not report ${targetVersion ? `version ${targetVersion}` : "the updated version"}. Check the update service log, then try again.`,
|
|
18051
|
-
phase: "update"
|
|
18751
|
+
phase: "update",
|
|
18752
|
+
logPath: serverUpdateLogPath()
|
|
18052
18753
|
});
|
|
18053
18754
|
return false;
|
|
18054
18755
|
}
|
|
@@ -18090,7 +18791,8 @@ ${pipRun.stderr.trim()}`,
|
|
|
18090
18791
|
// Cleaned at the source, for the reason written on the check-phase
|
|
18091
18792
|
// report above.
|
|
18092
18793
|
message: stripErrorPrefixes(error.message),
|
|
18093
|
-
phase: "update"
|
|
18794
|
+
phase: "update",
|
|
18795
|
+
logPath: serverUpdateLogPath()
|
|
18094
18796
|
});
|
|
18095
18797
|
}
|
|
18096
18798
|
return false;
|
|
@@ -18121,7 +18823,7 @@ ${pipRun.stderr.trim()}`,
|
|
|
18121
18823
|
try {
|
|
18122
18824
|
const controller = new AbortController();
|
|
18123
18825
|
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
18124
|
-
const response = await fetch(`${this.
|
|
18826
|
+
const response = await fetch(`${this.liveBackendUrl()}/health`, {
|
|
18125
18827
|
method: "GET",
|
|
18126
18828
|
headers: { Accept: "application/json" },
|
|
18127
18829
|
signal: controller.signal
|
|
@@ -19827,7 +20529,7 @@ electron.app.on("window-all-closed", () => {
|
|
|
19827
20529
|
electron.app.quit();
|
|
19828
20530
|
});
|
|
19829
20531
|
const QUIT_FAILSAFE_MARGIN_MS = 5e3;
|
|
19830
|
-
const QUIT_CLEANUP_FAILSAFE_MS =
|
|
20532
|
+
const QUIT_CLEANUP_FAILSAFE_MS = INTERPRETER_RESOLUTION_WORST_MS + OWNED_STOP_WORST_MS + READINESS_POLL_INTERVAL_MS + QUIT_FAILSAFE_MARGIN_MS;
|
|
19831
20533
|
let backendQuitPending = false;
|
|
19832
20534
|
electron.app.on("will-quit", (event) => {
|
|
19833
20535
|
viewerEndpoint?.close();
|