local-operator-ui 0.30.6 → 0.30.8

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 (74) hide show
  1. package/out/main/index.js +269 -340
  2. package/out/renderer/assets/{_basePickBy-D5b3yDx8.js → _basePickBy-D6m3_qfe.js} +1 -1
  3. package/out/renderer/assets/{_baseUniq-D1tkhcv6.js → _baseUniq-wDJxXsek.js} +1 -1
  4. package/out/renderer/assets/{agent-details-page-DZh__Ocy.js → agent-details-page-DNZ6RZUm.js} +1 -1
  5. package/out/renderer/assets/{agent-hub-page-C_kDnp60.js → agent-hub-page-DyUgZhpO.js} +1 -1
  6. package/out/renderer/assets/{agents-page-CnB9CXuW.js → agents-page-DaUHwLca.js} +2 -2
  7. package/out/renderer/assets/{arc-CUyHzDaA.js → arc-CRBgG6A8.js} +1 -1
  8. package/out/renderer/assets/{architectureDiagram-IEHRJDOE-Cvo28P8g.js → architectureDiagram-IEHRJDOE-BAwxDQAG.js} +1 -1
  9. package/out/renderer/assets/{blockDiagram-JOT3LUYC-CCMUOsjA.js → blockDiagram-JOT3LUYC-BUcka7_8.js} +1 -1
  10. package/out/renderer/assets/browser-page-DL1x3WBT.js +1 -0
  11. package/out/renderer/assets/{browser-webauthn-prompt-Bz42nxS6.js → browser-webauthn-prompt-aAeFmgfg.js} +1 -1
  12. package/out/renderer/assets/{c4Diagram-VJAJSXHY-DqacZ02T.js → c4Diagram-VJAJSXHY-D-vCRdn6.js} +1 -1
  13. package/out/renderer/assets/channel-1pBNOA31.js +1 -0
  14. package/out/renderer/assets/{chunk-4BMEZGHF-Vs_Om413.js → chunk-4BMEZGHF-BT85X6s5.js} +1 -1
  15. package/out/renderer/assets/{chunk-A2AXSNBT-nlUPemkk.js → chunk-A2AXSNBT-DCVsaFb-.js} +1 -1
  16. package/out/renderer/assets/{chunk-AEK57VVT-BvAdLttG.js → chunk-AEK57VVT-D78w8wwi.js} +1 -1
  17. package/out/renderer/assets/{chunk-D6G4REZN-B47lDjp3.js → chunk-D6G4REZN-BfoprKJl.js} +1 -1
  18. package/out/renderer/assets/{chunk-RZ5BOZE2-BtJp8S0M.js → chunk-RZ5BOZE2-CL9h0sVd.js} +1 -1
  19. package/out/renderer/assets/{chunk-XZIHB7SX-9C073wve.js → chunk-XZIHB7SX-DgvQsTga.js} +1 -1
  20. package/out/renderer/assets/classDiagram-GIVACNV2-_bMGz-X-.js +1 -0
  21. package/out/renderer/assets/classDiagram-v2-COTLJTTW-_bMGz-X-.js +1 -0
  22. package/out/renderer/assets/clone-C39ZMdqY.js +1 -0
  23. package/out/renderer/assets/{compact-pagination-DqAqETGQ.js → compact-pagination-BA4qmoC_.js} +1 -1
  24. package/out/renderer/assets/{console-mirror-BVW-_zui.js → console-mirror-BgM6RjOl.js} +1 -1
  25. package/out/renderer/assets/{consoleCapture-BIeBPj-4.js → consoleCapture-DKxuSaAr.js} +1 -1
  26. package/out/renderer/assets/{dagre-OKDRZEBW-aOl_nKn1.js → dagre-OKDRZEBW-CD9qRFjZ.js} +1 -1
  27. package/out/renderer/assets/{diagram-SSKATNLV-BuelnlQq.js → diagram-SSKATNLV-DhnCqRoL.js} +1 -1
  28. package/out/renderer/assets/{diagram-VNBRO52H-CnvD7FYh.js → diagram-VNBRO52H-mnH7BbnX.js} +1 -1
  29. package/out/renderer/assets/{erDiagram-Q7BY3M3F-DiuVMmfr.js → erDiagram-Q7BY3M3F-6CNogTs7.js} +1 -1
  30. package/out/renderer/assets/{flowDiagram-4HSFHLVR-BKzqYFvF.js → flowDiagram-4HSFHLVR-DjeJtANn.js} +1 -1
  31. package/out/renderer/assets/{ganttDiagram-APWFNJXF-BR8sfw6V.js → ganttDiagram-APWFNJXF-CbI_qkQM.js} +1 -1
  32. package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-DBUM9S0w.js → gitGraphDiagram-7IBYFJ6S-BipReMcN.js} +1 -1
  33. package/out/renderer/assets/{graph-By8JgBue.js → graph-i6-7DRz1.js} +1 -1
  34. package/out/renderer/assets/{icon-BQh0NRL5.js → icon-BO__FAYr.js} +1 -1
  35. package/out/renderer/assets/{index-up32o88v.js → index-BN_NjFWw.js} +1 -1
  36. package/out/renderer/assets/{index-Bv85BxaP.js → index-BeA59B5i.js} +366 -366
  37. package/out/renderer/assets/index-CE_NdDSn.css +1 -0
  38. package/out/renderer/assets/{index-kvxi4VOW.js → index-VcxYERrS.js} +1 -1
  39. package/out/renderer/assets/{infoDiagram-PH2N3AL5-BhD7ac-z.js → infoDiagram-PH2N3AL5-BwX5HflG.js} +1 -1
  40. package/out/renderer/assets/{installer-CbTFoEpL.js → installer-BF6ZtnHE.js} +1 -1
  41. package/out/renderer/assets/{journeyDiagram-U35MCT3I-Dv2mu_Re.js → journeyDiagram-U35MCT3I-Df2XmcU5.js} +1 -1
  42. package/out/renderer/assets/{kanban-definition-NDS4AKOZ-BqBdPTZf.js → kanban-definition-NDS4AKOZ-D9DanFko.js} +1 -1
  43. package/out/renderer/assets/{layout-DzuHNJry.js → layout-CCCYQm0K.js} +1 -1
  44. package/out/renderer/assets/{legacy-agents-page-B1YsuHQM.js → legacy-agents-page-Cj3nK2pW.js} +1 -1
  45. package/out/renderer/assets/{mermaid.core-DymZo21v.js → mermaid.core-Uc1lh8jM.js} +5 -5
  46. package/out/renderer/assets/{mindmap-definition-ALO5MXBD-BMN7owbo.js → mindmap-definition-ALO5MXBD-DvSLV_Ap.js} +1 -1
  47. package/out/renderer/assets/{page-header-Cc0XdSll.js → page-header-TLDGdE7N.js} +1 -1
  48. package/out/renderer/assets/{parseISO-0hNehfLH.js → parseISO-DB5cwwJg.js} +1 -1
  49. package/out/renderer/assets/{pieDiagram-IB7DONF6-D0dvCyyn.js → pieDiagram-IB7DONF6-C06SU7Cr.js} +1 -1
  50. package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-Bkb3lcZe.js → quadrantDiagram-7GDLP6J5-DcipZkjo.js} +1 -1
  51. package/out/renderer/assets/{radar-MK3ICKWK-2JuG8irb.js → radar-MK3ICKWK-CGWIY91X.js} +1 -1
  52. package/out/renderer/assets/{radient-auth-buttons-C2cd-JNV.js → radient-auth-buttons-D2mimOsQ.js} +1 -1
  53. package/out/renderer/assets/{requirementDiagram-KVF5MWMF-DE_-TcrO.js → requirementDiagram-KVF5MWMF-D8I9OAcS.js} +1 -1
  54. package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-BoMvFI-6.js → sankeyDiagram-QLVOVGJD-DPIwzQ2a.js} +1 -1
  55. package/out/renderer/assets/{schedules-page-CQpjyrDH.js → schedules-page-B68YQ0CA.js} +1 -1
  56. package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-BMcSZEau.js → sequenceDiagram-X6HHIX6F-BjZ0I0Hh.js} +1 -1
  57. package/out/renderer/assets/{settings-page-CEH0ix19.js → settings-page-xH1dLUSB.js} +1 -1
  58. package/out/renderer/assets/{stateDiagram-DGXRK772-B6DVk8hu.js → stateDiagram-DGXRK772-BjMvzb-1.js} +1 -1
  59. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-5Fz5E9kh.js +1 -0
  60. package/out/renderer/assets/{timeline-definition-BDJGKUSR-CkMFtRgw.js → timeline-definition-BDJGKUSR-DmGZ47LF.js} +1 -1
  61. package/out/renderer/assets/{use-agent-like-mutation-C7OkrJmI.js → use-agent-like-mutation-C1m5YSFT.js} +1 -1
  62. package/out/renderer/assets/{xychartDiagram-VJFVF3MP-uj7ccV7g.js → xychartDiagram-VJFVF3MP-CbAAoVNV.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 +2 -2
  67. package/out/renderer/assets/browser-page-DcAY6eY-.js +0 -1
  68. package/out/renderer/assets/channel-CNjcqKwB.js +0 -1
  69. package/out/renderer/assets/classDiagram-GIVACNV2-CQVISGwS.js +0 -1
  70. package/out/renderer/assets/classDiagram-v2-COTLJTTW-CQVISGwS.js +0 -1
  71. package/out/renderer/assets/clone-C38ets0J.js +0 -1
  72. package/out/renderer/assets/index-n0ogWXxx.css +0 -1
  73. package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-DTKEGiyu.js +0 -1
  74. /package/out/renderer/assets/{index-Q1Oi0B95.js → index-ujpapIu9.js} +0 -0
package/out/main/index.js CHANGED
@@ -5540,6 +5540,205 @@ function ensureVenvBytecodeGuard(venvPath) {
5540
5540
  reason: `${path2} now sets sys.dont_write_bytecode for every process using this venv`
5541
5541
  };
5542
5542
  }
5543
+ const SHELL_PATH_BEGIN = "__LOCAL_OPERATOR_SHELL_PATH_BEGIN__";
5544
+ const SHELL_PATH_END = "__LOCAL_OPERATOR_SHELL_PATH_END__";
5545
+ const SHELL_PATH_TIMEOUT_MS = 3e3;
5546
+ const SHELL_PATH_ANSWER_GRACE_MS = 500;
5547
+ const SHELL_PATH_ARGS = [
5548
+ "-l",
5549
+ "-i",
5550
+ "-c",
5551
+ `command printf '${SHELL_PATH_BEGIN}%s${SHELL_PATH_END}' "$PATH"`
5552
+ ];
5553
+ const POSIX_SHELL_NAMES = /* @__PURE__ */ new Set([
5554
+ "sh",
5555
+ "bash",
5556
+ "dash",
5557
+ "zsh",
5558
+ "ksh",
5559
+ "ksh93",
5560
+ "mksh",
5561
+ "lksh",
5562
+ "pdksh",
5563
+ "ash",
5564
+ "posh",
5565
+ "yash",
5566
+ "rbash"
5567
+ ]);
5568
+ function isPosixShell(shell) {
5569
+ const name = path$1.basename(shell).toLowerCase().replace(/\.exe$/, "");
5570
+ return POSIX_SHELL_NAMES.has(name);
5571
+ }
5572
+ function defaultShell(env, options = {}) {
5573
+ const shell = env.SHELL?.trim();
5574
+ if (shell && path$1.isAbsolute(shell)) return shell;
5575
+ const platform = options.platform ?? process.platform;
5576
+ const exists = options.exists ?? fs.existsSync;
5577
+ const candidates = platform === "darwin" ? ["/bin/zsh", "/bin/bash", "/bin/sh"] : ["/bin/bash", "/bin/sh", "/bin/zsh"];
5578
+ return candidates.find((candidate) => exists(candidate)) ?? "/bin/sh";
5579
+ }
5580
+ function shellPathRunner() {
5581
+ return ({ shell, args, env, timeoutMs }) => new Promise((resolve, reject) => {
5582
+ const child = node_child_process.spawn(shell, [...args], {
5583
+ stdio: ["ignore", "pipe", "pipe"],
5584
+ env,
5585
+ detached: true
5586
+ });
5587
+ let stdout = "";
5588
+ let timedOut = false;
5589
+ const deadline2 = setTimeout(() => {
5590
+ timedOut = true;
5591
+ try {
5592
+ if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
5593
+ else child.kill("SIGKILL");
5594
+ } catch {
5595
+ child.kill("SIGKILL");
5596
+ }
5597
+ }, timeoutMs);
5598
+ child.stdout?.setEncoding("utf8");
5599
+ child.stdout?.on("data", (chunk) => {
5600
+ stdout += chunk;
5601
+ });
5602
+ child.stderr?.resume();
5603
+ child.on("error", (error) => {
5604
+ clearTimeout(deadline2);
5605
+ reject(error);
5606
+ });
5607
+ child.on("close", () => {
5608
+ clearTimeout(deadline2);
5609
+ resolve({ stdout, timedOut });
5610
+ });
5611
+ });
5612
+ }
5613
+ function countOccurrences(value, needle) {
5614
+ let count = 0;
5615
+ let at = value.indexOf(needle);
5616
+ while (at !== -1) {
5617
+ count += 1;
5618
+ at = value.indexOf(needle, at + needle.length);
5619
+ }
5620
+ return count;
5621
+ }
5622
+ function pathFromStdout(stdout, shell) {
5623
+ const begins = countOccurrences(stdout, SHELL_PATH_BEGIN);
5624
+ const ends = countOccurrences(stdout, SHELL_PATH_END);
5625
+ if (begins === 0 || ends === 0) {
5626
+ return { ok: false, reason: "empty", shell };
5627
+ }
5628
+ if (begins > 1 || ends > 1) {
5629
+ return {
5630
+ ok: false,
5631
+ reason: "unparsable",
5632
+ shell,
5633
+ detail: `${begins} BEGIN and ${ends} END sentinels`
5634
+ };
5635
+ }
5636
+ const start = stdout.indexOf(SHELL_PATH_BEGIN) + SHELL_PATH_BEGIN.length;
5637
+ const path2 = stdout.slice(start, stdout.indexOf(SHELL_PATH_END));
5638
+ if (path2 === "") return { ok: false, reason: "empty", shell };
5639
+ return { ok: true, path: path2, shell };
5640
+ }
5641
+ function pathFromError(error, shell) {
5642
+ const code = error?.code;
5643
+ const detail = error instanceof Error ? error.message : String(error);
5644
+ if (code === "ENOENT") return { ok: false, reason: "missing", shell, detail };
5645
+ if (code === "EACCES" || code === "EPERM")
5646
+ return { ok: false, reason: "not-executable", shell, detail };
5647
+ return { ok: false, reason: "failed", shell, detail };
5648
+ }
5649
+ async function resolveLoginShellPath(options = {}) {
5650
+ const platform = options.platform ?? process.platform;
5651
+ if (platform !== "darwin" && platform !== "linux") {
5652
+ return { ok: false, reason: "unsupported-platform", shell: null };
5653
+ }
5654
+ const env = options.env ?? process.env;
5655
+ const run = options.run ?? shellPathRunner();
5656
+ const timeoutMs = options.timeoutMs ?? SHELL_PATH_TIMEOUT_MS;
5657
+ const shell = defaultShell(env, { platform });
5658
+ if (!isPosixShell(shell)) {
5659
+ return { ok: false, reason: "unsupported-shell", shell };
5660
+ }
5661
+ const attempt = run({
5662
+ shell,
5663
+ args: SHELL_PATH_ARGS,
5664
+ env,
5665
+ timeoutMs
5666
+ }).then(
5667
+ (result) => (
5668
+ // A run the runner cut short at the bound is a timeout even when the pipe
5669
+ // holds a complete-looking value: a shell killed mid-rc has a PATH that is
5670
+ // whatever the file had reached, which is not the terminal's PATH and is
5671
+ // not worth preferring to the one the app already had.
5672
+ result.timedOut ? { ok: false, reason: "timeout", shell } : pathFromStdout(result.stdout ?? "", shell)
5673
+ ),
5674
+ (error) => pathFromError(error, shell)
5675
+ );
5676
+ let timer;
5677
+ const expired = new Promise((resolve) => {
5678
+ timer = setTimeout(
5679
+ () => resolve("timeout"),
5680
+ timeoutMs + SHELL_PATH_ANSWER_GRACE_MS
5681
+ );
5682
+ });
5683
+ try {
5684
+ const settled = await Promise.race([attempt, expired]);
5685
+ if (settled === "timeout") return { ok: false, reason: "timeout", shell };
5686
+ return settled;
5687
+ } finally {
5688
+ if (timer) clearTimeout(timer);
5689
+ }
5690
+ }
5691
+ const USER_SHELL_PATH_MAX_ATTEMPTS = 3;
5692
+ function createUserShellPath(options = {}) {
5693
+ let answer = null;
5694
+ let pending = null;
5695
+ let attempts = 0;
5696
+ return {
5697
+ resolve() {
5698
+ if (answer) return Promise.resolve(answer);
5699
+ if (pending) return pending;
5700
+ if (attempts >= USER_SHELL_PATH_MAX_ATTEMPTS)
5701
+ return Promise.resolve(null);
5702
+ attempts += 1;
5703
+ const attempt = attempts;
5704
+ pending = resolveLoginShellPath(options).then((result) => {
5705
+ try {
5706
+ if (result.ok) {
5707
+ options.log?.(
5708
+ `[shell-path] ${result.shell} reports PATH: ${result.path}`
5709
+ );
5710
+ } else {
5711
+ options.log?.(
5712
+ `[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"}`
5713
+ );
5714
+ }
5715
+ } catch {
5716
+ }
5717
+ if (result.ok) answer = result.path;
5718
+ return answer;
5719
+ }).finally(() => {
5720
+ pending = null;
5721
+ });
5722
+ return pending;
5723
+ }
5724
+ };
5725
+ }
5726
+ function mergeShellPath(resolved, base) {
5727
+ if (!base) return resolved;
5728
+ const seen = /* @__PURE__ */ new Set();
5729
+ const merged = [];
5730
+ for (const entry of [...resolved.split(":"), ...base.split(":")]) {
5731
+ if (seen.has(entry)) continue;
5732
+ seen.add(entry);
5733
+ merged.push(entry);
5734
+ }
5735
+ return merged.join(":");
5736
+ }
5737
+ async function withUserShellPath(env, userShellPath2) {
5738
+ const path2 = await userShellPath2?.resolve();
5739
+ if (!path2) return { ...env };
5740
+ return { ...env, PATH: mergeShellPath(path2, env.PATH) };
5741
+ }
5543
5742
  const launchEnv = {
5544
5743
  ...process.env
5545
5744
  };
@@ -7156,6 +7355,16 @@ class BackendServiceManager {
7156
7355
  /** Consumers of status changes in main (the window's IPC sender). */
7157
7356
  statusObserver = null;
7158
7357
  shellEnv = {};
7358
+ /**
7359
+ * The user's own login-shell PATH, resolved once for the whole app.
7360
+ *
7361
+ * Handed in by `src/main/index.ts` rather than constructed here, for one
7362
+ * reason: the console host is given the SAME instance, so "what is this user's
7363
+ * PATH" has one answer in this process instead of one per consumer. Absent -
7364
+ * every test and rig that builds a manager directly - means this instance keeps
7365
+ * the launch environment's PATH, which is exactly what it did before.
7366
+ */
7367
+ userShellPath;
7159
7368
  // External/dev backends may be explicitly paired through main's environment.
7160
7369
  // Managed starts always rotate this; it is never exposed by preload or logs.
7161
7370
  /**
@@ -7468,8 +7677,13 @@ class BackendServiceManager {
7468
7677
  // Configurable timeouts for different shutdown scenarios
7469
7678
  /**
7470
7679
  * Constructor
7680
+ *
7681
+ * @param options.userShellPath the app's one login-shell PATH resolver (see
7682
+ * `../shell-path`). Optional so a manager built without the app - a test, a rig
7683
+ * - behaves exactly as it did before this option existed.
7471
7684
  */
7472
- constructor() {
7685
+ constructor(options = {}) {
7686
+ this.userShellPath = options.userShellPath;
7473
7687
  try {
7474
7688
  const apiUrl = new URL(backendConfig.VITE_LOCAL_OPERATOR_API_URL);
7475
7689
  this.port = Number.parseInt(apiUrl.port, 10) || 1111;
@@ -7526,11 +7740,12 @@ class BackendServiceManager {
7526
7740
  LogFileType.BACKEND
7527
7741
  );
7528
7742
  }
7743
+ this.shellEnv = withPythonBytecodeCache(this.shellEnv, this.appDataPath);
7744
+ await this.settleUserShellPath();
7529
7745
  logger.info(
7530
7746
  `Shell environment variables loaded successfully. PATH: ${this.shellEnv.PATH || this.shellEnv.Path || "(not set)"}`,
7531
7747
  LogFileType.BACKEND
7532
7748
  );
7533
- this.shellEnv = withPythonBytecodeCache(this.shellEnv, this.appDataPath);
7534
7749
  } catch (error) {
7535
7750
  logger.error(
7536
7751
  "Error loading shell environment variables:",
@@ -7539,6 +7754,24 @@ class BackendServiceManager {
7539
7754
  );
7540
7755
  }
7541
7756
  }
7757
+ /**
7758
+ * Fold the user's login-shell PATH into `shellEnv`, when the app handed one in.
7759
+ *
7760
+ * Awaited at BOTH of its call sites, unlike the other enrichments the spawn
7761
+ * environment carries, because unlike them this one IS the value being fixed: a
7762
+ * fire-and-forget resolution is the defect it exists to remove, and the first
7763
+ * spawn would keep the launchd PATH on exactly the machines whose rc is slowest.
7764
+ * `resolve()` is memoized, so the second caller - and the console host, which
7765
+ * holds the same resolver - is answered from the first shell's result rather
7766
+ * than starting one.
7767
+ *
7768
+ * A no-op without a resolver: a manager a test or rig builds directly keeps the
7769
+ * launch environment's PATH exactly as it did before.
7770
+ */
7771
+ async settleUserShellPath() {
7772
+ if (!this.userShellPath) return;
7773
+ this.shellEnv = await withUserShellPath(this.shellEnv, this.userShellPath);
7774
+ }
7542
7775
  /**
7543
7776
  * The environment the backend process is spawned with.
7544
7777
  *
@@ -7552,6 +7785,14 @@ class BackendServiceManager {
7552
7785
  * it here makes the guarantee structural instead of ordering-dependent, so
7553
7786
  * no spawn path can miss it.
7554
7787
  *
7788
+ * PATH is the one term of that race this method no longer has to carry: its
7789
+ * resolver is awaited before the first spawn (`settleUserShellPath`), so a slow
7790
+ * rc delays the start rather than quietly leaving the launchd PATH in place.
7791
+ * The prefix and the kill switch stay here all the same, because the seed they
7792
+ * are read from is `process.env` itself - an exported `PYTHONPYCACHEPREFIX`
7793
+ * inside the bundle, or a `.env` folded over the launch - and no shell
7794
+ * resolution of PATH has anything to say about either.
7795
+ *
7555
7796
  * These spawns are the ones whose `python` is the interpreter we ship:
7556
7797
  * CPython writes `__pycache__/*.pyc` beside the sources it imports, those
7557
7798
  * sources are inside the code-sealed `.app`, and every such write breaks the
@@ -8388,6 +8629,7 @@ class BackendServiceManager {
8388
8629
  packaged: electron.app.isPackaged
8389
8630
  });
8390
8631
  const globalInstall = await this.checkLocalOperatorExists();
8632
+ await this.settleUserShellPath();
8391
8633
  const env = this.backendSpawnEnv();
8392
8634
  let interpreters;
8393
8635
  if (globalInstall) {
@@ -9490,338 +9732,9 @@ function managedPythonOptions() {
9490
9732
  arch: process.arch
9491
9733
  };
9492
9734
  }
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
- `;
9735
+ 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# 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. 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\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';
9736
+ 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# 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. 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\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 -sI --fail --connect-timeout 5 --max-time 30 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';
9737
+ 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# 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& python -m pip install --upgrade pip\n& python -m pip install --upgrade local-operator\n\nif ($LASTEXITCODE -ne 0) {\n Write-Error "Failed to install packages in virtual environment"\n exit 1\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
9738
  const macosInstallScript = macosInstallScriptRaw;
9826
9739
  const linuxInstallScript = linuxInstallScriptRaw;
9827
9740
  const windowsInstallScript = windowsInstallScriptRaw;
@@ -12632,11 +12545,6 @@ function surfaceEnvironment(input) {
12632
12545
  }
12633
12546
  return env;
12634
12547
  }
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
12548
  function resolveCwd(cwd) {
12641
12549
  const wanted = cwd?.trim();
12642
12550
  if (wanted && path$1.isAbsolute(wanted)) return wanted;
@@ -13615,6 +13523,15 @@ async function startConsoleHost(options) {
13615
13523
  log,
13616
13524
  history,
13617
13525
  now: options.now,
13526
+ // Resolved BEFORE the host exists, so no surface can be created against the
13527
+ // launch environment and then start behaving like the user's terminal later:
13528
+ // a surface's PATH is decided here, once, and every surface this host makes
13529
+ // carries the same one. Bounded by the resolver's own timeout (it never
13530
+ // rejects and answers null when the shell cannot speak), and no part of the
13531
+ // app's own startup waits on it: the browser host is attached to the window
13532
+ // without being awaited, so the cost of a slow rc here is this host coming
13533
+ // online later, not the app failing to come up.
13534
+ env: await withUserShellPath(process.env, options.userShellPath),
13618
13535
  captureOffscreen: capture ? (request) => capture.capture(request) : void 0,
13619
13536
  spawn: options.spawn ?? ((spawnOptions) => spawnPty(load.module, spawnOptions, (report) => {
13620
13537
  log(
@@ -20668,6 +20585,7 @@ async function startBrowserHost(options) {
20668
20585
  */
20669
20586
  consoleCaptureUrl: options.consoleCaptureUrl,
20670
20587
  preloadPath: options.preloadPath,
20588
+ userShellPath: options.userShellPath,
20671
20589
  log
20672
20590
  });
20673
20591
  const sessionKey = mintSessionKey();
@@ -29225,7 +29143,10 @@ function createWindow(initialSession = null, openCatalogue = false, request = ow
29225
29143
  attachBrowserHostToWindow?.(mainWindow2);
29226
29144
  return mainWindow2;
29227
29145
  }
29228
- const backendService = new BackendServiceManager();
29146
+ const userShellPath = createUserShellPath({
29147
+ log: (message) => logger.info(message, LogFileType.BACKEND)
29148
+ });
29149
+ const backendService = new BackendServiceManager({ userShellPath });
29229
29150
  const backendInstaller = new BackendInstaller();
29230
29151
  const reportBackendFailure = (message, fileType) => {
29231
29152
  if (backendService.isShuttingDown()) {
@@ -30132,6 +30053,14 @@ electron.app.whenReady().then(async () => {
30132
30053
  // lets main feed the reconstruction to it.
30133
30054
  consoleCaptureUrl,
30134
30055
  preloadPath: path$1.join(__dirname, "../preload/index.js"),
30056
+ /*
30057
+ * The console's surfaces are handed the user's own PATH, from the same
30058
+ * resolver the backend spawn uses (above). Without it a surface inherits
30059
+ * this app's launchd environment, in which `brew` - and every other tool
30060
+ * the user installed - does not exist, so the console cannot run the
30061
+ * tooling the agent is meant to acquire through it.
30062
+ */
30063
+ userShellPath,
30135
30064
  log: (message) => logger.info(message, LogFileType.BACKEND)
30136
30065
  });
30137
30066
  } catch (error) {