local-operator-ui 0.30.7 → 0.30.9

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.
Files changed (75) hide show
  1. package/out/main/index.js +394 -363
  2. package/out/renderer/assets/{_basePickBy-DBYR8O9B.js → _basePickBy-DNyrIcj1.js} +1 -1
  3. package/out/renderer/assets/{_baseUniq-OhMNZhi7.js → _baseUniq-DTlS10_0.js} +1 -1
  4. package/out/renderer/assets/{agent-details-page-BVicz2tX.js → agent-details-page-DfTUC4Hw.js} +1 -1
  5. package/out/renderer/assets/{agent-hub-page-CHs73703.js → agent-hub-page-BPCMUqt2.js} +1 -1
  6. package/out/renderer/assets/{agents-page-rb2Hwnbu.js → agents-page-DtiGLvDg.js} +2 -2
  7. package/out/renderer/assets/{arc-DJQs7XhV.js → arc-r7sdgMui.js} +1 -1
  8. package/out/renderer/assets/{architectureDiagram-IEHRJDOE-xr0V8iP9.js → architectureDiagram-IEHRJDOE-D03Sjn85.js} +1 -1
  9. package/out/renderer/assets/{blockDiagram-JOT3LUYC--j5Arlup.js → blockDiagram-JOT3LUYC-o1MAS1ip.js} +1 -1
  10. package/out/renderer/assets/browser-page-ezC6CfoA.js +1 -0
  11. package/out/renderer/assets/{browser-webauthn-prompt-BJLi2ZJa.js → browser-webauthn-prompt-RjkxSG7g.js} +1 -1
  12. package/out/renderer/assets/{c4Diagram-VJAJSXHY-Bc6EP_HP.js → c4Diagram-VJAJSXHY-CvIwPJ79.js} +1 -1
  13. package/out/renderer/assets/channel-DBe6P1iM.js +1 -0
  14. package/out/renderer/assets/{chunk-4BMEZGHF--zNo-yKh.js → chunk-4BMEZGHF-BGO6_8-k.js} +1 -1
  15. package/out/renderer/assets/{chunk-A2AXSNBT-CIYOm0W9.js → chunk-A2AXSNBT-DVYXRJr2.js} +1 -1
  16. package/out/renderer/assets/{chunk-AEK57VVT-W5eNrSxP.js → chunk-AEK57VVT-DXpD2k6e.js} +1 -1
  17. package/out/renderer/assets/{chunk-D6G4REZN-DOaOf7dY.js → chunk-D6G4REZN-Ztagh5ID.js} +1 -1
  18. package/out/renderer/assets/{chunk-RZ5BOZE2-ClT3mZ9I.js → chunk-RZ5BOZE2-5YGou0Fl.js} +1 -1
  19. package/out/renderer/assets/{chunk-XZIHB7SX-BI7BMUin.js → chunk-XZIHB7SX-B9G_zSy5.js} +1 -1
  20. package/out/renderer/assets/classDiagram-GIVACNV2-Bk7Q5IpA.js +1 -0
  21. package/out/renderer/assets/classDiagram-v2-COTLJTTW-Bk7Q5IpA.js +1 -0
  22. package/out/renderer/assets/clone-VBTybRso.js +1 -0
  23. package/out/renderer/assets/{compact-pagination-BwRh3C3H.js → compact-pagination-BQWoS_T3.js} +1 -1
  24. package/out/renderer/assets/console-mirror-B-JHrGil.js +44 -0
  25. package/out/renderer/assets/{consoleCapture-BIeBPj-4.js → consoleCapture-DkZLnt0c.js} +1 -1
  26. package/out/renderer/assets/{dagre-OKDRZEBW-BWwWtgLx.js → dagre-OKDRZEBW-BF6tmEpr.js} +1 -1
  27. package/out/renderer/assets/{diagram-SSKATNLV-CWrYQie4.js → diagram-SSKATNLV-C56V-8HL.js} +1 -1
  28. package/out/renderer/assets/{diagram-VNBRO52H-CN2gAwct.js → diagram-VNBRO52H-BfQptzPg.js} +1 -1
  29. package/out/renderer/assets/{erDiagram-Q7BY3M3F-Bu8iPwGW.js → erDiagram-Q7BY3M3F-DL7s81Ai.js} +1 -1
  30. package/out/renderer/assets/{flowDiagram-4HSFHLVR-CuRpi4Dp.js → flowDiagram-4HSFHLVR-i31-vTqr.js} +1 -1
  31. package/out/renderer/assets/{ganttDiagram-APWFNJXF-mnURcNut.js → ganttDiagram-APWFNJXF-CHSCWcyL.js} +1 -1
  32. package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-BdYeZl9i.js → gitGraphDiagram-7IBYFJ6S-Bn8ip0vN.js} +1 -1
  33. package/out/renderer/assets/{graph-MLvZAGI_.js → graph-Z7cSlzW8.js} +1 -1
  34. package/out/renderer/assets/{icon-BQh0NRL5.js → icon-BO__FAYr.js} +1 -1
  35. package/out/renderer/assets/index-CE_NdDSn.css +1 -0
  36. package/out/renderer/assets/{index-BMCrBJ-B.js → index-CcRsLYH6.js} +1 -1
  37. package/out/renderer/assets/{index-J5rd4Dm5.js → index-Do3sSSD3.js} +387 -387
  38. package/out/renderer/assets/{index-wl3vxwRq.js → index-ctXB2K0F.js} +1 -1
  39. package/out/renderer/assets/{infoDiagram-PH2N3AL5-BmNe_X42.js → infoDiagram-PH2N3AL5-CDyGHZRX.js} +1 -1
  40. package/out/renderer/assets/{installer-CbTFoEpL.js → installer-BF6ZtnHE.js} +1 -1
  41. package/out/renderer/assets/{journeyDiagram-U35MCT3I-oz0SPXm5.js → journeyDiagram-U35MCT3I-DHbzBGCn.js} +1 -1
  42. package/out/renderer/assets/{kanban-definition-NDS4AKOZ-D7XbJu_a.js → kanban-definition-NDS4AKOZ-HXlHND8Z.js} +1 -1
  43. package/out/renderer/assets/{layout-Cfw0DBLH.js → layout-DKkLSjUg.js} +1 -1
  44. package/out/renderer/assets/{legacy-agents-page-Cpz2ZQCy.js → legacy-agents-page-CjKs-UM5.js} +1 -1
  45. package/out/renderer/assets/{mermaid.core-XqW4GKnj.js → mermaid.core-BZrracaZ.js} +5 -5
  46. package/out/renderer/assets/{mindmap-definition-ALO5MXBD-CNweV-Fd.js → mindmap-definition-ALO5MXBD-eyU6mx0T.js} +1 -1
  47. package/out/renderer/assets/{page-header-Cc0XdSll.js → page-header-TLDGdE7N.js} +1 -1
  48. package/out/renderer/assets/{parseISO-ew-BKu2O.js → parseISO-ylb_nwMY.js} +1 -1
  49. package/out/renderer/assets/{pieDiagram-IB7DONF6-BPiL68yC.js → pieDiagram-IB7DONF6-CY2nWAXc.js} +1 -1
  50. package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-m9iG_U6Q.js → quadrantDiagram-7GDLP6J5-PXbckdQ1.js} +1 -1
  51. package/out/renderer/assets/{radar-MK3ICKWK-CoKPIP0t.js → radar-MK3ICKWK-Ytg7zJyE.js} +1 -1
  52. package/out/renderer/assets/{radient-auth-buttons-GCNNfevt.js → radient-auth-buttons-BosEK_-0.js} +1 -1
  53. package/out/renderer/assets/{requirementDiagram-KVF5MWMF-BpAhKn90.js → requirementDiagram-KVF5MWMF-Dn7bQF2M.js} +1 -1
  54. package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-Kdb3WPgA.js → sankeyDiagram-QLVOVGJD-awGWDf1K.js} +1 -1
  55. package/out/renderer/assets/{schedules-page-BOd_LGf1.js → schedules-page-4chtzwhv.js} +1 -1
  56. package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-BxaWgsF6.js → sequenceDiagram-X6HHIX6F-CqCnx21b.js} +1 -1
  57. package/out/renderer/assets/{settings-page-DmM_rBG6.js → settings-page-D4oR8Nuf.js} +1 -1
  58. package/out/renderer/assets/{stateDiagram-DGXRK772-bighRJ2W.js → stateDiagram-DGXRK772-DXT6j4nW.js} +1 -1
  59. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-DFfs3v9W.js +1 -0
  60. package/out/renderer/assets/{timeline-definition-BDJGKUSR-DyVGXzVc.js → timeline-definition-BDJGKUSR-D8Zj-moJ.js} +1 -1
  61. package/out/renderer/assets/{use-agent-like-mutation-BQ6GyYOT.js → use-agent-like-mutation-DJSMMJqJ.js} +1 -1
  62. package/out/renderer/assets/{xychartDiagram-VJFVF3MP-CKXHo-5e.js → xychartDiagram-VJFVF3MP-DHhUXll7.js} +1 -1
  63. package/out/renderer/console-capture.html +4 -4
  64. package/out/renderer/index.html +5 -5
  65. package/out/renderer/installer.html +4 -4
  66. package/package.json +17 -4
  67. package/out/renderer/assets/browser-page-DoK-77aU.js +0 -1
  68. package/out/renderer/assets/channel-YCbc1E0Y.js +0 -1
  69. package/out/renderer/assets/classDiagram-GIVACNV2-DqWS7_8g.js +0 -1
  70. package/out/renderer/assets/classDiagram-v2-COTLJTTW-DqWS7_8g.js +0 -1
  71. package/out/renderer/assets/clone-Cyp-7Qpl.js +0 -1
  72. package/out/renderer/assets/console-mirror-BVW-_zui.js +0 -44
  73. package/out/renderer/assets/index-n0ogWXxx.css +0 -1
  74. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-Dheha3M6.js +0 -1
  75. /package/out/renderer/assets/{index-Q1Oi0B95.js → index-ujpapIu9.js} +0 -0
package/out/main/index.js CHANGED
@@ -50,6 +50,9 @@ const BACKEND_RECONNECT_CHANNEL = "backend-reconnect";
50
50
  function relayNeedsRebuild(current, next) {
51
51
  return current.url !== next.url || current.token !== next.token;
52
52
  }
53
+ function pairingHasRemedy(cause) {
54
+ return cause !== "governed-elsewhere" && cause !== "pre-handshake";
55
+ }
53
56
  const id$1 = zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/);
54
57
  const settingKey = zod.z.string().min(1).max(256).regex(/^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/);
55
58
  const secret = zod.z.string().min(1).max(32768);
@@ -2521,16 +2524,16 @@ const updateCheckVerdict = (channels) => ({
2521
2524
  server: channels.server,
2522
2525
  affirmation: updateCheckAffirmation(channels)
2523
2526
  });
2524
- const seedNamespace = "python-runtime-seed";
2527
+ const python = { "seedNamespace": "python-runtime-seed", "checkoutSeedNames": { "arm64": "python_aarch64", "x64": "python" } };
2528
+ const uv = { "namespace": "uv", "binaryNames": { "darwin": "uv", "linux": "uv", "win32": "uv.exe" }, "checkoutNames": { "arm64": "uv_aarch64", "x64": "uv" } };
2525
2529
  const architectures = ["arm64", "x64"];
2526
2530
  const legacyResourceNames = ["python", "python_aarch64"];
2527
- const checkoutSeedNames = { "arm64": "python_aarch64", "x64": "python" };
2528
2531
  const machOMagics = ["cffaedfe", "cefaedfe", "feedfacf", "feedface", "cafebabe", "bebafeca"];
2529
- const BUNDLED_PYTHON_LAYOUT = {
2530
- seedNamespace,
2532
+ const RUNTIME_LAYOUT = {
2533
+ python,
2534
+ uv,
2531
2535
  architectures,
2532
2536
  legacyResourceNames,
2533
- checkoutSeedNames,
2534
2537
  machOMagics
2535
2538
  };
2536
2539
  const embeddedProvisioningProfilePath = "Contents/embedded.provisionprofile";
@@ -2767,9 +2770,9 @@ function canonicalPath(path2) {
2767
2770
  }
2768
2771
  }
2769
2772
  const BUNDLED_PYTHON_DIRS = [
2770
- ...BUNDLED_PYTHON_LAYOUT.legacyResourceNames,
2771
- ...BUNDLED_PYTHON_LAYOUT.architectures.map(
2772
- (arch) => `${BUNDLED_PYTHON_LAYOUT.seedNamespace}/${arch}`
2773
+ ...RUNTIME_LAYOUT.legacyResourceNames,
2774
+ ...RUNTIME_LAYOUT.architectures.map(
2775
+ (arch) => `${RUNTIME_LAYOUT.python.seedNamespace}/${arch}`
2773
2776
  )
2774
2777
  ];
2775
2778
  function isPythonBytecodePath(bundlePath, candidate) {
@@ -5540,6 +5543,205 @@ function ensureVenvBytecodeGuard(venvPath) {
5540
5543
  reason: `${path2} now sets sys.dont_write_bytecode for every process using this venv`
5541
5544
  };
5542
5545
  }
5546
+ const SHELL_PATH_BEGIN = "__LOCAL_OPERATOR_SHELL_PATH_BEGIN__";
5547
+ const SHELL_PATH_END = "__LOCAL_OPERATOR_SHELL_PATH_END__";
5548
+ const SHELL_PATH_TIMEOUT_MS = 3e3;
5549
+ const SHELL_PATH_ANSWER_GRACE_MS = 500;
5550
+ const SHELL_PATH_ARGS = [
5551
+ "-l",
5552
+ "-i",
5553
+ "-c",
5554
+ `command printf '${SHELL_PATH_BEGIN}%s${SHELL_PATH_END}' "$PATH"`
5555
+ ];
5556
+ const POSIX_SHELL_NAMES = /* @__PURE__ */ new Set([
5557
+ "sh",
5558
+ "bash",
5559
+ "dash",
5560
+ "zsh",
5561
+ "ksh",
5562
+ "ksh93",
5563
+ "mksh",
5564
+ "lksh",
5565
+ "pdksh",
5566
+ "ash",
5567
+ "posh",
5568
+ "yash",
5569
+ "rbash"
5570
+ ]);
5571
+ function isPosixShell(shell) {
5572
+ const name = path$1.basename(shell).toLowerCase().replace(/\.exe$/, "");
5573
+ return POSIX_SHELL_NAMES.has(name);
5574
+ }
5575
+ function defaultShell(env, options = {}) {
5576
+ const shell = env.SHELL?.trim();
5577
+ if (shell && path$1.isAbsolute(shell)) return shell;
5578
+ const platform = options.platform ?? process.platform;
5579
+ const exists = options.exists ?? fs.existsSync;
5580
+ const candidates = platform === "darwin" ? ["/bin/zsh", "/bin/bash", "/bin/sh"] : ["/bin/bash", "/bin/sh", "/bin/zsh"];
5581
+ return candidates.find((candidate) => exists(candidate)) ?? "/bin/sh";
5582
+ }
5583
+ function shellPathRunner() {
5584
+ return ({ shell, args, env, timeoutMs }) => new Promise((resolve, reject) => {
5585
+ const child = node_child_process.spawn(shell, [...args], {
5586
+ stdio: ["ignore", "pipe", "pipe"],
5587
+ env,
5588
+ detached: true
5589
+ });
5590
+ let stdout = "";
5591
+ let timedOut = false;
5592
+ const deadline2 = setTimeout(() => {
5593
+ timedOut = true;
5594
+ try {
5595
+ if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
5596
+ else child.kill("SIGKILL");
5597
+ } catch {
5598
+ child.kill("SIGKILL");
5599
+ }
5600
+ }, timeoutMs);
5601
+ child.stdout?.setEncoding("utf8");
5602
+ child.stdout?.on("data", (chunk) => {
5603
+ stdout += chunk;
5604
+ });
5605
+ child.stderr?.resume();
5606
+ child.on("error", (error) => {
5607
+ clearTimeout(deadline2);
5608
+ reject(error);
5609
+ });
5610
+ child.on("close", () => {
5611
+ clearTimeout(deadline2);
5612
+ resolve({ stdout, timedOut });
5613
+ });
5614
+ });
5615
+ }
5616
+ function countOccurrences(value, needle) {
5617
+ let count = 0;
5618
+ let at = value.indexOf(needle);
5619
+ while (at !== -1) {
5620
+ count += 1;
5621
+ at = value.indexOf(needle, at + needle.length);
5622
+ }
5623
+ return count;
5624
+ }
5625
+ function pathFromStdout(stdout, shell) {
5626
+ const begins = countOccurrences(stdout, SHELL_PATH_BEGIN);
5627
+ const ends = countOccurrences(stdout, SHELL_PATH_END);
5628
+ if (begins === 0 || ends === 0) {
5629
+ return { ok: false, reason: "empty", shell };
5630
+ }
5631
+ if (begins > 1 || ends > 1) {
5632
+ return {
5633
+ ok: false,
5634
+ reason: "unparsable",
5635
+ shell,
5636
+ detail: `${begins} BEGIN and ${ends} END sentinels`
5637
+ };
5638
+ }
5639
+ const start = stdout.indexOf(SHELL_PATH_BEGIN) + SHELL_PATH_BEGIN.length;
5640
+ const path2 = stdout.slice(start, stdout.indexOf(SHELL_PATH_END));
5641
+ if (path2 === "") return { ok: false, reason: "empty", shell };
5642
+ return { ok: true, path: path2, shell };
5643
+ }
5644
+ function pathFromError(error, shell) {
5645
+ const code = error?.code;
5646
+ const detail = error instanceof Error ? error.message : String(error);
5647
+ if (code === "ENOENT") return { ok: false, reason: "missing", shell, detail };
5648
+ if (code === "EACCES" || code === "EPERM")
5649
+ return { ok: false, reason: "not-executable", shell, detail };
5650
+ return { ok: false, reason: "failed", shell, detail };
5651
+ }
5652
+ async function resolveLoginShellPath(options = {}) {
5653
+ const platform = options.platform ?? process.platform;
5654
+ if (platform !== "darwin" && platform !== "linux") {
5655
+ return { ok: false, reason: "unsupported-platform", shell: null };
5656
+ }
5657
+ const env = options.env ?? process.env;
5658
+ const run = options.run ?? shellPathRunner();
5659
+ const timeoutMs = options.timeoutMs ?? SHELL_PATH_TIMEOUT_MS;
5660
+ const shell = defaultShell(env, { platform });
5661
+ if (!isPosixShell(shell)) {
5662
+ return { ok: false, reason: "unsupported-shell", shell };
5663
+ }
5664
+ const attempt = run({
5665
+ shell,
5666
+ args: SHELL_PATH_ARGS,
5667
+ env,
5668
+ timeoutMs
5669
+ }).then(
5670
+ (result) => (
5671
+ // A run the runner cut short at the bound is a timeout even when the pipe
5672
+ // holds a complete-looking value: a shell killed mid-rc has a PATH that is
5673
+ // whatever the file had reached, which is not the terminal's PATH and is
5674
+ // not worth preferring to the one the app already had.
5675
+ result.timedOut ? { ok: false, reason: "timeout", shell } : pathFromStdout(result.stdout ?? "", shell)
5676
+ ),
5677
+ (error) => pathFromError(error, shell)
5678
+ );
5679
+ let timer;
5680
+ const expired = new Promise((resolve) => {
5681
+ timer = setTimeout(
5682
+ () => resolve("timeout"),
5683
+ timeoutMs + SHELL_PATH_ANSWER_GRACE_MS
5684
+ );
5685
+ });
5686
+ try {
5687
+ const settled = await Promise.race([attempt, expired]);
5688
+ if (settled === "timeout") return { ok: false, reason: "timeout", shell };
5689
+ return settled;
5690
+ } finally {
5691
+ if (timer) clearTimeout(timer);
5692
+ }
5693
+ }
5694
+ const USER_SHELL_PATH_MAX_ATTEMPTS = 3;
5695
+ function createUserShellPath(options = {}) {
5696
+ let answer = null;
5697
+ let pending = null;
5698
+ let attempts = 0;
5699
+ return {
5700
+ resolve() {
5701
+ if (answer) return Promise.resolve(answer);
5702
+ if (pending) return pending;
5703
+ if (attempts >= USER_SHELL_PATH_MAX_ATTEMPTS)
5704
+ return Promise.resolve(null);
5705
+ attempts += 1;
5706
+ const attempt = attempts;
5707
+ pending = resolveLoginShellPath(options).then((result) => {
5708
+ try {
5709
+ if (result.ok) {
5710
+ options.log?.(
5711
+ `[shell-path] ${result.shell} reports PATH: ${result.path}`
5712
+ );
5713
+ } else {
5714
+ options.log?.(
5715
+ `[shell-path] attempt ${attempt}/${USER_SHELL_PATH_MAX_ATTEMPTS}: ${result.shell ?? "no shell"} could not report a PATH (${result.reason}${result.detail ? `: ${result.detail}` : ""}); ${attempt < USER_SHELL_PATH_MAX_ATTEMPTS ? "a later ask will retry" : "the PATH this app was launched with is used instead"}`
5716
+ );
5717
+ }
5718
+ } catch {
5719
+ }
5720
+ if (result.ok) answer = result.path;
5721
+ return answer;
5722
+ }).finally(() => {
5723
+ pending = null;
5724
+ });
5725
+ return pending;
5726
+ }
5727
+ };
5728
+ }
5729
+ function mergeShellPath(resolved, base) {
5730
+ if (!base) return resolved;
5731
+ const seen = /* @__PURE__ */ new Set();
5732
+ const merged = [];
5733
+ for (const entry of [...resolved.split(":"), ...base.split(":")]) {
5734
+ if (seen.has(entry)) continue;
5735
+ seen.add(entry);
5736
+ merged.push(entry);
5737
+ }
5738
+ return merged.join(":");
5739
+ }
5740
+ async function withUserShellPath(env, userShellPath2) {
5741
+ const path2 = await userShellPath2?.resolve();
5742
+ if (!path2) return { ...env };
5743
+ return { ...env, PATH: mergeShellPath(path2, env.PATH) };
5744
+ }
5543
5745
  const launchEnv = {
5544
5746
  ...process.env
5545
5747
  };
@@ -6128,8 +6330,8 @@ const RUNTIME_ID = /^[a-f0-9]{64}$/;
6128
6330
  const PYVENV_HOME$1 = /^home\s*=\s*(.+)$/m;
6129
6331
  const LOCK_OWNER = /^\s*\d+\s*$/;
6130
6332
  const LEGACY_BUNDLE_PYTHON_PATH = /\.app\/Contents\/Resources\/python(?:_aarch64)?\//;
6131
- const PYTHON_SEED_NAMESPACE = BUNDLED_PYTHON_LAYOUT.seedNamespace;
6132
- const MACH_O_MAGICS = new Set(BUNDLED_PYTHON_LAYOUT.machOMagics);
6333
+ const PYTHON_SEED_NAMESPACE = RUNTIME_LAYOUT.python.seedNamespace;
6334
+ const MACH_O_MAGICS = new Set(RUNTIME_LAYOUT.machOMagics);
6133
6335
  const FORMAT = 1;
6134
6336
  const READY = "environment-ready.json";
6135
6337
  const POINTER = "selected-environment.json";
@@ -6226,7 +6428,7 @@ function managedPythonRoot(options) {
6226
6428
  function seedPath(options) {
6227
6429
  return options.packaged ? path$1.join(options.resources, PYTHON_SEED_NAMESPACE, options.arch) : path$1.join(
6228
6430
  options.resources,
6229
- BUNDLED_PYTHON_LAYOUT.checkoutSeedNames[options.arch]
6431
+ RUNTIME_LAYOUT.python.checkoutSeedNames[options.arch]
6230
6432
  );
6231
6433
  }
6232
6434
  function pythonPath(runtime) {
@@ -6536,13 +6738,13 @@ paths += [getattr(m, '__file__', '') for m in (_ssl, _sqlite3)]
6536
6738
  assert all('.app/' not in os.path.realpath(p).lower() for p in paths if p), paths
6537
6739
  print(json.dumps({'base_executable': sys._base_executable, 'base_prefix': sys.base_prefix, 'paths': paths}))`;
6538
6740
  async function smokeEnvironment(venv, env) {
6539
- const python = path$1.join(venv, "bin", "python");
6540
- await execute(python, ["-I", "-B", "-c", IMPORT_PROBE], {
6741
+ const python2 = path$1.join(venv, "bin", "python");
6742
+ await execute(python2, ["-I", "-B", "-c", IMPORT_PROBE], {
6541
6743
  env,
6542
6744
  timeout: 3e4
6543
6745
  });
6544
6746
  const { stdout: version } = await execute(
6545
- python,
6747
+ python2,
6546
6748
  [
6547
6749
  "-I",
6548
6750
  "-B",
@@ -7156,6 +7358,16 @@ class BackendServiceManager {
7156
7358
  /** Consumers of status changes in main (the window's IPC sender). */
7157
7359
  statusObserver = null;
7158
7360
  shellEnv = {};
7361
+ /**
7362
+ * The user's own login-shell PATH, resolved once for the whole app.
7363
+ *
7364
+ * Handed in by `src/main/index.ts` rather than constructed here, for one
7365
+ * reason: the console host is given the SAME instance, so "what is this user's
7366
+ * PATH" has one answer in this process instead of one per consumer. Absent -
7367
+ * every test and rig that builds a manager directly - means this instance keeps
7368
+ * the launch environment's PATH, which is exactly what it did before.
7369
+ */
7370
+ userShellPath;
7159
7371
  // External/dev backends may be explicitly paired through main's environment.
7160
7372
  // Managed starts always rotate this; it is never exposed by preload or logs.
7161
7373
  /**
@@ -7468,8 +7680,13 @@ class BackendServiceManager {
7468
7680
  // Configurable timeouts for different shutdown scenarios
7469
7681
  /**
7470
7682
  * Constructor
7683
+ *
7684
+ * @param options.userShellPath the app's one login-shell PATH resolver (see
7685
+ * `../shell-path`). Optional so a manager built without the app - a test, a rig
7686
+ * - behaves exactly as it did before this option existed.
7471
7687
  */
7472
- constructor() {
7688
+ constructor(options = {}) {
7689
+ this.userShellPath = options.userShellPath;
7473
7690
  try {
7474
7691
  const apiUrl = new URL(backendConfig.VITE_LOCAL_OPERATOR_API_URL);
7475
7692
  this.port = Number.parseInt(apiUrl.port, 10) || 1111;
@@ -7526,11 +7743,12 @@ class BackendServiceManager {
7526
7743
  LogFileType.BACKEND
7527
7744
  );
7528
7745
  }
7746
+ this.shellEnv = withPythonBytecodeCache(this.shellEnv, this.appDataPath);
7747
+ await this.settleUserShellPath();
7529
7748
  logger.info(
7530
7749
  `Shell environment variables loaded successfully. PATH: ${this.shellEnv.PATH || this.shellEnv.Path || "(not set)"}`,
7531
7750
  LogFileType.BACKEND
7532
7751
  );
7533
- this.shellEnv = withPythonBytecodeCache(this.shellEnv, this.appDataPath);
7534
7752
  } catch (error) {
7535
7753
  logger.error(
7536
7754
  "Error loading shell environment variables:",
@@ -7539,6 +7757,24 @@ class BackendServiceManager {
7539
7757
  );
7540
7758
  }
7541
7759
  }
7760
+ /**
7761
+ * Fold the user's login-shell PATH into `shellEnv`, when the app handed one in.
7762
+ *
7763
+ * Awaited at BOTH of its call sites, unlike the other enrichments the spawn
7764
+ * environment carries, because unlike them this one IS the value being fixed: a
7765
+ * fire-and-forget resolution is the defect it exists to remove, and the first
7766
+ * spawn would keep the launchd PATH on exactly the machines whose rc is slowest.
7767
+ * `resolve()` is memoized, so the second caller - and the console host, which
7768
+ * holds the same resolver - is answered from the first shell's result rather
7769
+ * than starting one.
7770
+ *
7771
+ * A no-op without a resolver: a manager a test or rig builds directly keeps the
7772
+ * launch environment's PATH exactly as it did before.
7773
+ */
7774
+ async settleUserShellPath() {
7775
+ if (!this.userShellPath) return;
7776
+ this.shellEnv = await withUserShellPath(this.shellEnv, this.userShellPath);
7777
+ }
7542
7778
  /**
7543
7779
  * The environment the backend process is spawned with.
7544
7780
  *
@@ -7552,6 +7788,14 @@ class BackendServiceManager {
7552
7788
  * it here makes the guarantee structural instead of ordering-dependent, so
7553
7789
  * no spawn path can miss it.
7554
7790
  *
7791
+ * PATH is the one term of that race this method no longer has to carry: its
7792
+ * resolver is awaited before the first spawn (`settleUserShellPath`), so a slow
7793
+ * rc delays the start rather than quietly leaving the launchd PATH in place.
7794
+ * The prefix and the kill switch stay here all the same, because the seed they
7795
+ * are read from is `process.env` itself - an exported `PYTHONPYCACHEPREFIX`
7796
+ * inside the bundle, or a `.env` folded over the launch - and no shell
7797
+ * resolution of PATH has anything to say about either.
7798
+ *
7555
7799
  * These spawns are the ones whose `python` is the interpreter we ship:
7556
7800
  * CPython writes `__pycache__/*.pyc` beside the sources it imports, those
7557
7801
  * sources are inside the code-sealed `.app`, and every such write breaks the
@@ -8388,6 +8632,7 @@ class BackendServiceManager {
8388
8632
  packaged: electron.app.isPackaged
8389
8633
  });
8390
8634
  const globalInstall = await this.checkLocalOperatorExists();
8635
+ await this.settleUserShellPath();
8391
8636
  const env = this.backendSpawnEnv();
8392
8637
  let interpreters;
8393
8638
  if (globalInstall) {
@@ -9051,7 +9296,12 @@ class BackendServiceManager {
9051
9296
  * daemon that might have been mid-restart, busy with someone else's turn, or
9052
9297
  * serving another app - and, for an external daemon, adopted it as "ours" and
9053
9298
  * started a second one. Neither is possible here: `degraded` never starts
9054
- * anything, and only `detached` reaches {@link recoverFromDetachment}.
9299
+ * anything, and {@link recoverFromDetachment} is reached only from the facts
9300
+ * the state machine has already established - a `detached`/`wedged`
9301
+ * connection, a pid its liveness probe read as gone (EVIDENCE, not a sample),
9302
+ * and, since the 2026-09-21 change below, a pairing record that says the
9303
+ * pairing is broken with a cause a sweep could repair, even while the
9304
+ * connection still reads `attached`.
9055
9305
  */
9056
9306
  async checkBackendHealth() {
9057
9307
  if (this.isAppClosing) return;
@@ -9084,6 +9334,11 @@ class BackendServiceManager {
9084
9334
  LogFileType.BACKEND
9085
9335
  );
9086
9336
  await this.recoverFromDetachment();
9337
+ return;
9338
+ }
9339
+ const pairing = this.daemonState.snapshot().pairing;
9340
+ if (!pairing.available && pairingHasRemedy(pairing.cause)) {
9341
+ await this.recoverFromDetachment();
9087
9342
  }
9088
9343
  }
9089
9344
  /**
@@ -9105,6 +9360,31 @@ class BackendServiceManager {
9105
9360
  await this.recoverFromDetachment();
9106
9361
  return this.getStatusSnapshot();
9107
9362
  }
9363
+ /**
9364
+ * Whether a broken pairing main knows about is one a sweep could repair, and
9365
+ * is therefore a subject for the reattach backoff.
9366
+ *
9367
+ * WHY the pacing needs this second clause at all. The identity gate above is
9368
+ * `expectedInstanceId() !== null`, and the pairing record can be broken while
9369
+ * NO identity is held: `attachIfUsable`'s wrong-key arm (and its refused arm)
9370
+ * records the cause and returns WITHOUT attaching, so the old gate has nothing
9371
+ * to pace against. That state IS reachable in production, because
9372
+ * `checkExistingBackend()` drives that pass and a later `start()` calls
9373
+ * `discoverAndAttach()` again before it spawns, so the loop can be armed with
9374
+ * the app `connecting` and a cause already recorded. Without this clause such
9375
+ * a pairing would re-probe, sweep discovery and re-claim on every 10 s tick.
9376
+ *
9377
+ * THE EXCLUSION: `unpaired` is the control flow's own initial value - "the
9378
+ * cause is not yet established" (design § 2, S5) - and not an observation that
9379
+ * a pairing broke. It is what `attachIfUsable` records while the app is
9380
+ * `connecting` and has not attached yet, which is a FIRST RUN rather than a
9381
+ * break, so a first run with no daemon at all stays unpaced; every cause a
9382
+ * pass actually observed is paced.
9383
+ */
9384
+ pairingBreaksPacing() {
9385
+ const pairing = this.daemonState.snapshot().pairing;
9386
+ return !pairing.available && pairing.cause !== null && pairing.cause !== "unpaired" && pairingHasRemedy(pairing.cause);
9387
+ }
9108
9388
  /**
9109
9389
  * Recover from a lost daemon - by RE-DISCOVERING, and only then by starting
9110
9390
  * one.
@@ -9118,7 +9398,7 @@ class BackendServiceManager {
9118
9398
  async recoverFromDetachment() {
9119
9399
  if (this.isAppClosing || this.isAutoUpdating || this.recoveryInFlight)
9120
9400
  return;
9121
- if (this.daemonState.expectedInstanceId() !== null) {
9401
+ if (this.daemonState.expectedInstanceId() !== null || this.pairingBreaksPacing()) {
9122
9402
  const now = Date.now();
9123
9403
  if (now < this.nextRecoveryAt) return;
9124
9404
  this.nextRecoveryAt = now + this.daemonState.nextBackoff();
@@ -9490,338 +9770,9 @@ function managedPythonOptions() {
9490
9770
  arch: process.arch
9491
9771
  };
9492
9772
  }
9493
- const linuxInstallScriptRaw = '#!/bin/bash\n# Local Operator Backend Installation Script for Linux\n# This script sets up a virtual environment for the Local Operator backend\n# without requiring sudo or admin privileges.\n\n# Exit on error, undefined variables, and failed pipe commands\nset -e # Exit immediately if a command exits with a non-zero status\nset -u # Error on unset variables\nset -o pipefail # Fail if any command in a pipe fails\n\n# Configuration\n# Variables APP_NAME and MIN_PYTHON_VERSION removed as they were unused\nVENV_NAME="local-operator-venv"\nAPP_DATA_DIR="${HOME}/.config/local-operator"\nVENV_PATH="${APP_DATA_DIR}/${VENV_NAME}"\nLOG_FILE="${APP_DATA_DIR}/backend-install.log"\nTIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")\n\n# Which environment this installs into - the app\'s decision, handed in. See the\n# macOS script for why the script must not re-derive it (a packaged and an\n# unpackaged instance use different environments, and only the app knows which\n# it is); `LOCAL_OPERATOR_VENV_PATH` is set from `managedVenvPath`.\n: "${LOCAL_OPERATOR_VENV_PATH:=$VENV_PATH}"\nVENV_PATH="$LOCAL_OPERATOR_VENV_PATH"\n\n# Keep CPython\'s bytecode cache out of the application directory.\n#\n# Same reason as the macOS script: an interpreter writing __pycache__/*.pyc\n# beside its own stdlib sources writes into the installed tree. Linux does not\n# code-seal the app the way macOS does, so this is uniformity and hygiene here\n# rather than the load-bearing fix it is on macOS - but it is also the value a\n# standalone run of this script has to agree with, because the app and the\n# script must not disagree about where bytecode goes.\n: "${PYTHONPYCACHEPREFIX:=${APP_DATA_DIR}/python-bytecode-cache}"\nexport PYTHONPYCACHEPREFIX\n\n# The refusal half of the pair, set rather than merged with whatever the caller\n# had: CPython reads the flag before its first import, so nothing this script\n# runs - `-m venv`, pip, the venv they create - can write a `__pycache__` at\n# all. Kept in step with the app\'s `withPythonBytecodeCache`, which sets both\n# variables on every python it spawns.\nexport PYTHONDONTWRITEBYTECODE=1\n\n# Create app data directory if it doesn\'t exist\nif ! mkdir -p "${APP_DATA_DIR}"; then\n echo "ERROR: Unable to create app data directory at ${APP_DATA_DIR}"\n echo "Please check permissions and try again."\n exit 1\nfi\n\n# Handle cleanup on script exit\ncleanup() {\n # Remove any temporary files created during execution\n if [ -f "${APP_DATA_DIR}/get-pip.py" ]; then\n rm -f "${APP_DATA_DIR}/get-pip.py"\n fi\n if [ -n "${FFMPEG_TEMP_ARCHIVE:-}" ] && [ -f "${FFMPEG_TEMP_ARCHIVE}" ]; then\n log "Removing temporary FFmpeg archive: ${FFMPEG_TEMP_ARCHIVE}"\n rm -f "${FFMPEG_TEMP_ARCHIVE}"\n fi\n echo "$(date): Cleanup completed."\n}\n\n# Set trap only for manual interruption (SIGINT, SIGTERM), not normal exits\ntrap cleanup SIGINT SIGTERM\n\n\n# Start logging\nexec > >(tee -a "${LOG_FILE}") 2>&1\necho "[${TIMESTAMP}]: Starting Local Operator backend installation..."\necho "[${TIMESTAMP}]: System information: $(uname -a)"\n\n# Function to log messages with timestamp (defined later, but used by FFmpeg section)\n_log_internal() {\n local internal_timestamp\n internal_timestamp=$(date +"%Y-%m-%d %H:%M:%S")\n echo "[${internal_timestamp}] $1"\n}\n\n# --- FFmpeg Installation ---\nBIN_DIR="${APP_DATA_DIR}/bin"\nFFMPEG_BIN="${BIN_DIR}/ffmpeg" # Path to ffmpeg binary\n\n_log_internal "Ensuring bin directory exists: ${BIN_DIR}"\nif ! mkdir -p "${BIN_DIR}"; then\n _log_internal "ERROR: Unable to create bin directory at ${BIN_DIR}"\n exit 1\nfi\n\n# Check if FFmpeg is already installed and executable\nif [ -f "${FFMPEG_BIN}" ] && [ -x "${FFMPEG_BIN}" ]; then\n _log_internal "FFmpeg already installed at ${FFMPEG_BIN}. Skipping download."\nelse\n _log_internal "FFmpeg not found or not executable. Attempting to download and install FFmpeg..."\n\n FFMPEG_DOWNLOAD_URL=""\n\n LINUX_ARCH=$(uname -m)\n if [[ "${LINUX_ARCH}" == "x86_64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-x64"\n elif [[ "${LINUX_ARCH}" == "aarch64" ]] || [[ "${LINUX_ARCH}" == "arm64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-arm64"\n else\n _log_internal "Error: Unsupported CPU architecture for FFmpeg download: ${LINUX_ARCH}"\n exit 1\n fi\n\n _log_internal "Downloading FFmpeg from: ${FFMPEG_DOWNLOAD_URL}"\n if command -v curl >/dev/null 2>&1; then\n if ! curl -L "${FFMPEG_DOWNLOAD_URL}" -o "${FFMPEG_BIN}"; then\n _log_internal "Error: Failed to download FFmpeg using curl from ${FFMPEG_DOWNLOAD_URL}"\n exit 1\n fi\n elif command -v wget >/dev/null 2>&1; then\n if ! wget -O "${FFMPEG_BIN}" "${FFMPEG_DOWNLOAD_URL}"; then\n _log_internal "Error: Failed to download FFmpeg using wget from ${FFMPEG_DOWNLOAD_URL}"\n exit 1\n fi\n else\n _log_internal "Error: Neither curl nor wget found. Cannot download FFmpeg."\n exit 1\n fi\n _log_internal "FFmpeg binary downloaded successfully to ${FFMPEG_BIN}"\n\n chmod +x "${FFMPEG_BIN}"\n _log_internal "Set executable permissions for ${FFMPEG_BIN}"\n\n # Verify FFmpeg is executable after download\n if [ ! -f "${FFMPEG_BIN}" ] || [ ! -x "${FFMPEG_BIN}" ]; then\n _log_internal "Error: FFmpeg binary not found or not executable after download."\n exit 1\n fi\nfi\n\n_log_internal "FFmpeg installation complete. FFmpeg binary is at: ${FFMPEG_BIN}"\n\n# Function to check if a command exists\ncommand_exists() {\n command -v "$1" >/dev/null 2>&1\n}\n\n# Function to log messages with timestamp\nlog() {\n local timestamp\n timestamp=$(date +"%Y-%m-%d %H:%M:%S")\n echo "[${timestamp}] $1"\n}\n\n# Function to display error messages and exit\nerror_exit() {\n log "ERROR: $1" >&2\n exit "${2:-1}"\n}\n\n# Function to check if Python binary is valid for current architecture\nis_valid_python_binary() {\n local python_path="$1"\n \n # First check if file exists and is executable\n if [ ! -f "${python_path}" ] || [ ! -x "${python_path}" ]; then\n return 1\n fi\n \n # Try to run a simple Python command to verify it works on current architecture\n "${python_path}" -c "print(\'Testing Python executable\')" >/dev/null 2>&1\n return $?\n}\n\n# Check for network connectivity to key servers\ncheck_connectivity() {\n log "Checking network connectivity..."\n local servers=("pypi.org" "bootstrap.pypa.io")\n local has_connectivity=false\n \n for server in "${servers[@]}"; do\n if command_exists curl; then\n if curl --connect-timeout 5 -s "https://${server}" -o /dev/null; then\n has_connectivity=true\n break\n fi\n elif command_exists wget; then\n if wget --timeout=5 -q --spider "https://${server}"; then\n has_connectivity=true\n break\n fi\n fi\n done\n \n if [ "$has_connectivity" = false ]; then\n log "WARNING: Network connectivity issues detected. This may affect installation."\n else\n log "Network connectivity confirmed."\n fi\n}\n\n# Call connectivity check\ncheck_connectivity\n\n# Check if PYTHON_BIN is already set by the installer\nif [ -n "${PYTHON_BIN:-}" ]; then\n log "Using Python executable provided by installer: ${PYTHON_BIN}"\n # Verify the provided Python binary works on this architecture\n if ! is_valid_python_binary "${PYTHON_BIN}"; then\n log "Warning: The provided Python binary is not compatible with this system architecture."\n log "Will attempt to find a system Python installation instead."\n PYTHON_BIN=""\n fi\nfi\n\n# If PYTHON_BIN is empty or invalid, try to find a suitable Python installation\nif [ -z "${PYTHON_BIN:-}" ] || ! is_valid_python_binary "${PYTHON_BIN:-}"; then\n # Try to find a suitable Python installation\n log "Looking for a suitable Python installation..."\n \n # Try multiple possible locations to find Python\n POSSIBLE_PYTHON_PATHS=(\n # System paths first (more likely to be compatible with current architecture)\n "/usr/local/bin/python3.12"\n "/usr/bin/python3.12"\n "/usr/local/bin/python3"\n "/usr/bin/python3"\n # From environment variable (set by the installer)\n "${ELECTRON_RESOURCE_PATH:-}/python/bin/python3"\n # Development paths\n "$(dirname "$0")/../../../resources/python/bin/python3"\n "$(pwd)/resources/python/bin/python3"\n )\n\n # Find the first Python that exists and is version 3.12+ and works on this architecture\n PYTHON_BIN=""\n for path in "${POSSIBLE_PYTHON_PATHS[@]}"; do\n # Skip empty paths that might come from unset environment variables\n if [ -z "${path}" ]; then\n continue\n fi\n \n if is_valid_python_binary "${path}"; then\n # Check Python version\n if PY_VERSION=$("${path}" -c "import sys; print(f\'{sys.version_info.major}.{sys.version_info.minor}\')" 2>/dev/null); then\n MAJOR=$(echo "${PY_VERSION}" | cut -d. -f1)\n MINOR=$(echo "${PY_VERSION}" | cut -d. -f2)\n if [ "${MAJOR}" -eq 3 ] && [ "${MINOR}" -ge 12 ]; then\n PYTHON_BIN="${path}"\n log "Found suitable Python ${PY_VERSION} at ${path}"\n break\n else\n log "Python at ${path} is version ${PY_VERSION}, which is below the required 3.12+"\n fi\n fi\n else\n if [ -f "${path}" ]; then\n log "Python at ${path} exists but is not compatible with this system architecture"\n fi\n fi\n done\n\n # If we couldn\'t find a suitable Python, exit with error\n if [ -z "${PYTHON_BIN:-}" ]; then\n error_exit "No suitable Python installation found (version 3.12 or higher required).\\nPlease install Python 3.12 or higher before running this script.\\nYou can install Python from https://www.python.org/downloads/"\n fi\nfi\n\necho "Using Python: $PYTHON_BIN"\n"$PYTHON_BIN" --version || {\n echo "Error: Failed to run Python. Please ensure it is executable and accessible."\n echo "System architecture: $(uname -m)"\n echo "Python binary architecture: $(file "$PYTHON_BIN" 2>/dev/null || echo \'Unable to determine\')"\n exit 1\n}\n\n# Detect the Linux distribution to provide specific instructions\nif command_exists lsb_release; then\n DISTRO=$(lsb_release -is)\n DISTRO_VERSION=$(lsb_release -rs)\n echo "Detected Linux distribution: $DISTRO $DISTRO_VERSION"\nelif [ -f /etc/os-release ]; then\n DISTRO=$(grep -oP \'(?<=^ID=).+\' /etc/os-release | tr -d \'"\')\n DISTRO_VERSION=$(grep -oP \'(?<=^VERSION_ID=).+\' /etc/os-release | tr -d \'"\')\n echo "Detected Linux distribution: $DISTRO $DISTRO_VERSION"\nelse\n DISTRO="unknown"\n echo "Unable to detect Linux distribution"\nfi\n\n# Check if Python has venv module and ensurepip available\nlog "Checking for venv module and ensurepip availability..."\nVENV_AVAILABLE=0\nENSUREPIP_AVAILABLE=0\n\n"${PYTHON_BIN}" -c "import venv" > /dev/null 2>&1 || VENV_AVAILABLE=1\n"${PYTHON_BIN}" -c "import ensurepip" > /dev/null 2>&1 || ENSUREPIP_AVAILABLE=1\n\n# If venv is still not available, try to use virtualenv as a fallback\nif [ $VENV_AVAILABLE -ne 0 ]; then\n echo "venv module is not available. Checking for virtualenv as an alternative..."\n \n # Check if virtualenv is already installed\n if "$PYTHON_BIN" -c "import virtualenv" > /dev/null 2>&1; then\n echo "virtualenv is available, will use it instead of venv"\n else\n echo "ERROR: Neither venv nor virtualenv is available."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n exit 1\n fi\nfi\n\n# Create virtual environment if it doesn\'t exist\nif [ ! -d "$VENV_PATH" ]; then\n echo "Creating virtual environment at $VENV_PATH..."\n # Remove any potentially corrupted virtual environment\n if [ -e "$VENV_PATH" ]; then\n echo "Removing existing but potentially corrupted venv directory..."\n rm -rf "$VENV_PATH"\n fi\n \n # Make sure parent directory exists and is writable\n mkdir -p "$(dirname "$VENV_PATH")"\n \n # Try different methods to create a virtual environment\n VENV_CREATE_STATUS=1\n \n # First try with venv if available\n if [ $VENV_AVAILABLE -eq 0 ] && [ $ENSUREPIP_AVAILABLE -eq 0 ]; then\n echo "Creating virtual environment using venv module..."\n "$PYTHON_BIN" -m venv "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n fi\n \n # If venv failed, try virtualenv\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n echo "venv creation failed with status $VENV_CREATE_STATUS. Falling back to virtualenv..."\n if command_exists virtualenv; then\n virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n elif "$PYTHON_BIN" -m pip list | grep -q virtualenv; then\n "$PYTHON_BIN" -m virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n else\n echo "ERROR: Both venv and virtualenv are unavailable or failed."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n echo "You can install Python 3.12 from https://www.python.org/downloads/"\n exit 1\n fi\n fi\n \n # Check if virtual environment creation was successful\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n log "ERROR: Failed to create virtual environment. Exit code: $VENV_CREATE_STATUS"\n log "Virtual environment path: $VENV_PATH"\n log "Python binary used: $PYTHON_BIN"\n ls -la "$(dirname "$VENV_PATH")" || true\n log "Python executable permissions:"\n ls -la "$PYTHON_BIN" || true\n \n # Check for common issues\n log "Checking for common virtual environment creation issues..."\n \n # Check disk space\n df -h "$(dirname "$VENV_PATH")" || true\n \n # Check if directory is writable\n if [ ! -w "$(dirname "$VENV_PATH")" ]; then\n log "ERROR: Directory $(dirname "$VENV_PATH") is not writable"\n fi\n \n # Try to create a minimal virtual environment manually as a last resort\n log "Attempting to create a minimal virtual environment manually..."\n mkdir -p "$VENV_PATH/bin" || error_exit "Could not create directory $VENV_PATH/bin"\n echo "#!/bin/bash" > "$VENV_PATH/bin/activate"\n echo "export VIRTUAL_ENV=\\"$VENV_PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "export PATH=\\"$VENV_PATH/bin:\\$PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "unset PYTHONHOME" >> "$VENV_PATH/bin/activate"\n chmod +x "$VENV_PATH/bin/activate" || error_exit "Could not set execute permissions on activate script"\n \n # Create symlinks to the system Python\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python" || log "WARNING: Failed to create symlink for python"\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python3" || log "WARNING: Failed to create symlink for python3"\n \n # Try to bootstrap pip\n log "Bootstrapping pip in the minimal virtual environment..."\n if command_exists curl; then\n curl -s https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py" || log "WARNING: Failed to download get-pip.py"\n elif command_exists wget; then\n wget -q -O "$APP_DATA_DIR/get-pip.py" https://bootstrap.pypa.io/get-pip.py || log "WARNING: Failed to download get-pip.py"\n else\n log "ERROR: Neither curl nor wget available to download get-pip.py"\n error_exit "Installation cannot continue without being able to download pip"\n fi\n \n # shellcheck disable=SC1090\n source "$VENV_PATH/bin/activate" && python "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n error_exit "Failed to create even a minimal virtual environment."\n else\n log "Created a minimal virtual environment as a fallback."\n fi\n else\n log "Successfully created virtual environment"\n fi\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ]; then\n echo "Python executable missing in virtual environment, creating symlink..."\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python"\nfi\n\nif [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "pip missing in virtual environment, attempting to bootstrap it..."\n curl -s https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py"\n "$VENV_PATH/bin/python" "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Failed to bootstrap pip in the virtual environment"\n exit 1\n fi\nfi\n\necho "Virtual environment structure verified"\n\n# Activate virtual environment and install local-operator\necho "Installing local-operator in virtual environment..."\nsource "$VENV_PATH/bin/activate"\n\necho "Upgrading pip..."\npython -m pip install --upgrade pip || {\n echo "WARNING: Failed to upgrade pip. Will try to continue with existing pip version."\n pip --version\n}\n\n# Check network connectivity to PyPI\necho "Checking network connectivity to PyPI..."\nif command_exists curl; then\n curl -s https://pypi.org/pypi/local-operator/json -o /dev/null || {\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n }\nelif command_exists wget; then\n wget -q --spider https://pypi.org/pypi/local-operator/json || {\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n }\nfi\n\necho "Installing local-operator package..."\npython -m pip install --verbose local-operator || {\n echo "ERROR: Failed to install local-operator package. Exit code: $?"\n echo "Python version:"\n python --version\n echo "pip version:"\n pip --version\n echo "Available pip packages:"\n pip list\n exit 1\n}\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n log "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator" || true\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n \n # Try to run the version command with full error output\n log "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n log "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n log "Binary details:"\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n log "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator" || true\n log ""\n log "This could be due to missing dependencies or architecture incompatibility."\n log "Try running the binary manually to see specific errors."\n \n # Don\'t exit here - just warn the user\n log "WARNING: Installation completed but binary verification failed."\n }\n \n # Create a summary of the installation\n SUMMARY_FILE="${APP_DATA_DIR}/installation-summary-${TIMESTAMP}.txt"\n {\n echo "=== Local Operator Backend Installation Summary ==="\n echo "Timestamp: $(date)"\n echo "Python Version: $("${PYTHON_BIN}" --version 2>&1)"\n echo "Virtual Environment: ${VENV_PATH}"\n echo "Binary Location: ${VENV_PATH}/bin/local-operator"\n echo "To activate the virtual environment, run: source ${VENV_PATH}/bin/activate"\n echo "To test the installation, run: ${VENV_PATH}/bin/local-operator --version"\n echo "Log file: ${LOG_FILE}"\n echo "=================================================="\n } > "${SUMMARY_FILE}"\n \n log "Installation summary saved to: ${SUMMARY_FILE}"\nelse\n log "Error: Failed to install Local Operator backend. Binary not found."\n log "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/" || true\n log "Check the log file for details: ${LOG_FILE}"\n error_exit "Installation failed. Binary not installed properly."\nfi\n\nlog "Installation completed successfully at $(date)"\n\n# Clean up temporary files now that installation is complete\ncleanup\n\n# Print final instructions\nlog ""\nlog "=== INSTALLATION COMPLETE ==="\nlog "You can now use the Local Operator backend by running: ${VENV_PATH}/bin/local-operator"\nlog "Installation directory: ${VENV_PATH}"\nlog "Log file: ${LOG_FILE}"\nlog "================================"\n';
9494
- const macosInstallScriptRaw = '#!/bin/bash\n# Local Operator Backend Installation Script for macOS\n# This script uses the bundled standalone Python and sets up a virtual environment for the Local Operator backend.\n\nset -e # Exit immediately if a command exits with a non-zero status\n\n# Configuration\nAPP_NAME="Local Operator"\nVENV_NAME="local-operator-venv"\nAPP_DATA_DIR="${LOCAL_OPERATOR_SUPPORT_PATH:-$HOME/Library/Application Support/$APP_NAME}"\nVENV_PATH="$APP_DATA_DIR/$VENV_NAME"\nLOG_FILE="$APP_DATA_DIR/backend-install.log"\n\n# Which environment this installs into. The path is the app\'s decision, not this\n# script\'s: a packaged install and an unpackaged one must not share an\n# environment (the venv is built on whatever interpreter the instance resolves,\n# and the installed bundle\'s stdlib lives inside the code-sealed .app), and only\n# the app knows which one it is. The app passes its answer in\n# (`LOCAL_OPERATOR_VENV_PATH`, set from `managedVenvPath` in\n# src/main/backend/venv-paths.ts).\n#\n# macOS REFUSES a standalone run rather than falling back, and the asymmetry with\n# the Linux and Windows scripts is deliberate. Their default is the packaged name,\n# which is harmless there; here it is the exact environment this split exists to\n# stop a second instance from writing into - the measured failure is a dev-venv\n# interpreter whose stdlib is `/Applications/Local Operator.app/Contents/\n# Resources/python_aarch64`, so a silent default would rebuild that venv and\n# `pip install` into it, which is what R1 of the review caught. A caller that\n# cannot name the environment is a caller that should not be installing into one.\n: "${LOCAL_OPERATOR_VENV_PATH:?Pass the resolved managed environment path (see venv-paths.ts); this script will not guess which instance it belongs to}"\nVENV_PATH="$LOCAL_OPERATOR_VENV_PATH"\n\n# Keep CPython\'s bytecode cache out of the application bundle.\n#\n# The bundled interpreter writes __pycache__/*.pyc beside the stdlib sources it\n# imports, and those sources live inside the code-signed .app - which is the\n# build\'s own extraResource. Every such write is a change to a sealed resource:\n# measured on an installed 0.17.3, `codesign --verify --deep` reported 308\n# `file added:` violations, all of them .pyc, and ShipIt then refuses the\n# in-place update with -67028 errSecCSBadBundleFormat. A file codesign reports\n# as *added* can be deleted and the seal comes back; a *modified* one cannot,\n# which is why nothing may ship a .pyc at all. The app sets this variable when\n# it spawns us; defaulting it here keeps a standalone run of this script in\n# agreement with the app about where bytecode goes. It is not a complete answer:\n# the app does not spawn every python that runs this interpreter, and a python\n# started by something else - a shell, a CLI script, an agent - has no prefix at\n# all (measured: a venv over this tree wrote 25 .pyc into it that way). The half\n# that covers those is the app\'s seal on the tree itself\n# (src/main/python-bytecode-cache.ts), which a standalone run of this script does\n# not apply. $HOME is the right place for the cache for the same reason the venv\n# lives there: it is never inside the thing that gets signed and swapped.\n: "${PYTHONPYCACHEPREFIX:=$APP_DATA_DIR/python-bytecode-cache}"\nexport PYTHONPYCACHEPREFIX\n\n# And the refusal half of the same pair, so a standalone run of this script\n# cannot write bytecode into the bundle even where the redirect above does not\n# apply - a relative or unwritable prefix, or a child that drops the variable.\n# CPython reads the flag before its first import, so this script\'s own\n# `python -m venv`, its pip runs and the venv they create all compile without\n# writing a `__pycache__` anywhere. Measured on CPython: `0` and the empty\n# string are the two falsy spellings, so the value is set rather than merged\n# with whatever the caller had.\nexport PYTHONDONTWRITEBYTECODE=1\n\n# The architecture block that used to live here computed and logged the name of\n# a directory for an in-bundle search this script no longer performs: every run\n# announced which architecture-named directory it was about to use, and then\n# installed from `PYTHON_BIN` anyway - the line stated the opposite of how the\n# script finds Python, and the names it computed were used nowhere else (review\n# N1). Which interpreter to build the environment with is the caller\'s decision,\n# checked immediately below.\n\n# The app prepares a complete external runtime before invoking this script.\n# Searching /Applications here would reintroduce legacy-bundle execution during\n# migration. Standalone callers must make the same explicit path decision.\n: "${PYTHON_BIN:?Pass an external prepared Python executable}"\ncase "$PYTHON_BIN" in\n *.app/*) echo "Refusing to execute Python inside an application bundle" >&2; exit 1 ;;\nesac\n\n# Create app data directory if it doesn\'t exist\nmkdir -p "$APP_DATA_DIR"\n\n# Start logging - capture everything and ensure it\'s visible to parent process\nexec > >(tee -a "$LOG_FILE") 2> >(tee -a "$LOG_FILE" >&2)\necho "=============================================="\necho "$(date): Starting Local Operator backend installation..."\necho "=============================================="\necho "Python bin path: $PYTHON_BIN"\necho "Virtual environment path: $VENV_PATH"\necho "App data directory: $APP_DATA_DIR"\necho "Log file: $LOG_FILE"\necho "=============================================="\n\n# --- FFmpeg Installation ---\nBIN_DIR="$APP_DATA_DIR/bin"\nFFMPEG_BIN="$BIN_DIR/ffmpeg"\n\necho "Ensuring bin directory exists: $BIN_DIR"\nmkdir -p "$BIN_DIR"\n\n# Check if FFmpeg is already installed and executable\nif [ -f "$FFMPEG_BIN" ] && [ -x "$FFMPEG_BIN" ]; then\n echo "FFmpeg already installed at $FFMPEG_BIN. Skipping download."\nelse\n echo "FFmpeg not found or not executable. Attempting to download and install FFmpeg..."\n\n # The architecture, read here because this URL is the one place left that needs\n # it: the environment is built on `PYTHON_BIN`, handed in above, so nothing\n # else in this script derives anything from `uname -m`. The block that used to\n # compute it at the top of the file was removed with the in-bundle interpreter\n # search it existed for (review N1) - and that removal took this variable with\n # it while this block still read it, so `$ARCH` was empty for every caller,\n # including the app, and any install on a machine without a cached ffmpeg\n # exited 1 here, before the venv was ever created. Measured: `bash\n # src/main/backend/scripts/macos-install-script.sh` with `PYTHON_BIN` set stops\n # on "Unsupported CPU architecture for FFmpeg download:".\n ARCH=$(uname -m)\n\n FFMPEG_DOWNLOAD_URL=""\n\n if [[ "$ARCH" == "x86_64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-mac-x64"\n elif [[ "$ARCH" == "arm64" ]] || [[ "$ARCH" == "aarch64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-mac-arm64"\n else\n echo "Error: Unsupported CPU architecture for FFmpeg download: $ARCH"\n exit 1\n fi\n\n echo "Downloading FFmpeg from: $FFMPEG_DOWNLOAD_URL"\n if curl -L "$FFMPEG_DOWNLOAD_URL" -o "$FFMPEG_BIN"; then\n echo "FFmpeg downloaded successfully to $FFMPEG_BIN"\n chmod +x "$FFMPEG_BIN"\n echo "Set executable permissions for $FFMPEG_BIN"\n else\n echo "Error: Failed to download FFmpeg from $FFMPEG_DOWNLOAD_URL"\n exit 1\n fi\n\n # Verify FFmpeg is executable after download\n if [ ! -f "$FFMPEG_BIN" ] || [ ! -x "$FFMPEG_BIN" ]; then\n echo "Error: FFmpeg binary not found or not executable after download."\n exit 1\n fi\nfi\n\necho "FFmpeg installation complete. FFmpeg binary is at: $FFMPEG_BIN"\n# --- End FFmpeg Installation ---\n\n# Verify bundled Python exists\nif [ ! -f "$PYTHON_BIN" ]; then\n echo "Error: Bundled Python not found at $PYTHON_BIN"\n echo "Please ensure standalone Python is properly installed in the application resources."\n exit 1\nfi\n\n# Make sure Python binary is executable\necho "Using bundled Python: $PYTHON_BIN"\n"$PYTHON_BIN" --version\n\n# Check if Python has venv module available\necho "Checking for venv module availability..."\n"$PYTHON_BIN" -m venv --help > /dev/null 2>&1 || {\n echo "ERROR: Python venv module not available in the Python installation"\n echo "Python details:"\n "$PYTHON_BIN" --version\n "$PYTHON_BIN" -c "import sys; print(\'Prefix:\', sys.prefix); print(\'Exec Prefix:\', sys.exec_prefix)"\n "$PYTHON_BIN" -c "import sys; print(\'Modules path:\'); print(\'\\n\'.join(sys.path))"\n exit 1\n}\necho "venv module is available"\n\n# Create virtual environment if it doesn\'t exist\nif [ ! -d "$VENV_PATH" ]; then\n echo "Creating virtual environment at $VENV_PATH..."\n # Never repair a path we did not create. Preparation allocates a fresh final\n # pathname; a collision is evidence to preserve, not a reason to delete it.\n if [ -e "$VENV_PATH" ] || [ -L "$VENV_PATH" ]; then\n echo "Refusing to replace an existing environment path: $VENV_PATH" >&2\n exit 1\n fi\n \n # Make sure parent directory exists and is writable\n mkdir -p "$(dirname "$VENV_PATH")"\n \n # Create the virtual environment with verbosity\n "$PYTHON_BIN" -m venv "$VENV_PATH" || {\n echo "ERROR: Failed to create virtual environment. Exit code: $?"\n echo "Virtual environment path: $VENV_PATH"\n echo "Python binary used: $PYTHON_BIN"\n ls -la "$(dirname "$VENV_PATH")"\n echo "Python executable permissions:"\n ls -la "$PYTHON_BIN"\n exit 1\n }\n echo "Successfully created virtual environment"\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ] || [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Virtual environment is missing critical components"\n echo "Contents of virtual environment directory:"\n ls -la "$VENV_PATH"\n if [ -d "$VENV_PATH/bin" ]; then\n echo "Contents of bin directory:"\n ls -la "$VENV_PATH/bin"\n fi\n exit 1\nfi\necho "Virtual environment structure verified"\n\n# Activate virtual environment and install local-operator\necho "Installing local-operator in virtual environment..."\nsource "$VENV_PATH/bin/activate"\n\necho "Upgrading pip..."\npython -m pip install --upgrade pip || {\n echo "ERROR: Failed to upgrade pip. Exit code: $?"\n echo "pip version before failing:"\n pip --version\n exit 1\n}\necho "pip upgrade successful:"\npip --version\n\n# Check network connectivity to PyPI\necho "Checking network connectivity to PyPI..."\ncurl -s https://pypi.org/pypi/local-operator/json -o /dev/null || {\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n echo "Attempting to ping common domains to diagnose network issues:"\n ping -c 1 google.com || echo "Cannot ping google.com"\n ping -c 1 pypi.org || echo "Cannot ping pypi.org"\n}\n\necho "Installing local-operator package..."\npython -m pip install --upgrade --verbose local-operator || {\n echo "ERROR: Failed to install local-operator package. Exit code: $?"\n echo "Python version:"\n python --version\n echo "pip version:"\n pip --version\n echo "Available pip packages:"\n pip list\n echo "Pip config:"\n pip config list\n echo "Network diagnosis:"\n curl -I https://pypi.org || echo "Cannot reach PyPI server"\n exit 1\n}\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n echo "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator"\n file "$VENV_PATH/bin/local-operator"\n \n # Try to run the version command with full error output\n echo "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n echo "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n echo "Binary details:"\n file "$VENV_PATH/bin/local-operator"\n echo "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator"\n echo "Dependencies:"\n if command -v otool >/dev/null; then\n otool -L "$VENV_PATH/bin/local-operator" || echo "Could not get dependencies with otool"\n fi\n exit 1\n }\nelse\n echo "Error: Failed to install Local Operator backend. Binary not found."\n echo "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/"\n exit 1\nfi\n\necho "$(date): Installation completed successfully."\n';
9495
- const windowsInstallScriptRaw = `# Local Operator Backend Installation Script for Windows
9496
- # This script installs pyenv-win, Python 3.12, and sets up a virtual environment for the Local Operator backend.
9497
-
9498
- # Configuration
9499
- $AppName = "Local Operator"
9500
- $PythonVersion = "3.12.0"
9501
- $VenvName = "local-operator-venv"
9502
- $AppDataDir = "$env:APPDATA\\\\$AppName"
9503
- $VenvPath = "$AppDataDir\\\\$VenvName"
9504
- $LogFile = "$AppDataDir\\\\backend-install-shell.log"
9505
- $PyenvDir = "$env:USERPROFILE\\\\.pyenv"
9506
-
9507
- # Which environment this installs into - the app's decision, handed in rather
9508
- # than re-derived. A packaged install and an unpackaged one must not share an
9509
- # environment (the venv is built on whatever interpreter the instance resolves),
9510
- # and only the app knows which it is: LOCAL_OPERATOR_VENV_PATH carries
9511
- # managedVenvPath's answer (src/main/backend/venv-paths.ts). The default above is
9512
- # what a standalone run uses - the packaged name, because that is what every
9513
- # install on a disk today has.
9514
- if ($env:LOCAL_OPERATOR_VENV_PATH) {
9515
- $VenvPath = $env:LOCAL_OPERATOR_VENV_PATH
9516
- }
9517
-
9518
- # Keep CPython's bytecode cache out of the application directory.
9519
- #
9520
- # Same rule as the macOS script, and stated here for the same reason: an
9521
- # interpreter writes __pycache__/*.pyc beside the stdlib sources it imports. On
9522
- # Windows those sources are the bundled python directory rather than a
9523
- # code-signed bundle, so this is hygiene rather than the load-bearing fix it is
9524
- # on macOS - but the app and the script must not disagree about where bytecode
9525
- # goes. $AppDataDir is used for the same reason the venv is: it is never inside
9526
- # the installed application tree.
9527
- if (-not $env:PYTHONPYCACHEPREFIX) {
9528
- $env:PYTHONPYCACHEPREFIX = "$AppDataDir\\\\python-bytecode-cache"
9529
- }
9530
-
9531
- # The refusal half of the same pair, kept in step with the app's
9532
- # \`withPythonBytecodeCache\`: CPython reads the flag before its first import, so
9533
- # nothing this script runs can write a __pycache__ at all.
9534
- $env:PYTHONDONTWRITEBYTECODE = "1"
9535
-
9536
- # Create app data directory if it doesn't exist
9537
- if (-not (Test-Path $AppDataDir)) {
9538
- New-Item -ItemType Directory -Path $AppDataDir -Force | Out-Null
9539
- }
9540
-
9541
- # Start logging
9542
- Start-Transcript -Path $LogFile -Append
9543
- Write-Output "$(Get-Date): Starting Local Operator backend installation..."
9544
-
9545
- $BinDir = "$AppDataDir\\\\bin"
9546
- $FFmpegBin = "$BinDir\\\\ffmpeg.exe"
9547
-
9548
- Write-Output "Ensuring bin directory exists: $BinDir"
9549
- if (-not (Test-Path $BinDir)) {
9550
- New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
9551
- }
9552
-
9553
- # Check if FFmpeg is already installed
9554
- if (Test-Path $FFmpegBin) {
9555
- Write-Output "FFmpeg already installed at $FFmpegBin. Skipping download."
9556
- } else {
9557
- Write-Output "FFmpeg not found. Attempting to download and install FFmpeg..."
9558
-
9559
- $FFmpegDownloadUrl = ""
9560
-
9561
- # Determine architecture and set appropriate download URL
9562
- if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64" -or $env:PROCESSOR_ARCHITEW6432 -eq "AMD64") {
9563
- $FFmpegDownloadUrl = "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-win32-x64"
9564
- } else {
9565
- Write-Error "Unsupported CPU architecture for FFmpeg download: $env:PROCESSOR_ARCHITECTURE"
9566
- exit 1
9567
- }
9568
-
9569
- Write-Output "Downloading FFmpeg from: $FFmpegDownloadUrl"
9570
- try {
9571
- Invoke-WebRequest -Uri $FFmpegDownloadUrl -OutFile $FFmpegBin -ErrorAction Stop
9572
- Write-Output "FFmpeg downloaded successfully to $FFmpegBin"
9573
-
9574
- # Verify FFmpeg is accessible after download
9575
- if (-not (Test-Path $FFmpegBin)) {
9576
- Write-Error "FFmpeg binary not found at $FFmpegBin after download."
9577
- exit 1
9578
- }
9579
- } catch {
9580
- Write-Error "Failed to download FFmpeg: $($_.Exception.Message)"
9581
- exit 1
9582
- }
9583
- }
9584
- Write-Output "FFmpeg installation complete. FFmpeg binary is at: $FFmpegBin"
9585
-
9586
- # Function to check if a command exists
9587
- function Test-CommandExists {
9588
- param ($command)
9589
- $oldPreference = $ErrorActionPreference
9590
- $ErrorActionPreference = 'stop'
9591
- try {
9592
- if (Get-Command $command) { return $true }
9593
- } catch {
9594
- return $false
9595
- } finally {
9596
- $ErrorActionPreference = $oldPreference
9597
- }
9598
- }
9599
-
9600
- # Install pyenv-win if not installed
9601
- if (-not (Test-Path $PyenvDir)) {
9602
- Write-Output "Installing pyenv-win..."
9603
-
9604
- # Create temporary directory
9605
- $TempDir = "$env:TEMP\\\\pyenv-win"
9606
- if (Test-Path $TempDir) {
9607
- Remove-Item -Path $TempDir -Recurse -Force
9608
- }
9609
- New-Item -ItemType Directory -Path $TempDir -Force | Out-Null
9610
-
9611
- # Download and extract pyenv-win
9612
- $PyenvZip = "$TempDir\\\\pyenv-win.zip"
9613
- Invoke-WebRequest -Uri "https://github.com/pyenv-win/pyenv-win/archive/master.zip" -OutFile $PyenvZip
9614
- Expand-Archive -Path $PyenvZip -DestinationPath $TempDir
9615
-
9616
- # Create .pyenv directory
9617
- New-Item -ItemType Directory -Path $PyenvDir -Force | Out-Null
9618
-
9619
- # Copy pyenv-win files
9620
- Copy-Item -Path "$TempDir\\\\pyenv-win-master\\\\*" -Destination $PyenvDir -Recurse
9621
-
9622
- # Set environment variables
9623
- [System.Environment]::SetEnvironmentVariable("PYENV", "$PyenvDir\\\\pyenv-win", "User")
9624
- [System.Environment]::SetEnvironmentVariable("PYENV_HOME", "$PyenvDir\\\\pyenv-win", "User")
9625
-
9626
- # Update PATH - ensure both bin and shims are added separately for better compatibility
9627
- $Path = [System.Environment]::GetEnvironmentVariable("PATH", "User")
9628
- $PyenvBinPath = "$PyenvDir\\\\pyenv-win\\\\bin"
9629
- $PyenvShimsPath = "$PyenvDir\\\\pyenv-win\\\\shims"
9630
-
9631
- # Add bin path if not already in PATH
9632
- if ($Path -notlike "*$PyenvBinPath*") {
9633
- [System.Environment]::SetEnvironmentVariable("PATH", "$PyenvBinPath;$Path", "User")
9634
- $Path = [System.Environment]::GetEnvironmentVariable("PATH", "User")
9635
- }
9636
-
9637
- # Add shims path if not already in PATH
9638
- if ($Path -notlike "*$PyenvShimsPath*") {
9639
- [System.Environment]::SetEnvironmentVariable("PATH", "$PyenvShimsPath;$Path", "User")
9640
- }
9641
-
9642
- # Set PYENV environment variables
9643
- [System.Environment]::SetEnvironmentVariable("PYENV", "$PyenvDir\\\\pyenv-win", "User")
9644
- [System.Environment]::SetEnvironmentVariable("PYENV_HOME", "$PyenvDir\\\\pyenv-win", "User")
9645
-
9646
- # Update current session PATH
9647
- $env:PYENV = "$PyenvDir\\\\pyenv-win"
9648
- $env:PYENV_HOME = "$PyenvDir\\\\pyenv-win"
9649
- $env:PATH = "$PyenvDir\\\\pyenv-win\\\\bin;$PyenvDir\\\\pyenv-win\\\\shims;$env:PATH"
9650
-
9651
- # Refresh environment variables for the current process
9652
- $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "User") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "Machine")
9653
-
9654
- # Clean up
9655
- Remove-Item -Path $TempDir -Recurse -Force
9656
- }
9657
-
9658
- # Refresh environment variables for current session
9659
- $env:PYENV = "$PyenvDir\\\\pyenv-win"
9660
- $env:PYENV_HOME = "$PyenvDir\\\\pyenv-win"
9661
- $env:PATH = "$PyenvDir\\\\pyenv-win\\\\bin;$PyenvDir\\\\pyenv-win\\\\shims;$env:PATH"
9662
-
9663
- # Install Python 3.12 if not installed
9664
- $PythonInstalled = $false
9665
- try {
9666
- $InstalledVersions = & pyenv versions
9667
- if ($InstalledVersions -like "*$PythonVersion*") {
9668
- $PythonInstalled = $true
9669
- }
9670
- } catch {
9671
- $PythonInstalled = $false
9672
- }
9673
-
9674
- if (-not $PythonInstalled) {
9675
- Write-Output "Installing Python $PythonVersion..."
9676
- & pyenv install $PythonVersion
9677
- if ($LASTEXITCODE -ne 0) {
9678
- Write-Error "Failed to install Python $PythonVersion"
9679
- exit 1
9680
- }
9681
- }
9682
-
9683
- # Set Python 3.12 as the local version
9684
- & pyenv local $PythonVersion
9685
- if ($LASTEXITCODE -ne 0) {
9686
- Write-Error "Failed to set Python $PythonVersion as local version"
9687
- exit 1
9688
- }
9689
-
9690
- # Create virtual environment if it doesn't exist
9691
- if (-not (Test-Path $VenvPath)) {
9692
- Write-Output "Creating virtual environment at $VenvPath..."
9693
-
9694
- # Ensure the directory exists
9695
- if (-not (Test-Path $AppDataDir)) {
9696
- New-Item -ItemType Directory -Path $AppDataDir -Force | Out-Null
9697
- Write-Output "Created directory: $AppDataDir"
9698
- }
9699
-
9700
- # Use the full path to python from pyenv
9701
- $PythonExe = "$PyenvDir\\\\pyenv-win\\\\versions\\\\$PythonVersion\\\\python.exe"
9702
-
9703
- if (Test-Path $PythonExe) {
9704
- Write-Output "Using Python at: $PythonExe"
9705
- & $PythonExe -m venv $VenvPath
9706
- } else {
9707
- Write-Output "Using system Python"
9708
- & python -m venv $VenvPath
9709
- }
9710
-
9711
- if ($LASTEXITCODE -ne 0) {
9712
- Write-Error "Failed to create virtual environment"
9713
- exit 1
9714
- }
9715
- }
9716
-
9717
- # Verify the virtual environment was created
9718
- if (-not (Test-Path "$VenvPath\\\\Scripts\\\\Activate.ps1")) {
9719
- Write-Error "Virtual environment activation script not found at $VenvPath\\\\Scripts\\\\Activate.ps1"
9720
- exit 1
9721
- }
9722
-
9723
- # Activate virtual environment and install local-operator
9724
- Write-Output "Installing local-operator in virtual environment..."
9725
- # Use PowerShell to run the activation script
9726
- & "$VenvPath\\\\Scripts\\\\Activate.ps1"
9727
- & python -m pip install --upgrade pip
9728
- & python -m pip install --upgrade local-operator
9729
-
9730
- if ($LASTEXITCODE -ne 0) {
9731
- Write-Error "Failed to install packages in virtual environment"
9732
- exit 1
9733
- }
9734
-
9735
- # Verify installation
9736
- $LocalOperatorInstalled = $false
9737
- try {
9738
- # First try to run local-operator directly (it should be in PATH from the activated venv)
9739
- $Version = & local-operator --version
9740
- $LocalOperatorInstalled = $true
9741
- Write-Output "Local Operator backend installed successfully!"
9742
- Write-Output $Version
9743
- } catch {
9744
- Write-Output "Could not run local-operator directly, trying with full path..."
9745
- try {
9746
- # Try with explicit path
9747
- $LocalOperatorExe = "$VenvPath\\\\Scripts\\\\local-operator.exe"
9748
- if (Test-Path $LocalOperatorExe) {
9749
- $Version = & $LocalOperatorExe --version
9750
- $LocalOperatorInstalled = $true
9751
- Write-Output "Local Operator backend installed successfully at $LocalOperatorExe!"
9752
- Write-Output $Version
9753
- } else {
9754
- # Try without .exe extension
9755
- $LocalOperatorCmd = "$VenvPath\\\\Scripts\\\\local-operator"
9756
- if (Test-Path $LocalOperatorCmd) {
9757
- $Version = & $LocalOperatorCmd --version
9758
- $LocalOperatorInstalled = $true
9759
- Write-Output "Local Operator backend installed successfully at $LocalOperatorCmd!"
9760
- Write-Output $Version
9761
- } else {
9762
- Write-Error "Error: local-operator executable not found in expected locations."
9763
-
9764
- # Create a symlink to make it more accessible
9765
- Write-Output "Attempting to create a symlink for local-operator..."
9766
- $PythonModule = "$VenvPath\\\\Lib\\\\site-packages\\\\local_operator"
9767
- if (Test-Path $PythonModule) {
9768
- Write-Output "Found local_operator module at $PythonModule"
9769
-
9770
- # Create a batch file that runs the module
9771
- $BatchContent = "@echo off\`npython -m local_operator %*"
9772
- Set-Content -Path "$VenvPath\\\\Scripts\\\\local-operator.bat" -Value $BatchContent
9773
-
9774
- # Test the batch file
9775
- if (Test-Path "$VenvPath\\\\Scripts\\\\local-operator.bat") {
9776
- Write-Output "Created local-operator.bat in Scripts directory"
9777
- $LocalOperatorInstalled = $true
9778
- } else {
9779
- Write-Error "Failed to create local-operator.bat"
9780
- exit 1
9781
- }
9782
- } else {
9783
- Write-Error "local_operator module not found in site-packages"
9784
- exit 1
9785
- }
9786
- }
9787
- }
9788
- } catch {
9789
- Write-Error "Error: Failed to install Local Operator backend."
9790
- Write-Error $_.Exception.Message
9791
- exit 1
9792
- }
9793
- }
9794
-
9795
- # If we got here, installation was successful
9796
- if ($LocalOperatorInstalled) {
9797
- Write-Output "Local Operator backend installation verified."
9798
- } else {
9799
- Write-Error "Error: Failed to verify Local Operator backend installation."
9800
- exit 1
9801
- }
9802
-
9803
- Write-Output "$(Get-Date): Installation completed successfully."
9804
-
9805
- # Properly close the transcript and ensure it's released
9806
- try {
9807
- Write-Output "Finalizing installation and releasing resources..."
9808
- # Flush any pending output
9809
- [System.Console]::Out.Flush()
9810
- # Stop transcript properly
9811
- Stop-Transcript
9812
-
9813
- # Add a small delay to ensure file handles are released
9814
- Start-Sleep -Seconds 1
9815
-
9816
- # Explicitly release any COM objects
9817
- [System.GC]::Collect()
9818
- [System.GC]::WaitForPendingFinalizers()
9819
-
9820
- Write-Output "Installation completed and resources released."
9821
- } catch {
9822
- Write-Error "Error during cleanup: $_"
9823
- }
9824
- `;
9773
+ const linuxInstallScriptRaw = '#!/bin/bash\n# Local Operator Backend Installation Script for Linux\n# This script sets up a virtual environment for the Local Operator backend\n# without requiring sudo or admin privileges.\n\n# Exit on error, undefined variables, and failed pipe commands\nset -e # Exit immediately if a command exits with a non-zero status\nset -u # Error on unset variables\nset -o pipefail # Fail if any command in a pipe fails\n\n# Configuration\n# Variables APP_NAME and MIN_PYTHON_VERSION removed as they were unused\nVENV_NAME="local-operator-venv"\nAPP_DATA_DIR="${HOME}/.config/local-operator"\nVENV_PATH="${APP_DATA_DIR}/${VENV_NAME}"\nLOG_FILE="${APP_DATA_DIR}/backend-install.log"\nTIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")\n\n# Which environment this installs into - the app\'s decision, handed in. See the\n# macOS script for why the script must not re-derive it (a packaged and an\n# unpackaged instance use different environments, and only the app knows which\n# it is); `LOCAL_OPERATOR_VENV_PATH` is set from `managedVenvPath`.\n: "${LOCAL_OPERATOR_VENV_PATH:=$VENV_PATH}"\nVENV_PATH="$LOCAL_OPERATOR_VENV_PATH"\n\n# Keep CPython\'s bytecode cache out of the application directory.\n#\n# Same reason as the macOS script: an interpreter writing __pycache__/*.pyc\n# beside its own stdlib sources writes into the installed tree. Linux does not\n# code-seal the app the way macOS does, so this is uniformity and hygiene here\n# rather than the load-bearing fix it is on macOS - but it is also the value a\n# standalone run of this script has to agree with, because the app and the\n# script must not disagree about where bytecode goes.\n: "${PYTHONPYCACHEPREFIX:=${APP_DATA_DIR}/python-bytecode-cache}"\nexport PYTHONPYCACHEPREFIX\n\n# The refusal half of the pair, set rather than merged with whatever the caller\n# had: CPython reads the flag before its first import, so nothing this script\n# runs - `-m venv`, pip, the venv they create - can write a `__pycache__` at\n# all. Kept in step with the app\'s `withPythonBytecodeCache`, which sets both\n# variables on every python it spawns.\nexport PYTHONDONTWRITEBYTECODE=1\n\n# Create app data directory if it doesn\'t exist\nif ! mkdir -p "${APP_DATA_DIR}"; then\n echo "ERROR: Unable to create app data directory at ${APP_DATA_DIR}"\n echo "Please check permissions and try again."\n exit 1\nfi\n\n# Handle cleanup on script exit\ncleanup() {\n # Remove any temporary files created during execution\n if [ -f "${APP_DATA_DIR}/get-pip.py" ]; then\n rm -f "${APP_DATA_DIR}/get-pip.py"\n fi\n echo "$(date): Cleanup completed."\n}\n\n# Set trap only for manual interruption (SIGINT, SIGTERM), not normal exits\ntrap cleanup SIGINT SIGTERM\n\n\n# Start logging\nexec > >(tee -a "${LOG_FILE}") 2>&1\necho "[${TIMESTAMP}]: Starting Local Operator backend installation..."\necho "[${TIMESTAMP}]: System information: $(uname -a)"\n\n# Nothing is fetched here but the package itself.\n#\n# This script used to download a third-party FFmpeg binary from a GitHub release\n# into `${APP_DATA_DIR}/bin`, and under `set -e` a failed download killed the\n# install before the venv existed - so a machine that can reach PyPI but not\n# github.com could not install at all. Nothing in the app or in `local-operator`\n# ever executed that binary. Tooling a task actually needs is acquired later, on\n# demand, through the app\'s Console with the user\'s approval; this script\'s job is\n# the environment below and nothing else. (`_log_internal`, the logging helper\n# that read like a second `log` and existed only for that section, went with it.)\n# Function to check if a command exists\ncommand_exists() {\n command -v "$1" >/dev/null 2>&1\n}\n\n# Function to log messages with timestamp\nlog() {\n local timestamp\n timestamp=$(date +"%Y-%m-%d %H:%M:%S")\n echo "[${timestamp}] $1"\n}\n\n# Function to display error messages and exit\nerror_exit() {\n log "ERROR: $1" >&2\n exit "${2:-1}"\n}\n\n# Function to check if Python binary is valid for current architecture\nis_valid_python_binary() {\n local python_path="$1"\n \n # First check if file exists and is executable\n if [ ! -f "${python_path}" ] || [ ! -x "${python_path}" ]; then\n return 1\n fi\n \n # Try to run a simple Python command to verify it works on current architecture\n "${python_path}" -c "print(\'Testing Python executable\')" >/dev/null 2>&1\n return $?\n}\n\n# Check for network connectivity to key servers.\n#\n# Bounded on both spellings: `--connect-timeout 5`/`--max-time 30` for curl and\n# `--timeout=30`/`--tries=1` for wget (a single attempt, whose timeout covers\n# connect and read since wget has no separate connect bound for BusyBox). 5 ends\n# a black-hole network; 30 is the stall bound and is deliberately well above a\n# slow-but-working link\'s answer time, because a probe that fails a working\n# connection is noise users learn to ignore.\n#\n# `--fail` turns an HTTP error status into a non-zero exit (a proxy\'s 403/407,\n# any 4xx/5xx). It does NOT notice a captive portal answering 200 with its own\n# HTML page - measured, a portal-shaped 200 exits 0 with and without the flag -\n# so this check answers "did a request to the host complete", and the PyPI probe\n# below is the one that also checks WHAT came back.\ncheck_connectivity() {\n log "Checking network connectivity..."\n local servers=("pypi.org" "bootstrap.pypa.io")\n local has_connectivity=false\n \n for server in "${servers[@]}"; do\n if command_exists curl; then\n if curl --fail --connect-timeout 5 --max-time 30 -s "https://${server}" -o /dev/null; then\n has_connectivity=true\n break\n fi\n elif command_exists wget; then\n if wget --timeout=30 --tries=1 -q --spider "https://${server}"; then\n has_connectivity=true\n break\n fi\n fi\n done\n \n if [ "$has_connectivity" = false ]; then\n log "WARNING: Network connectivity issues detected. This may affect installation."\n else\n log "Network connectivity confirmed."\n fi\n}\n\n# Call connectivity check\ncheck_connectivity\n\n# Check if PYTHON_BIN is already set by the installer\nif [ -n "${PYTHON_BIN:-}" ]; then\n log "Using Python executable provided by installer: ${PYTHON_BIN}"\n # Verify the provided Python binary works on this architecture\n if ! is_valid_python_binary "${PYTHON_BIN}"; then\n log "Warning: The provided Python binary is not compatible with this system architecture."\n log "Will attempt to find a system Python installation instead."\n PYTHON_BIN=""\n fi\nfi\n\n# If PYTHON_BIN is empty or invalid, try to find a suitable Python installation\nif [ -z "${PYTHON_BIN:-}" ] || ! is_valid_python_binary "${PYTHON_BIN:-}"; then\n # Try to find a suitable Python installation\n log "Looking for a suitable Python installation..."\n \n # Try multiple possible locations to find Python\n POSSIBLE_PYTHON_PATHS=(\n # System paths first (more likely to be compatible with current architecture)\n "/usr/local/bin/python3.12"\n "/usr/bin/python3.12"\n "/usr/local/bin/python3"\n "/usr/bin/python3"\n # From environment variable (set by the installer)\n "${ELECTRON_RESOURCE_PATH:-}/python/bin/python3"\n # Development paths\n "$(dirname "$0")/../../../resources/python/bin/python3"\n "$(pwd)/resources/python/bin/python3"\n )\n\n # Find the first Python that exists and is version 3.12+ and works on this architecture\n PYTHON_BIN=""\n for path in "${POSSIBLE_PYTHON_PATHS[@]}"; do\n # Skip empty paths that might come from unset environment variables\n if [ -z "${path}" ]; then\n continue\n fi\n \n if is_valid_python_binary "${path}"; then\n # Check Python version\n if PY_VERSION=$("${path}" -c "import sys; print(f\'{sys.version_info.major}.{sys.version_info.minor}\')" 2>/dev/null); then\n MAJOR=$(echo "${PY_VERSION}" | cut -d. -f1)\n MINOR=$(echo "${PY_VERSION}" | cut -d. -f2)\n if [ "${MAJOR}" -eq 3 ] && [ "${MINOR}" -ge 12 ]; then\n PYTHON_BIN="${path}"\n log "Found suitable Python ${PY_VERSION} at ${path}"\n break\n else\n log "Python at ${path} is version ${PY_VERSION}, which is below the required 3.12+"\n fi\n fi\n else\n if [ -f "${path}" ]; then\n log "Python at ${path} exists but is not compatible with this system architecture"\n fi\n fi\n done\n\n # If we couldn\'t find a suitable Python, exit with error\n if [ -z "${PYTHON_BIN:-}" ]; then\n error_exit "No suitable Python installation found (version 3.12 or higher required).\\nPlease install Python 3.12 or higher before running this script.\\nYou can install Python from https://www.python.org/downloads/"\n fi\nfi\n\necho "Using Python: $PYTHON_BIN"\n"$PYTHON_BIN" --version || {\n echo "Error: Failed to run Python. Please ensure it is executable and accessible."\n echo "System architecture: $(uname -m)"\n echo "Python binary architecture: $(file "$PYTHON_BIN" 2>/dev/null || echo \'Unable to determine\')"\n exit 1\n}\n\n# Detect the Linux distribution to provide specific instructions\nif command_exists lsb_release; then\n DISTRO=$(lsb_release -is)\n DISTRO_VERSION=$(lsb_release -rs)\n echo "Detected Linux distribution: $DISTRO $DISTRO_VERSION"\nelif [ -f /etc/os-release ]; then\n DISTRO=$(grep -oP \'(?<=^ID=).+\' /etc/os-release | tr -d \'"\')\n DISTRO_VERSION=$(grep -oP \'(?<=^VERSION_ID=).+\' /etc/os-release | tr -d \'"\')\n echo "Detected Linux distribution: $DISTRO $DISTRO_VERSION"\nelse\n DISTRO="unknown"\n echo "Unable to detect Linux distribution"\nfi\n\n# Check if Python has venv module and ensurepip available\nlog "Checking for venv module and ensurepip availability..."\nVENV_AVAILABLE=0\nENSUREPIP_AVAILABLE=0\n\n"${PYTHON_BIN}" -c "import venv" > /dev/null 2>&1 || VENV_AVAILABLE=1\n"${PYTHON_BIN}" -c "import ensurepip" > /dev/null 2>&1 || ENSUREPIP_AVAILABLE=1\n\n# If venv is still not available, try to use virtualenv as a fallback\nif [ $VENV_AVAILABLE -ne 0 ]; then\n echo "venv module is not available. Checking for virtualenv as an alternative..."\n \n # Check if virtualenv is already installed\n if "$PYTHON_BIN" -c "import virtualenv" > /dev/null 2>&1; then\n echo "virtualenv is available, will use it instead of venv"\n else\n echo "ERROR: Neither venv nor virtualenv is available."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n exit 1\n fi\nfi\n\n# Create virtual environment if it doesn\'t exist\nif [ ! -d "$VENV_PATH" ]; then\n echo "Creating virtual environment at $VENV_PATH..."\n # Remove any potentially corrupted virtual environment\n if [ -e "$VENV_PATH" ]; then\n echo "Removing existing but potentially corrupted venv directory..."\n rm -rf "$VENV_PATH"\n fi\n \n # Make sure parent directory exists and is writable\n mkdir -p "$(dirname "$VENV_PATH")"\n \n # Try different methods to create a virtual environment\n VENV_CREATE_STATUS=1\n \n # First try with venv if available\n if [ $VENV_AVAILABLE -eq 0 ] && [ $ENSUREPIP_AVAILABLE -eq 0 ]; then\n echo "Creating virtual environment using venv module..."\n "$PYTHON_BIN" -m venv "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n fi\n \n # If venv failed, try virtualenv\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n echo "venv creation failed with status $VENV_CREATE_STATUS. Falling back to virtualenv..."\n if command_exists virtualenv; then\n virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n elif "$PYTHON_BIN" -m pip list | grep -q virtualenv; then\n "$PYTHON_BIN" -m virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n else\n echo "ERROR: Both venv and virtualenv are unavailable or failed."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n echo "You can install Python 3.12 from https://www.python.org/downloads/"\n exit 1\n fi\n fi\n \n # Check if virtual environment creation was successful\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n log "ERROR: Failed to create virtual environment. Exit code: $VENV_CREATE_STATUS"\n log "Virtual environment path: $VENV_PATH"\n log "Python binary used: $PYTHON_BIN"\n ls -la "$(dirname "$VENV_PATH")" || true\n log "Python executable permissions:"\n ls -la "$PYTHON_BIN" || true\n \n # Check for common issues\n log "Checking for common virtual environment creation issues..."\n \n # Check disk space\n df -h "$(dirname "$VENV_PATH")" || true\n \n # Check if directory is writable\n if [ ! -w "$(dirname "$VENV_PATH")" ]; then\n log "ERROR: Directory $(dirname "$VENV_PATH") is not writable"\n fi\n \n # Try to create a minimal virtual environment manually as a last resort\n log "Attempting to create a minimal virtual environment manually..."\n mkdir -p "$VENV_PATH/bin" || error_exit "Could not create directory $VENV_PATH/bin"\n echo "#!/bin/bash" > "$VENV_PATH/bin/activate"\n echo "export VIRTUAL_ENV=\\"$VENV_PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "export PATH=\\"$VENV_PATH/bin:\\$PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "unset PYTHONHOME" >> "$VENV_PATH/bin/activate"\n chmod +x "$VENV_PATH/bin/activate" || error_exit "Could not set execute permissions on activate script"\n \n # Create symlinks to the system Python\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python" || log "WARNING: Failed to create symlink for python"\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python3" || log "WARNING: Failed to create symlink for python3"\n \n # Try to bootstrap pip\n log "Bootstrapping pip in the minimal virtual environment..."\n if command_exists curl; then\n curl -s --fail --connect-timeout 10 --max-time 60 https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py" || log "WARNING: Failed to download get-pip.py"\n elif command_exists wget; then\n wget -q --timeout=30 --tries=1 -O "$APP_DATA_DIR/get-pip.py" https://bootstrap.pypa.io/get-pip.py || log "WARNING: Failed to download get-pip.py"\n else\n log "ERROR: Neither curl nor wget available to download get-pip.py"\n error_exit "Installation cannot continue without being able to download pip"\n fi\n \n # shellcheck disable=SC1090\n source "$VENV_PATH/bin/activate" && python "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n error_exit "Failed to create even a minimal virtual environment."\n else\n log "Created a minimal virtual environment as a fallback."\n fi\n else\n log "Successfully created virtual environment"\n fi\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ]; then\n echo "Python executable missing in virtual environment, creating symlink..."\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python"\nfi\n\nif [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "pip missing in virtual environment, attempting to bootstrap it..."\n curl -s --fail --connect-timeout 10 --max-time 60 https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py"\n "$VENV_PATH/bin/python" "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Failed to bootstrap pip in the virtual environment"\n exit 1\n fi\nfi\n\necho "Virtual environment structure verified"\n\n# --- The package install: uv when there is one, pip otherwise ------------------\n#\n# Same shape as the macOS script, and for the same reasons: uv resolves and\n# fetches in parallel - measured there, cold cache, three runs each, same\n# interpreter: uv\'s package install is 12.8-16.1 s against pip\'s 33.0-40.7 s,\n# plus the 2.3-2.8 s pip self-upgrade this path skips, so 1.5-2.8x across two\n# operators rather than the "14.8 s against 128.9 s" quoted here before that\n# reading was withdrawn (`docs/BUILD.md` has the full set). The pip path below is\n# unchanged and runs whenever uv is absent or cannot do the job, and pip STAYS in the venv\n# because the app\'s backend-update path runs `pip install --upgrade\n# local-operator` inside this same environment - which is also why the venv is\n# still created with `python -m venv` rather than `uv venv`.\n#\n# Nothing here searches PATH for a uv: an installed uv is a version and a\n# configuration nobody in this repository chose. `LOCAL_OPERATOR_UV_BIN` is the\n# app\'s own answer (`src/main/backend/uv-tool.ts`).\nUV_BIN="${LOCAL_OPERATOR_UV_BIN:-}"\n\n# Drop every UV_* variable the launching environment carried. Measured on uv\n# 0.12.17: a user-level `uv.toml` naming an unreachable index is obeyed by\n# `uv pip install` and ignored with `UV_NO_CONFIG=1`; an ambient `UV_INDEX_URL`\n# changes where packages come from, while `PIP_INDEX_URL` does not affect uv at\n# all. A name list would drift the day uv adds a variable - the namespace cannot.\n#\n# IT RUNS BEFORE THE SETTINGS BELOW ARE SET: a sweep after them takes them away,\n# and an empty `UV_CACHE_DIR` makes uv exit 2 with `a value is required for\n# \'--cache-dir <CACHE_DIR>\'` rather than falling back to a default.\nfor uv_ambient in $(env | sed -n \'s/^\\(UV_[A-Za-z0-9_]*\\)=.*/\\1/p\'); do\n unset "$uv_ambient"\ndone\n\n# The cache lives under the app\'s own support directory rather than the user\'s\n# shared `~/.cache/uv`, and is handed to uv per invocation rather than exported.\n# It persists (~118 MB for a full install, measured on macOS) and nothing else\n# reads it today; it is what makes a retry converge in seconds rather than tens\n# of seconds.\nUV_CACHE_DIR="${APP_DATA_DIR}/uv-cache"\n\n# Is the handed-down uv something we can actually run?\nuv_is_usable() {\n [ -n "${UV_BIN}" ] && [ -x "${UV_BIN}" ] && "${UV_BIN}" --version >/dev/null 2>&1\n}\n\n# UV_NO_CONFIG: never read `pyproject.toml`/`uv.toml`, wherever they are.\n# UV_PYTHON_DOWNLOADS=never: this install uses the interpreter it was handed and\n# never fetches another.\nuv_run() {\n UV_NO_CONFIG=1 UV_PYTHON_DOWNLOADS=never UV_CACHE_DIR="${UV_CACHE_DIR}" \\\n "${UV_BIN}" "$@"\n}\n\n# Activate virtual environment and install local-operator\necho "Installing local-operator in virtual environment..."\nsource "$VENV_PATH/bin/activate"\n\n# Check network connectivity to PyPI. A DIAGNOSTIC, not a gate: the install below\n# decides whether it can proceed.\n#\n# What this answers, stated precisely because an earlier version of this comment\n# claimed more than the flags buy: "did a TLS fetch to PyPI\'s JSON API complete,\n# and did the answer come back as JSON?". `--fail` turns an HTTP ERROR status\n# into a non-zero exit, and does NOT notice a captive portal answering 200 with\n# its own HTML page (measured: a portal-shaped 200 exits 0 with and without the\n# flag). Both spellings therefore check the CONTENT TYPE as well, since a portal\n# that reports "reachable" while pip is about to fail is the false negative this\n# warning exists to catch: curl through `-w \'%{content_type}\'`, and wget through\n# the response headers `-S` prints (`-S`/`--server-response` is in BusyBox\'s wget\n# since 2017 as well as GNU\'s, which is the wget a system without curl has).\n#\n# What the content type does NOT prove, stated so this paragraph is not read as\n# more than it says: a proxy answering 200 with `application/json` and an error\n# body (`{"detail":"blocked by proxy policy"}`) is silent here, because the\n# answer did come back as JSON. Only parsing the body - a fetch of PyPI\'s own\n# payload shape - would tell those apart, and a diagnostic that costs a parse is\n# not what stands in front of an install.\n#\n# Two bounds, two jobs: the connect bound ends a black-hole network, the total\n# stops a connected-but-stalled peer. Both must be POSITIVE - `--max-time 0` and\n# `--timeout=0` disable the bound rather than making it immediate on both curl and\n# wget. 30 rather than 10 because the total must not fire on a slow-but-working\n# link: a working endpoint that answered in 15s tripped a 10s bound and printed\n# this warning on an install that then succeeded.\necho "Checking network connectivity to PyPI..."\nif command_exists curl; then\n PYPI_PROBE_CONTENT_TYPE=$(curl -s --fail --connect-timeout 5 --max-time 30 -o /dev/null -w \'%{content_type}\' https://pypi.org/pypi/local-operator/json) || PYPI_PROBE_CONTENT_TYPE=""\n if [[ "${PYPI_PROBE_CONTENT_TYPE}" != application/json* ]]; then\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n fi\nelif command_exists wget; then\n # The headers are captured into a variable and matched with a here-string\n # rather than piped into `grep -q`: `-q` exits on the first match, the closed\n # pipe gives wget SIGPIPE, and the script runs under `set -o pipefail`, so a\n # WORKING link printed this warning whenever the match landed before wget had\n # finished writing its headers. The piped shape was this remediation\'s own - the\n # first fix piped it and was measured warning 1 time in 20 against real PyPI\n # (and 1 in 20 against a healthy local endpoint) while this shape warns 0 in 40\n # with the same flags - so the trap is recorded here because it is one line away\n # from being reintroduced, not because the released script ever shipped it.\n PYPI_PROBE_HEADERS=$(wget -q -S --spider --timeout=30 --tries=1 https://pypi.org/pypi/local-operator/json 2>&1) || PYPI_PROBE_HEADERS=""\n if ! grep -qi \'^ *content-type: application/json\' <<< "$PYPI_PROBE_HEADERS"; then\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n fi\nfi\n\nUV_INSTALLED=false\nif uv_is_usable; then\n echo "Installing local-operator with uv ($("${UV_BIN}" --version 2>/dev/null || echo \'version unavailable\'))..."\n # No `pip install --upgrade pip` on this path: uv does not use pip.\n if uv_run pip install --python "${VENV_PATH}/bin/python" --upgrade local-operator; then\n UV_INSTALLED=true\n echo "local-operator installation with uv successful"\n else\n # The exit code matters here for the same reason it does on macOS: the\n # fallback is deliberately forgiving, so this line is the only evidence that\n # a bundled uv is present and failing for every user (QA Q2).\n UV_STATUS=$?\n echo "WARNING: the bundled uv is present but its install failed (exit ${UV_STATUS}); retrying with pip, which is what this script used before uv was bundled."\n fi\nelse\n echo "Bundled uv not available (LOCAL_OPERATOR_UV_BIN=${UV_BIN:-unset}); installing with pip."\nfi\n\nif [ "${UV_INSTALLED}" != true ]; then\n echo "Upgrading pip..."\n python -m pip install --upgrade pip || {\n echo "WARNING: Failed to upgrade pip. Will try to continue with existing pip version."\n pip --version\n }\n\n echo "Installing local-operator package..."\n python -m pip install --verbose local-operator || {\n echo "ERROR: Failed to install local-operator package. Exit code: $?"\n echo "Python version:"\n python --version\n echo "pip version:"\n pip --version\n echo "Available pip packages:"\n pip list\n exit 1\n }\nfi\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n log "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator" || true\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n \n # Try to run the version command with full error output\n log "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n log "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n log "Binary details:"\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n log "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator" || true\n log ""\n log "This could be due to missing dependencies or architecture incompatibility."\n log "Try running the binary manually to see specific errors."\n \n # Don\'t exit here - just warn the user\n log "WARNING: Installation completed but binary verification failed."\n }\n \n # Create a summary of the installation\n SUMMARY_FILE="${APP_DATA_DIR}/installation-summary-${TIMESTAMP}.txt"\n {\n echo "=== Local Operator Backend Installation Summary ==="\n echo "Timestamp: $(date)"\n echo "Python Version: $("${PYTHON_BIN}" --version 2>&1)"\n echo "Virtual Environment: ${VENV_PATH}"\n echo "Binary Location: ${VENV_PATH}/bin/local-operator"\n echo "To activate the virtual environment, run: source ${VENV_PATH}/bin/activate"\n echo "To test the installation, run: ${VENV_PATH}/bin/local-operator --version"\n echo "Log file: ${LOG_FILE}"\n echo "=================================================="\n } > "${SUMMARY_FILE}"\n \n log "Installation summary saved to: ${SUMMARY_FILE}"\nelse\n log "Error: Failed to install Local Operator backend. Binary not found."\n log "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/" || true\n log "Check the log file for details: ${LOG_FILE}"\n error_exit "Installation failed. Binary not installed properly."\nfi\n\nlog "Installation completed successfully at $(date)"\n\n# Clean up temporary files now that installation is complete\ncleanup\n\n# Print final instructions\nlog ""\nlog "=== INSTALLATION COMPLETE ==="\nlog "You can now use the Local Operator backend by running: ${VENV_PATH}/bin/local-operator"\nlog "Installation directory: ${VENV_PATH}"\nlog "Log file: ${LOG_FILE}"\nlog "================================"\n';
9774
+ const macosInstallScriptRaw = '#!/bin/bash\n# Local Operator Backend Installation Script for macOS\n# This script uses the bundled standalone Python and sets up a virtual environment for the Local Operator backend.\n\nset -e # Exit immediately if a command exits with a non-zero status\n\n# Configuration\nAPP_NAME="Local Operator"\nVENV_NAME="local-operator-venv"\nAPP_DATA_DIR="${LOCAL_OPERATOR_SUPPORT_PATH:-$HOME/Library/Application Support/$APP_NAME}"\nVENV_PATH="$APP_DATA_DIR/$VENV_NAME"\nLOG_FILE="$APP_DATA_DIR/backend-install.log"\n\n# Which environment this installs into. The path is the app\'s decision, not this\n# script\'s: a packaged install and an unpackaged one must not share an\n# environment (the venv is built on whatever interpreter the instance resolves,\n# and the installed bundle\'s stdlib lives inside the code-sealed .app), and only\n# the app knows which one it is. The app passes its answer in\n# (`LOCAL_OPERATOR_VENV_PATH`, set from `managedVenvPath` in\n# src/main/backend/venv-paths.ts).\n#\n# macOS REFUSES a standalone run rather than falling back, and the asymmetry with\n# the Linux and Windows scripts is deliberate. Their default is the packaged name,\n# which is harmless there; here it is the exact environment this split exists to\n# stop a second instance from writing into - the measured failure is a dev-venv\n# interpreter whose stdlib is `/Applications/Local Operator.app/Contents/\n# Resources/python_aarch64`, so a silent default would rebuild that venv and\n# `pip install` into it, which is what R1 of the review caught. A caller that\n# cannot name the environment is a caller that should not be installing into one.\n: "${LOCAL_OPERATOR_VENV_PATH:?Pass the resolved managed environment path (see venv-paths.ts); this script will not guess which instance it belongs to}"\nVENV_PATH="$LOCAL_OPERATOR_VENV_PATH"\n\n# Keep CPython\'s bytecode cache out of the application bundle.\n#\n# The bundled interpreter writes __pycache__/*.pyc beside the stdlib sources it\n# imports, and those sources live inside the code-signed .app - which is the\n# build\'s own extraResource. Every such write is a change to a sealed resource:\n# measured on an installed 0.17.3, `codesign --verify --deep` reported 308\n# `file added:` violations, all of them .pyc, and ShipIt then refuses the\n# in-place update with -67028 errSecCSBadBundleFormat. A file codesign reports\n# as *added* can be deleted and the seal comes back; a *modified* one cannot,\n# which is why nothing may ship a .pyc at all. The app sets this variable when\n# it spawns us; defaulting it here keeps a standalone run of this script in\n# agreement with the app about where bytecode goes. It is not a complete answer:\n# the app does not spawn every python that runs this interpreter, and a python\n# started by something else - a shell, a CLI script, an agent - has no prefix at\n# all (measured: a venv over this tree wrote 25 .pyc into it that way). The half\n# that covers those is the app\'s seal on the tree itself\n# (src/main/python-bytecode-cache.ts), which a standalone run of this script does\n# not apply. $HOME is the right place for the cache for the same reason the venv\n# lives there: it is never inside the thing that gets signed and swapped.\n: "${PYTHONPYCACHEPREFIX:=$APP_DATA_DIR/python-bytecode-cache}"\nexport PYTHONPYCACHEPREFIX\n\n# And the refusal half of the same pair, so a standalone run of this script\n# cannot write bytecode into the bundle even where the redirect above does not\n# apply - a relative or unwritable prefix, or a child that drops the variable.\n# CPython reads the flag before its first import, so this script\'s own\n# `python -m venv`, its pip runs and the venv they create all compile without\n# writing a `__pycache__` anywhere. Measured on CPython: `0` and the empty\n# string are the two falsy spellings, so the value is set rather than merged\n# with whatever the caller had.\nexport PYTHONDONTWRITEBYTECODE=1\n\n# The architecture block that used to live here computed and logged the name of\n# a directory for an in-bundle search this script no longer performs: every run\n# announced which architecture-named directory it was about to use, and then\n# installed from `PYTHON_BIN` anyway - the line stated the opposite of how the\n# script finds Python, and the names it computed were used nowhere else (review\n# N1). Which interpreter to build the environment with is the caller\'s decision,\n# checked immediately below.\n\n# The app prepares a complete external runtime before invoking this script.\n# Searching /Applications here would reintroduce legacy-bundle execution during\n# migration. Standalone callers must make the same explicit path decision.\n: "${PYTHON_BIN:?Pass an external prepared Python executable}"\ncase "$PYTHON_BIN" in\n *.app/*) echo "Refusing to execute Python inside an application bundle" >&2; exit 1 ;;\nesac\n\n# Create app data directory if it doesn\'t exist\nmkdir -p "$APP_DATA_DIR"\n\n# Start logging - capture everything and ensure it\'s visible to parent process\nexec > >(tee -a "$LOG_FILE") 2> >(tee -a "$LOG_FILE" >&2)\necho "=============================================="\necho "$(date): Starting Local Operator backend installation..."\necho "=============================================="\necho "Python bin path: $PYTHON_BIN"\necho "Virtual environment path: $VENV_PATH"\necho "App data directory: $APP_DATA_DIR"\necho "Log file: $LOG_FILE"\necho "=============================================="\n\n# Nothing is fetched here but the package itself.\n#\n# This script used to download a third-party FFmpeg binary from a GitHub\n# release into `$APP_DATA_DIR/bin`. The macOS asset names it asked for do not\n# exist (curl without `--fail` wrote the 404 body to the binary path, `chmod +x`\n# made it executable, its own `[ -f ] && [ -x ]` verification passed and the next\n# run skipped the download, so the broken file was permanent), nothing in the app\n# or in `local-operator` ever executed it, and under `set -e` a failed download\n# killed the install before the venv existed - so a machine that can reach PyPI\n# but not github.com could not install at all. Tooling a task actually needs is\n# acquired later, on demand, through the app\'s Console with the user\'s approval;\n# this script\'s job is the environment below and nothing else.\n# Verify bundled Python exists\nif [ ! -f "$PYTHON_BIN" ]; then\n echo "Error: Bundled Python not found at $PYTHON_BIN"\n echo "Please ensure standalone Python is properly installed in the application resources."\n exit 1\nfi\n\n# Make sure Python binary is executable\necho "Using bundled Python: $PYTHON_BIN"\n"$PYTHON_BIN" --version\n\n# Check if Python has venv module available\necho "Checking for venv module availability..."\n"$PYTHON_BIN" -m venv --help > /dev/null 2>&1 || {\n echo "ERROR: Python venv module not available in the Python installation"\n echo "Python details:"\n "$PYTHON_BIN" --version\n "$PYTHON_BIN" -c "import sys; print(\'Prefix:\', sys.prefix); print(\'Exec Prefix:\', sys.exec_prefix)"\n "$PYTHON_BIN" -c "import sys; print(\'Modules path:\'); print(\'\\n\'.join(sys.path))"\n exit 1\n}\necho "venv module is available"\n\n# Create virtual environment if it doesn\'t exist\nif [ ! -d "$VENV_PATH" ]; then\n echo "Creating virtual environment at $VENV_PATH..."\n # Never repair a path we did not create. Preparation allocates a fresh final\n # pathname; a collision is evidence to preserve, not a reason to delete it.\n if [ -e "$VENV_PATH" ] || [ -L "$VENV_PATH" ]; then\n echo "Refusing to replace an existing environment path: $VENV_PATH" >&2\n exit 1\n fi\n \n # Make sure parent directory exists and is writable\n mkdir -p "$(dirname "$VENV_PATH")"\n \n # Create the virtual environment with verbosity\n "$PYTHON_BIN" -m venv "$VENV_PATH" || {\n echo "ERROR: Failed to create virtual environment. Exit code: $?"\n echo "Virtual environment path: $VENV_PATH"\n echo "Python binary used: $PYTHON_BIN"\n ls -la "$(dirname "$VENV_PATH")"\n echo "Python executable permissions:"\n ls -la "$PYTHON_BIN"\n exit 1\n }\n echo "Successfully created virtual environment"\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ] || [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Virtual environment is missing critical components"\n echo "Contents of virtual environment directory:"\n ls -la "$VENV_PATH"\n if [ -d "$VENV_PATH/bin" ]; then\n echo "Contents of bin directory:"\n ls -la "$VENV_PATH/bin"\n fi\n exit 1\nfi\necho "Virtual environment structure verified"\n\n# --- The package install: uv when there is one, pip otherwise ------------------\n#\n# WHY UV. Installing the backend is the phase a user waits through on a first\n# run, and pip spends it resolving and fetching serially. Measured on this\n# machine, cold cache, three runs each, the same interpreter and dependency set:\n# pip\'s package install is 33.0-40.7 s against uv\'s 12.8-16.1 s, plus the\n# 2.3-2.8 s `pip install --upgrade pip` the uv path skips. QA\'s independent pair\n# on a quieter box was 33.9 s against 22.8 s, so read the ratio as 1.5-2.8x\n# ACROSS those two operators, and the seconds as this box\'s. Warm, a retry or a\n# repair: 15.9-33.8 s against 0.65-1.57 s.\n#\n# THE FIGURES THIS COMMENT USED TO QUOTE ARE WITHDRAWN, and this note is here so\n# they are not restored: "pip 128.9 s against uv 14.8 s, 178.3 s against 4.5 s\n# warm". The pip reading was taken at load ~90 and did not reproduce at five\n# further attempts. A comment that argues from a withdrawn number is how the next\n# maintainer decides on a ratio five times the measured one - and the decision it\n# argues is whether ~16-20 MiB per artifact earns its place, so the number is\n# load-bearing. `docs/BUILD.md` carries the full set.\n#\n# WHY A FALLBACK RATHER THAN UV ALONE. uv is a NEW resource in the bundle, and\n# every artifact built before this change has none. The pip path below is the one\n# that shipped until now, unchanged, and it runs whenever uv is absent or cannot\n# do the job - a dev checkout whose `pnpm setup-python` was never run, an older\n# artifact, a uv the platform refuses to spawn, a uv install that failed. A\n# fallback that has never been exercised is a claim rather than a feature, which\n# is why the CI install-script jobs run this script with no uv at all.\n#\n# WHY pip STAYS IN THE VENV, and why this does NOT use `uv venv`: the app\'s\n# backend-update path runs `pip install --upgrade local-operator` inside this same\n# environment (`update-service.ts`, and `update-install.ts` documents why pip is\n# the right command there). `python -m venv` seeds pip from the interpreter\'s own\n# `ensurepip` wheel, where `uv venv` produces an environment with no pip at all -\n# so switching the creation would silently break every later update. The check\n# above (`"$VENV_PATH/bin/pip"`) is what holds that on both paths.\n#\n# The app hands the path of the pinned, bundled uv in `LOCAL_OPERATOR_UV_BIN`\n# (`src/main/backend/uv-tool.ts`). Nothing here searches PATH for a uv: an\n# installed uv is a version and a configuration nobody in this repository chose,\n# and the point of bundling one is that the install is the same for every user.\nUV_BIN="${LOCAL_OPERATOR_UV_BIN:-}"\n\n# Drop every UV_* variable the launching environment carried.\n#\n# WHY THE WHOLE NAMESPACE rather than a list of the dangerous ones: uv reads its\n# configuration from `UV_*` and from `uv.toml`, and both are the caller\'s, not\n# this app\'s. Measured on uv 0.12.17: with a user-level `uv.toml` naming an index\n# that is not reachable, `uv pip install --dry-run six` fails with `tcp connect\n# error`; `UV_NO_CONFIG=1` makes the same command resolve from PyPI. And an\n# ambient `UV_INDEX_URL` changes where packages come from - while `PIP_INDEX_URL`\n# does not affect uv at all (measured, both directions). Unsetting a name list\n# would drift the day uv adds a variable; unsetting the namespace cannot.\n#\n# IT RUNS BEFORE THE SETTINGS BELOW ARE SET, and that order is load-bearing: a\n# sweep after them takes them away, and an empty `UV_CACHE_DIR` is not "use the\n# default cache" - uv exits 2 with `a value is required for \'--cache-dir\n# <CACHE_DIR>\'`. Measured by running this script on the uv path, where the\n# failure first surfaced as a silent pip install, because the fallback below\n# caught it exactly as designed.\nfor uv_ambient in $(env | sed -n \'s/^\\(UV_[A-Za-z0-9_]*\\)=.*/\\1/p\'); do\n unset "$uv_ambient"\ndone\n\n# The cache lives under the app\'s own support directory rather than the user\'s\n# shared `~/.cache/uv`, so the install neither reads nor pollutes a cache that\n# another tool (or another version of uv) is maintaining. Written here, after the\n# sweep, and handed to uv per invocation rather than exported.\n#\n# IT PERSISTS, and that is worth knowing on a user\'s disk: a full install leaves\n# ~118 MB there (measured; pip\'s own cache for the same dependency set is ~40 MB\n# and it also persists). Nothing else reads it today - the app\'s backend-update\n# path installs with pip - so it is there for the next provisioning or repair,\n# and it is what makes a retry converge in ~1.5 s instead of ~20 s.\nUV_CACHE_DIR="$APP_DATA_DIR/uv-cache"\n\n# Is the handed-down uv something we can actually run?\nuv_is_usable() {\n [ -n "$UV_BIN" ] && [ -x "$UV_BIN" ] && "$UV_BIN" --version >/dev/null 2>&1\n}\n\n# UV_NO_CONFIG: never read `pyproject.toml`/`uv.toml`, wherever they are.\n# UV_PYTHON_DOWNLOADS=never: this install uses the interpreter it was handed and\n# may never fetch another, which is also what keeps it working offline.\n# UV_CACHE_DIR: this install\'s own cache, passed explicitly for the same reason.\nuv_run() {\n UV_NO_CONFIG=1 UV_PYTHON_DOWNLOADS=never UV_CACHE_DIR="$UV_CACHE_DIR" \\\n "$UV_BIN" "$@"\n}\n\n# Activate virtual environment and install local-operator\necho "Installing local-operator in virtual environment..."\nsource "$VENV_PATH/bin/activate"\n\n# Check network connectivity to PyPI. A DIAGNOSTIC, not a gate: the install below\n# decides whether it can proceed.\n#\n# What this answers, stated precisely because an earlier version of this comment\n# claimed more than the flags buy: "did a TLS fetch to PyPI\'s JSON API complete,\n# and did the answer come back as JSON?". `--fail` turns an HTTP ERROR status\n# into a non-zero exit - a proxy\'s 403/407, any 4xx/5xx - and does NOT notice a\n# captive portal answering 200 with its own HTML page (measured: a portal-shaped\n# 200 returns exit 0 with AND without the flag). `-o /dev/null` cannot tell a\n# portal\'s page from PyPI\'s JSON either, so the content type is what\n# discriminates, and on a captive network a probe that reports "reachable" while\n# pip is about to fail is the false negative this warning exists to catch.\n#\n# What the content type does NOT prove, stated so this paragraph is not read as\n# more than it says: a proxy answering 200 with `application/json` and an error\n# body (`{"detail":"blocked by proxy policy"}`) is silent here, because the\n# answer did come back as JSON. Only parsing the body - a fetch of PyPI\'s own\n# payload shape - would tell those apart, and a diagnostic that costs a parse is\n# not what stands in front of an install.\n#\n# Two bounds, two jobs: `--connect-timeout 5` ends a black-hole network (a\n# connect that never completes), `--max-time 30` stops a connected-but-stalled\n# peer. Both must be POSITIVE: `--max-time 0` and `--connect-timeout 0` disable\n# the bound rather than making it immediate, which is why the test beside this\n# script requires `[1-9]`. 30 rather than 10 because the total must not fire on a\n# slow-but-working link: a working endpoint that answered in 15s tripped a 10s\n# total bound and printed this warning on an install that then succeeded, and a\n# warning that cries wolf is one users learn to ignore.\necho "Checking network connectivity to PyPI..."\nPYPI_PROBE_CONTENT_TYPE=$(curl -s --fail --connect-timeout 5 --max-time 30 -o /dev/null -w \'%{content_type}\' https://pypi.org/pypi/local-operator/json) || PYPI_PROBE_CONTENT_TYPE=""\nif [[ "$PYPI_PROBE_CONTENT_TYPE" != application/json* ]]; then\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n echo "Attempting to ping common domains to diagnose network issues:"\n ping -c 1 -W 2000 google.com || echo "Cannot ping google.com"\n ping -c 1 -W 2000 pypi.org || echo "Cannot ping pypi.org"\nfi\n\nUV_INSTALLED=false\nif uv_is_usable; then\n echo "Installing local-operator with uv ($("$UV_BIN" --version 2>/dev/null || echo \'version unavailable\'))..."\n # No `pip install --upgrade pip` on this path: uv does not use pip, so the\n # upgrade would be a whole extra network round trip that changes nothing about\n # the result.\n if uv_run pip install --python "$VENV_PATH/bin/python" --upgrade local-operator; then\n UV_INSTALLED=true\n echo "local-operator installation with uv successful"\n else\n # WHY THE EXIT CODE IS PRINTED (review round 1, QA Q2): this fallback has to\n # be forgiving - an install must not fail because uv did - but "a bundled uv\n # is present and fails" is a defect rather than a degraded path, and this line\n # is the only place it shows up: the exit code is 0 and the UI is unchanged.\n # `uv_is_usable` passing and a uv install SUCCEEDING are two different facts.\n UV_STATUS=$?\n echo "WARNING: the bundled uv is present but its install failed (exit ${UV_STATUS}); retrying with pip, which is what this script used before uv was bundled."\n fi\nelse\n echo "Bundled uv not available (LOCAL_OPERATOR_UV_BIN=${UV_BIN:-unset}); installing with pip."\nfi\n\nif [ "$UV_INSTALLED" != true ]; then\n echo "Upgrading pip..."\n python -m pip install --upgrade pip || {\n echo "ERROR: Failed to upgrade pip. Exit code: $?"\n echo "pip version before failing:"\n pip --version\n exit 1\n }\n echo "pip upgrade successful:"\n pip --version\n\n echo "Installing local-operator package..."\n python -m pip install --upgrade --verbose local-operator || {\n echo "ERROR: Failed to install local-operator package. Exit code: $?"\n echo "Python version:"\n python --version\n echo "pip version:"\n pip --version\n echo "Available pip packages:"\n pip list\n echo "Pip config:"\n pip config list\n echo "Network diagnosis:"\n curl -sI --fail --connect-timeout 5 --max-time 30 https://pypi.org || echo "Cannot reach PyPI server"\n exit 1\n }\nfi\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n echo "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator"\n file "$VENV_PATH/bin/local-operator"\n \n # Try to run the version command with full error output\n echo "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n echo "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n echo "Binary details:"\n file "$VENV_PATH/bin/local-operator"\n echo "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator"\n echo "Dependencies:"\n if command -v otool >/dev/null; then\n otool -L "$VENV_PATH/bin/local-operator" || echo "Could not get dependencies with otool"\n fi\n exit 1\n }\nelse\n echo "Error: Failed to install Local Operator backend. Binary not found."\n echo "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/"\n exit 1\nfi\n\necho "$(date): Installation completed successfully."\n';
9775
+ const windowsInstallScriptRaw = '# Local Operator Backend Installation Script for Windows\n# This script installs pyenv-win, Python 3.12, and sets up a virtual environment for the Local Operator backend.\n\n# Configuration\n$AppName = "Local Operator"\n$PythonVersion = "3.12.0"\n$VenvName = "local-operator-venv"\n$AppDataDir = "$env:APPDATA\\\\$AppName"\n$VenvPath = "$AppDataDir\\\\$VenvName"\n$LogFile = "$AppDataDir\\\\backend-install-shell.log"\n$PyenvDir = "$env:USERPROFILE\\\\.pyenv"\n\n# Which environment this installs into - the app\'s decision, handed in rather\n# than re-derived. A packaged install and an unpackaged one must not share an\n# environment (the venv is built on whatever interpreter the instance resolves),\n# and only the app knows which it is: LOCAL_OPERATOR_VENV_PATH carries\n# managedVenvPath\'s answer (src/main/backend/venv-paths.ts). The default above is\n# what a standalone run uses - the packaged name, because that is what every\n# install on a disk today has.\nif ($env:LOCAL_OPERATOR_VENV_PATH) {\n $VenvPath = $env:LOCAL_OPERATOR_VENV_PATH\n}\n\n# Keep CPython\'s bytecode cache out of the application directory.\n#\n# Same rule as the macOS script, and stated here for the same reason: an\n# interpreter writes __pycache__/*.pyc beside the stdlib sources it imports. On\n# Windows those sources are the bundled python directory rather than a\n# code-signed bundle, so this is hygiene rather than the load-bearing fix it is\n# on macOS - but the app and the script must not disagree about where bytecode\n# goes. $AppDataDir is used for the same reason the venv is: it is never inside\n# the installed application tree.\nif (-not $env:PYTHONPYCACHEPREFIX) {\n $env:PYTHONPYCACHEPREFIX = "$AppDataDir\\\\python-bytecode-cache"\n}\n\n# The refusal half of the same pair, kept in step with the app\'s\n# `withPythonBytecodeCache`: CPython reads the flag before its first import, so\n# nothing this script runs can write a __pycache__ at all.\n$env:PYTHONDONTWRITEBYTECODE = "1"\n\n# Create app data directory if it doesn\'t exist\nif (-not (Test-Path $AppDataDir)) {\n New-Item -ItemType Directory -Path $AppDataDir -Force | Out-Null\n}\n\n# Start logging\nStart-Transcript -Path $LogFile -Append\nWrite-Output "$(Get-Date): Starting Local Operator backend installation..."\n\n# Nothing is fetched here but the package itself and Python\'s own toolchain.\n#\n# This script used to download a third-party FFmpeg binary from a GitHub release\n# into `$AppDataDir\\bin`. Nothing in the app or in `local-operator` ever executed\n# it. Tooling a task actually needs is acquired later, on demand, through the\n# app\'s Console with the user\'s approval; this script\'s job is the environment\n# below and nothing else. (pyenv-win\'s source archive below is the one remaining\n# third-party fetch, and it is a source archive the Windows install cannot do\n# without - see `scripts/install-scripts-network.test.mjs`, which keeps that list\n# down to the fetches each platform genuinely needs.)\n\n# Function to check if a command exists\nfunction Test-CommandExists {\n param ($command)\n $oldPreference = $ErrorActionPreference\n $ErrorActionPreference = \'stop\'\n try {\n if (Get-Command $command) { return $true }\n } catch {\n return $false\n } finally {\n $ErrorActionPreference = $oldPreference\n }\n}\n\n# Install pyenv-win if not installed\nif (-not (Test-Path $PyenvDir)) {\n Write-Output "Installing pyenv-win..."\n \n # Create temporary directory\n $TempDir = "$env:TEMP\\\\pyenv-win"\n if (Test-Path $TempDir) {\n Remove-Item -Path $TempDir -Recurse -Force\n }\n New-Item -ItemType Directory -Path $TempDir -Force | Out-Null\n \n # Download and extract pyenv-win\n $PyenvZip = "$TempDir\\\\pyenv-win.zip"\n # Bounded, and the bound FAILS LOUDLY. An unbounded request here holds a\n # first-run install open behind the progress bar forever on a black-hole\n # network; and without -ErrorAction Stop a fired -TimeoutSec is a\n # NON-TERMINATING error, so the script would walk straight into\n # Expand-Archive with an absent or partial zip and report an archive error\n # instead of "the download timed out". The partial file is removed in the\n # failure branch so a later run cannot expand what this one failed to fetch\n # (the next run clears $TempDir before it downloads at all, which is the\n # `if (Test-Path $TempDir) { Remove-Item ... }` above - named rather than\n # cited by line, because a line number in a script that keeps changing is\n # what a stale citation is made of).\n # 120 seconds is a payload bound rather than the 30-second stall bound the PyPI\n # probes use: this downloads a source archive instead of answering an API\n # call, so it only has to stop an indefinite hang.\n #\n # WHICH BOUND `-TimeoutSec` ACTUALLY IS DEPENDS ON THE POWERSHELL, and that is\n # a trap worth naming because the two paths differ here. The app spawns this\n # script with `powershell.exe` (Windows PowerShell 5.1, see\n # backend-installer.ts), where -TimeoutSec is the REQUEST\'s timeout - 120\n # seconds to complete the transfer. On PowerShell 7.4+ it was renamed to\n # -OperationTimeoutSeconds and -TimeoutSec survives only as an ALIAS of\n # -ConnectionTimeoutSeconds, i.e. a connect bound, so on a 7.x host (the CI\n # runner is one) a mirror that accepts and then stalls is not ended by this.\n # Do not "fix" that by adding the 7.x spelling: 5.1 does not know\n # -OperationTimeoutSeconds, and an unknown parameter is a binding error which\n # the catch below turns into `exit 1` on every install. The version-agnostic\n # answer is a bound on the transfer itself (a BITS job or a size/rate check),\n # which is a larger change than this one and is recorded rather than made.\n try {\n Invoke-WebRequest -Uri "https://github.com/pyenv-win/pyenv-win/archive/master.zip" -OutFile $PyenvZip -TimeoutSec 120 -ErrorAction Stop\n } catch {\n Remove-Item -Path $PyenvZip -Force -ErrorAction SilentlyContinue\n Write-Error "Failed to download pyenv-win from https://github.com/pyenv-win/pyenv-win/archive/master.zip within 120 seconds: $($_.Exception.Message)"\n exit 1\n }\n # -ErrorAction Stop for the same reason as the download: a truncated archive\n # must fail here rather than half-copy into $PyenvDir.\n Expand-Archive -Path $PyenvZip -DestinationPath $TempDir -ErrorAction Stop\n \n # Create .pyenv directory\n New-Item -ItemType Directory -Path $PyenvDir -Force | Out-Null\n \n # Copy pyenv-win files\n Copy-Item -Path "$TempDir\\\\pyenv-win-master\\\\*" -Destination $PyenvDir -Recurse\n \n # Set environment variables\n [System.Environment]::SetEnvironmentVariable("PYENV", "$PyenvDir\\\\pyenv-win", "User")\n [System.Environment]::SetEnvironmentVariable("PYENV_HOME", "$PyenvDir\\\\pyenv-win", "User")\n \n # Update PATH - ensure both bin and shims are added separately for better compatibility\n $Path = [System.Environment]::GetEnvironmentVariable("PATH", "User")\n $PyenvBinPath = "$PyenvDir\\\\pyenv-win\\\\bin"\n $PyenvShimsPath = "$PyenvDir\\\\pyenv-win\\\\shims"\n \n # Add bin path if not already in PATH\n if ($Path -notlike "*$PyenvBinPath*") {\n [System.Environment]::SetEnvironmentVariable("PATH", "$PyenvBinPath;$Path", "User")\n $Path = [System.Environment]::GetEnvironmentVariable("PATH", "User")\n }\n \n # Add shims path if not already in PATH\n if ($Path -notlike "*$PyenvShimsPath*") {\n [System.Environment]::SetEnvironmentVariable("PATH", "$PyenvShimsPath;$Path", "User")\n }\n \n # Set PYENV environment variables\n [System.Environment]::SetEnvironmentVariable("PYENV", "$PyenvDir\\\\pyenv-win", "User")\n [System.Environment]::SetEnvironmentVariable("PYENV_HOME", "$PyenvDir\\\\pyenv-win", "User")\n \n # Update current session PATH\n $env:PYENV = "$PyenvDir\\\\pyenv-win"\n $env:PYENV_HOME = "$PyenvDir\\\\pyenv-win"\n $env:PATH = "$PyenvDir\\\\pyenv-win\\\\bin;$PyenvDir\\\\pyenv-win\\\\shims;$env:PATH"\n \n # Refresh environment variables for the current process\n $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "User") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "Machine")\n \n # Clean up\n Remove-Item -Path $TempDir -Recurse -Force\n}\n\n# Refresh environment variables for current session\n$env:PYENV = "$PyenvDir\\\\pyenv-win"\n$env:PYENV_HOME = "$PyenvDir\\\\pyenv-win"\n$env:PATH = "$PyenvDir\\\\pyenv-win\\\\bin;$PyenvDir\\\\pyenv-win\\\\shims;$env:PATH"\n\n# Install Python 3.12 if not installed\n$PythonInstalled = $false\ntry {\n $InstalledVersions = & pyenv versions\n if ($InstalledVersions -like "*$PythonVersion*") {\n $PythonInstalled = $true\n }\n} catch {\n $PythonInstalled = $false\n}\n\nif (-not $PythonInstalled) {\n Write-Output "Installing Python $PythonVersion..."\n & pyenv install $PythonVersion\n if ($LASTEXITCODE -ne 0) {\n Write-Error "Failed to install Python $PythonVersion"\n exit 1\n }\n}\n\n# Set Python 3.12 as the local version\n& pyenv local $PythonVersion\nif ($LASTEXITCODE -ne 0) {\n Write-Error "Failed to set Python $PythonVersion as local version"\n exit 1\n}\n\n# Create virtual environment if it doesn\'t exist\nif (-not (Test-Path $VenvPath)) {\n Write-Output "Creating virtual environment at $VenvPath..."\n \n # Ensure the directory exists\n if (-not (Test-Path $AppDataDir)) {\n New-Item -ItemType Directory -Path $AppDataDir -Force | Out-Null\n Write-Output "Created directory: $AppDataDir"\n }\n \n # Use the full path to python from pyenv\n $PythonExe = "$PyenvDir\\\\pyenv-win\\\\versions\\\\$PythonVersion\\\\python.exe"\n \n if (Test-Path $PythonExe) {\n Write-Output "Using Python at: $PythonExe"\n & $PythonExe -m venv $VenvPath\n } else {\n Write-Output "Using system Python"\n & python -m venv $VenvPath\n }\n \n if ($LASTEXITCODE -ne 0) {\n Write-Error "Failed to create virtual environment"\n exit 1\n }\n}\n\n# Verify the virtual environment was created\nif (-not (Test-Path "$VenvPath\\\\Scripts\\\\Activate.ps1")) {\n Write-Error "Virtual environment activation script not found at $VenvPath\\\\Scripts\\\\Activate.ps1"\n exit 1\n}\n\n# --- The package install: uv when there is one, pip otherwise ------------------\n#\n# Same shape as the macOS and Linux scripts, and for the same reasons: uv resolves\n# and fetches in parallel - measured on macOS, cold cache, three runs each, same\n# interpreter: uv\'s package install is 12.8-16.1 s against pip\'s 33.0-40.7 s,\n# plus the 2.3-2.8 s pip self-upgrade this path skips, so 1.5-2.8x across two\n# operators rather than the "14.8 s against 128.9 s" quoted here before that\n# reading was withdrawn (`docs/BUILD.md` has the full set). The pip path below is\n# unchanged and runs whenever uv is absent or cannot do the job, and pip STAYS in the venv\n# because the app\'s backend-update path runs `pip install --upgrade\n# local-operator` inside this same environment - which is also why the venv is\n# still created with `python -m venv` rather than `uv venv`.\n#\n# Nothing here searches PATH for a uv: an installed uv is a version and a\n# configuration nobody in this repository chose. `LOCAL_OPERATOR_UV_BIN` is the\n# app\'s own answer (`src/main/backend/uv-tool.ts`).\n$UvBin = $env:LOCAL_OPERATOR_UV_BIN\n$UvCacheDir = "$AppDataDir\\uv-cache"\n\nfunction Test-UvUsable {\n if (-not $UvBin) { return $false }\n if (-not (Test-Path $UvBin)) { return $false }\n try {\n & $UvBin --version | Out-Null\n return ($LASTEXITCODE -eq 0)\n } catch {\n return $false\n }\n}\n\n# Drop every UV_* variable the launching environment carried, then set the three\n# settings this install depends on. Measured on uv 0.12.17: a user-level\n# `uv.toml` naming an unreachable index is obeyed by `uv pip install` and ignored\n# with `UV_NO_CONFIG=1`; an ambient `UV_INDEX_URL` changes where packages come\n# from, while `PIP_INDEX_URL` does not affect uv at all. A name list would drift\n# the day uv adds a variable - the namespace cannot.\n#\n# The names are MATERIALISED first (`@(...)` over a property projection):\n# removing entries of a collection that is still being enumerated is the shape\n# that throws `Collection was modified`.\n$uvAmbientNames = @(\n Get-ChildItem env: |\n Where-Object { $_.Name -like \'UV_*\' } |\n Select-Object -ExpandProperty Name\n)\nforeach ($uvAmbientName in $uvAmbientNames) {\n Remove-Item "env:$uvAmbientName" -ErrorAction SilentlyContinue\n}\n\n# UV_NO_CONFIG: never read `pyproject.toml`/`uv.toml`, wherever they are.\n# UV_PYTHON_DOWNLOADS=never: this install uses the interpreter it was handed and\n# never fetches another.\n# UV_CACHE_DIR: under the app\'s own support directory rather than the user\'s\n# shared uv cache.\n$env:UV_NO_CONFIG = "1"\n$env:UV_PYTHON_DOWNLOADS = "never"\n$env:UV_CACHE_DIR = $UvCacheDir\n\n# Activate virtual environment and install local-operator\nWrite-Output "Installing local-operator in virtual environment..."\n# Use PowerShell to run the activation script\n& "$VenvPath\\\\Scripts\\\\Activate.ps1"\n\n$UvInstalled = $false\nif (Test-UvUsable) {\n Write-Output "Installing local-operator with uv..."\n # No `pip install --upgrade pip` on this path: uv does not use pip.\n & $UvBin pip install --python "$VenvPath\\Scripts\\python.exe" --upgrade local-operator\n if ($LASTEXITCODE -eq 0) {\n $UvInstalled = $true\n Write-Output "local-operator installation with uv successful"\n } else {\n # The exit code is printed for the same reason as on macOS and Linux: the\n # fallback is deliberately forgiving, so this line is the only evidence\n # that a bundled uv is present and failing for every user (QA Q2).\n Write-Output "WARNING: the bundled uv is present but its install failed (exit $LASTEXITCODE); retrying with pip, which is what this script used before uv was bundled."\n }\n} else {\n if ($UvBin) {\n Write-Output "Bundled uv at $UvBin could not be run on this machine; installing with pip."\n } else {\n Write-Output "Bundled uv not available (LOCAL_OPERATOR_UV_BIN unset); installing with pip."\n }\n}\n\nif (-not $UvInstalled) {\n & python -m pip install --upgrade pip\n & python -m pip install --upgrade local-operator\n\n if ($LASTEXITCODE -ne 0) {\n Write-Error "Failed to install packages in virtual environment"\n exit 1\n }\n}\n\n# Verify installation\n$LocalOperatorInstalled = $false\ntry {\n # First try to run local-operator directly (it should be in PATH from the activated venv)\n $Version = & local-operator --version\n $LocalOperatorInstalled = $true\n Write-Output "Local Operator backend installed successfully!"\n Write-Output $Version\n} catch {\n Write-Output "Could not run local-operator directly, trying with full path..."\n try {\n # Try with explicit path\n $LocalOperatorExe = "$VenvPath\\\\Scripts\\\\local-operator.exe"\n if (Test-Path $LocalOperatorExe) {\n $Version = & $LocalOperatorExe --version\n $LocalOperatorInstalled = $true\n Write-Output "Local Operator backend installed successfully at $LocalOperatorExe!"\n Write-Output $Version\n } else {\n # Try without .exe extension\n $LocalOperatorCmd = "$VenvPath\\\\Scripts\\\\local-operator"\n if (Test-Path $LocalOperatorCmd) {\n $Version = & $LocalOperatorCmd --version\n $LocalOperatorInstalled = $true\n Write-Output "Local Operator backend installed successfully at $LocalOperatorCmd!"\n Write-Output $Version\n } else {\n Write-Error "Error: local-operator executable not found in expected locations."\n \n # Create a symlink to make it more accessible\n Write-Output "Attempting to create a symlink for local-operator..."\n $PythonModule = "$VenvPath\\\\Lib\\\\site-packages\\\\local_operator"\n if (Test-Path $PythonModule) {\n Write-Output "Found local_operator module at $PythonModule"\n \n # Create a batch file that runs the module\n $BatchContent = "@echo off`npython -m local_operator %*"\n Set-Content -Path "$VenvPath\\\\Scripts\\\\local-operator.bat" -Value $BatchContent\n \n # Test the batch file\n if (Test-Path "$VenvPath\\\\Scripts\\\\local-operator.bat") {\n Write-Output "Created local-operator.bat in Scripts directory"\n $LocalOperatorInstalled = $true\n } else {\n Write-Error "Failed to create local-operator.bat"\n exit 1\n }\n } else {\n Write-Error "local_operator module not found in site-packages"\n exit 1\n }\n }\n }\n } catch {\n Write-Error "Error: Failed to install Local Operator backend."\n Write-Error $_.Exception.Message\n exit 1\n }\n}\n\n# If we got here, installation was successful\nif ($LocalOperatorInstalled) {\n Write-Output "Local Operator backend installation verified."\n} else {\n Write-Error "Error: Failed to verify Local Operator backend installation."\n exit 1\n}\n\nWrite-Output "$(Get-Date): Installation completed successfully."\n\n# Properly close the transcript and ensure it\'s released\ntry {\n Write-Output "Finalizing installation and releasing resources..."\n # Flush any pending output\n [System.Console]::Out.Flush()\n # Stop transcript properly\n Stop-Transcript\n \n # Add a small delay to ensure file handles are released\n Start-Sleep -Seconds 1\n \n # Explicitly release any COM objects\n [System.GC]::Collect()\n [System.GC]::WaitForPendingFinalizers()\n \n Write-Output "Installation completed and resources released."\n} catch {\n Write-Error "Error during cleanup: $_"\n}\n';
9825
9776
  const macosInstallScript = macosInstallScriptRaw;
9826
9777
  const linuxInstallScript = linuxInstallScriptRaw;
9827
9778
  const windowsInstallScript = windowsInstallScriptRaw;
@@ -9856,6 +9807,57 @@ function setupFailureCause(error) {
9856
9807
  const raw = error instanceof Error ? error.message : String(error);
9857
9808
  return SETUP_FAILURE_CAUSES.find(([pattern]) => pattern.test(raw))?.[1] ?? null;
9858
9809
  }
9810
+ const UV_TOOL_ENV = "LOCAL_OPERATOR_UV_BIN";
9811
+ const UV_TOOL_MODE = 493;
9812
+ const OWNER_EXECUTE$1 = 64;
9813
+ function binaryName(platform) {
9814
+ const name = RUNTIME_LAYOUT.uv.binaryNames[platform];
9815
+ if (!name)
9816
+ throw new Error(
9817
+ `No bundled uv binary name is defined for platform "${platform}"; src/shared/bundled-runtime-layout.json names ${Object.keys(RUNTIME_LAYOUT.uv.binaryNames).join(", ")}`
9818
+ );
9819
+ return name;
9820
+ }
9821
+ function uvToolPath(options) {
9822
+ const platform = options.platform ?? process.platform;
9823
+ const name = binaryName(platform);
9824
+ const directory = options.packaged ? path$1.join(options.resources, RUNTIME_LAYOUT.uv.namespace, options.arch) : options.resources;
9825
+ const path2 = options.packaged ? path$1.join(directory, name) : path$1.join(
9826
+ directory,
9827
+ RUNTIME_LAYOUT.uv.checkoutNames[options.arch] ?? "",
9828
+ name
9829
+ );
9830
+ try {
9831
+ fs.accessSync(path2, fs.constants.F_OK);
9832
+ } catch {
9833
+ return null;
9834
+ }
9835
+ return fs.statSync(path2).isFile() ? path2 : null;
9836
+ }
9837
+ function ensureUvToolExecutable(options) {
9838
+ const path2 = uvToolPath(options);
9839
+ if (path2 === null)
9840
+ return {
9841
+ path: null,
9842
+ healed: false,
9843
+ mode: null,
9844
+ reason: "no bundled uv is staged for this architecture (a dev checkout whose `pnpm setup-python` has not run, or an artifact built before uv was bundled)"
9845
+ };
9846
+ const mode = fs.statSync(path2).mode & 511;
9847
+ if ((mode & OWNER_EXECUTE$1) !== 0)
9848
+ return { path: path2, healed: false, mode, reason: null };
9849
+ try {
9850
+ fs.chmodSync(path2, UV_TOOL_MODE);
9851
+ return { path: path2, healed: true, mode: UV_TOOL_MODE, reason: null };
9852
+ } catch (error) {
9853
+ return {
9854
+ path: null,
9855
+ healed: false,
9856
+ mode,
9857
+ reason: `the bundled uv at ${path2} has mode ${mode.toString(8)} and could not be repaired (${error instanceof Error ? error.message : String(error)})`
9858
+ };
9859
+ }
9860
+ }
9859
9861
  function backendSetupFailureDetail(error, support) {
9860
9862
  const raw = error instanceof Error ? error.message : String(error);
9861
9863
  const cause = setupFailureCause(error) ?? "The backend could not be set up on this Mac.";
@@ -10028,9 +10030,9 @@ class BackendInstaller {
10028
10030
  );
10029
10031
  await prepareManagedPython(
10030
10032
  this.managedOptions(),
10031
- async (venv, python) => {
10033
+ async (venv, python2) => {
10032
10034
  this.venvPath = venv;
10033
- this.pythonPath = python;
10035
+ this.pythonPath = python2;
10034
10036
  return this.installEnvironment();
10035
10037
  }
10036
10038
  );
@@ -10225,6 +10227,19 @@ class BackendInstaller {
10225
10227
  LogFileType.INSTALLER
10226
10228
  );
10227
10229
  }
10230
+ const uv2 = ensureUvToolExecutable(this.managedOptions());
10231
+ if (uv2.path !== null) {
10232
+ env[UV_TOOL_ENV] = uv2.path;
10233
+ logger.info(
10234
+ uv2.healed ? `Restored the execute bit on the bundled uv (${uv2.path}) before handing it to the installer; the mode a bundle arrives with is not covered by codesign's seal` : `Setting ${UV_TOOL_ENV} to ${uv2.path}`,
10235
+ LogFileType.INSTALLER
10236
+ );
10237
+ } else {
10238
+ logger.info(
10239
+ `No bundled uv for ${process.arch}: ${uv2.reason}`,
10240
+ LogFileType.INSTALLER
10241
+ );
10242
+ }
10228
10243
  installProcess = node_child_process.spawn(cmd, args, {
10229
10244
  detached: process.platform !== "win32",
10230
10245
  // Detach on Unix-like systems for process group
@@ -12632,11 +12647,6 @@ function surfaceEnvironment(input) {
12632
12647
  }
12633
12648
  return env;
12634
12649
  }
12635
- function defaultShell(env) {
12636
- const shell = env.SHELL?.trim();
12637
- if (shell && path$1.isAbsolute(shell)) return shell;
12638
- return "/bin/zsh";
12639
- }
12640
12650
  function resolveCwd(cwd) {
12641
12651
  const wanted = cwd?.trim();
12642
12652
  if (wanted && path$1.isAbsolute(wanted)) return wanted;
@@ -13615,6 +13625,15 @@ async function startConsoleHost(options) {
13615
13625
  log,
13616
13626
  history,
13617
13627
  now: options.now,
13628
+ // Resolved BEFORE the host exists, so no surface can be created against the
13629
+ // launch environment and then start behaving like the user's terminal later:
13630
+ // a surface's PATH is decided here, once, and every surface this host makes
13631
+ // carries the same one. Bounded by the resolver's own timeout (it never
13632
+ // rejects and answers null when the shell cannot speak), and no part of the
13633
+ // app's own startup waits on it: the browser host is attached to the window
13634
+ // without being awaited, so the cost of a slow rc here is this host coming
13635
+ // online later, not the app failing to come up.
13636
+ env: await withUserShellPath(process.env, options.userShellPath),
13618
13637
  captureOffscreen: capture ? (request) => capture.capture(request) : void 0,
13619
13638
  spawn: options.spawn ?? ((spawnOptions) => spawnPty(load.module, spawnOptions, (report) => {
13620
13639
  log(
@@ -20668,6 +20687,7 @@ async function startBrowserHost(options) {
20668
20687
  */
20669
20688
  consoleCaptureUrl: options.consoleCaptureUrl,
20670
20689
  preloadPath: options.preloadPath,
20690
+ userShellPath: options.userShellPath,
20671
20691
  log
20672
20692
  });
20673
20693
  const sessionKey = mintSessionKey();
@@ -26365,10 +26385,10 @@ ${heal.removed.join("\n")}`,
26365
26385
  if (identity.path && classifyGlobalInstall(identity) !== "global-unknown") {
26366
26386
  return identity;
26367
26387
  }
26368
- const uv = resolveCommandPath("uv");
26388
+ const uv2 = resolveCommandPath("uv");
26369
26389
  const pipx = resolveCommandPath("pipx");
26370
26390
  const [uvToolList, pipxList] = await Promise.all([
26371
- uv ? this.probeInstallerList(uv, ["tool", "list"]) : null,
26391
+ uv2 ? this.probeInstallerList(uv2, ["tool", "list"]) : null,
26372
26392
  pipx ? this.probeInstallerList(pipx, ["list"]) : null
26373
26393
  ]);
26374
26394
  identity.uvToolList = uvToolList;
@@ -28126,7 +28146,7 @@ ${heal.removed.join("\n")}`,
28126
28146
  try {
28127
28147
  outcome = await updateManagedPython(
28128
28148
  options,
28129
- (venv, python) => this.installEnvironmentInto(venv, python, target2),
28149
+ (venv, python2) => this.installEnvironmentInto(venv, python2, target2),
28130
28150
  {
28131
28151
  target: target2,
28132
28152
  isNewer: (candidate, subject) => this.isNewerVersion(candidate, subject)
@@ -28305,8 +28325,8 @@ ${reason}` : ""}`,
28305
28325
  * space reached the reader with no disk-space remedy on it. Throwing the child's
28306
28326
  * captured output keeps that table's matching text intact end to end.
28307
28327
  */
28308
- async installEnvironmentInto(venv, python, target2) {
28309
- const created = await runCommand(python, ["-m", "venv", venv], {
28328
+ async installEnvironmentInto(venv, python2, target2) {
28329
+ const created = await runCommand(python2, ["-m", "venv", venv], {
28310
28330
  timeoutMs: 10 * 60 * 1e3,
28311
28331
  env: this.pythonSpawnEnv()
28312
28332
  });
@@ -29225,7 +29245,10 @@ function createWindow(initialSession = null, openCatalogue = false, request = ow
29225
29245
  attachBrowserHostToWindow?.(mainWindow2);
29226
29246
  return mainWindow2;
29227
29247
  }
29228
- const backendService = new BackendServiceManager();
29248
+ const userShellPath = createUserShellPath({
29249
+ log: (message) => logger.info(message, LogFileType.BACKEND)
29250
+ });
29251
+ const backendService = new BackendServiceManager({ userShellPath });
29229
29252
  const backendInstaller = new BackendInstaller();
29230
29253
  const reportBackendFailure = (message, fileType) => {
29231
29254
  if (backendService.isShuttingDown()) {
@@ -30132,6 +30155,14 @@ electron.app.whenReady().then(async () => {
30132
30155
  // lets main feed the reconstruction to it.
30133
30156
  consoleCaptureUrl,
30134
30157
  preloadPath: path$1.join(__dirname, "../preload/index.js"),
30158
+ /*
30159
+ * The console's surfaces are handed the user's own PATH, from the same
30160
+ * resolver the backend spawn uses (above). Without it a surface inherits
30161
+ * this app's launchd environment, in which `brew` - and every other tool
30162
+ * the user installed - does not exist, so the console cannot run the
30163
+ * tooling the agent is meant to acquire through it.
30164
+ */
30165
+ userShellPath,
30135
30166
  log: (message) => logger.info(message, LogFileType.BACKEND)
30136
30167
  });
30137
30168
  } catch (error) {