local-operator-ui 0.33.6 → 0.33.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/out/main/index.js +97 -17
- package/out/renderer/assets/{_basePickBy-CkoMFf5J.js → _basePickBy-BhDX1PiM.js} +1 -1
- package/out/renderer/assets/{_baseUniq-BoGp7aXg.js → _baseUniq-zuguH--P.js} +1 -1
- package/out/renderer/assets/{agent-details-page-DnKxplQb.js → agent-details-page-BG5iQvpl.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-DaWE1zkN.js → agent-hub-page-B9Ss1x0Y.js} +2 -2
- package/out/renderer/assets/{agents-page-DRLL1A2Z.js → agents-page-Dor5TAJn.js} +3 -3
- package/out/renderer/assets/{arc-DDbm191y.js → arc-CWoNsPNP.js} +1 -1
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-CtQo1Whw.js → architectureDiagram-IEHRJDOE-DqJTdoOl.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-BIYZ30Qj.js → blockDiagram-JOT3LUYC-B-ExEGyk.js} +1 -1
- package/out/renderer/assets/{book-open-D8j_ZoIP.js → book-open-DPP7z-0y.js} +1 -1
- package/out/renderer/assets/browser-page-D2z4RyVy.js +1 -0
- package/out/renderer/assets/{browser-webauthn-prompt-Btk4lmO0.js → browser-webauthn-prompt-CV0R47KU.js} +1 -1
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-Xf4s2RLR.js → c4Diagram-VJAJSXHY-CODJNl1T.js} +1 -1
- package/out/renderer/assets/channel-CtlJnRLk.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-BeSTkHZN.js → chunk-4BMEZGHF-BB4g66je.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-Du_mT84q.js → chunk-A2AXSNBT-CM_ZHBeb.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-Cx-awbJf.js → chunk-AEK57VVT-Bn7qk8Er.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-CjB8Mrju.js → chunk-D6G4REZN-BIMBB93o.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-CeWf6wWy.js → chunk-RZ5BOZE2-85wkRwXW.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-C0vOXPUC.js → chunk-XZIHB7SX-bKWYUY1b.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-BNz8h3Dk.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-BNz8h3Dk.js +1 -0
- package/out/renderer/assets/clone-Uo0ttxzO.js +1 -0
- package/out/renderer/assets/{console-mirror-C021Pleg.js → console-mirror-kUFBQ0yT.js} +1 -1
- package/out/renderer/assets/{consoleCapture-CwsR7Pk5.js → consoleCapture-DNp4Nrze.js} +1 -1
- package/out/renderer/assets/{conversation-input-sync-CKFZgmQ4.js → conversation-input-sync-CIl2xNs4.js} +238 -236
- package/out/renderer/assets/{dagre-OKDRZEBW-Dlb8udV9.js → dagre-OKDRZEBW-D8wPOTCL.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-9pQBfRIV.js → diagram-SSKATNLV-UgEBnFDf.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-XJhBN8f8.js → diagram-VNBRO52H-kAq-HAji.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-CsiOSnwr.js → erDiagram-Q7BY3M3F-CnPhL7-o.js} +1 -1
- package/out/renderer/assets/{error-boundary-CYOMGBqy.js → error-boundary-BNOVKo4J.js} +5 -5
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-0jdxtrwY.js → flowDiagram-4HSFHLVR-Da9FyDiS.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-Dx1qFOJp.js → ganttDiagram-APWFNJXF-CHGPyt01.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-DnrtQqzQ.js → gitGraphDiagram-7IBYFJ6S-CJ7s4flg.js} +1 -1
- package/out/renderer/assets/{graph-B3AxGVxQ.js → graph-DVJ5ZLJf.js} +1 -1
- package/out/renderer/assets/index-C0GHTFm3.css +1 -0
- package/out/renderer/assets/{index-BCtcy-Az.js → index-CfyttM43.js} +1 -1
- package/out/renderer/assets/{index-C90HwIjj.js → index-ChzAo_gH.js} +191 -191
- package/out/renderer/assets/{index-dDgWp_lK.js → index-DqW2H0tb.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-BxhAX0zq.js → infoDiagram-PH2N3AL5-B2p-sVW8.js} +1 -1
- package/out/renderer/assets/installer-DMRj-tSA.js +1 -0
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-C2ESsRrd.js → journeyDiagram-U35MCT3I-DZ9HH25A.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-Sy7_JQxl.js → kanban-definition-NDS4AKOZ-DDg85b7g.js} +1 -1
- package/out/renderer/assets/{layout-BIY9Aicm.js → layout-BoXi9hJD.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-Cc0APBCV.js → legacy-agents-page-BxdYNqBy.js} +3 -3
- package/out/renderer/assets/{mermaid.core-Dd6Z4632.js → mermaid.core-CUKo5IMR.js} +5 -5
- package/out/renderer/assets/{mesh-page-DZvWWqUP.js → mesh-page-D5g8K8ON.js} +1 -1
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-g4Yb3UNq.js → mindmap-definition-ALO5MXBD-tJWvhEAx.js} +1 -1
- package/out/renderer/assets/{mini-DYCxYiKw.js → mini-CfPIy2o6.js} +1 -1
- package/out/renderer/assets/{org-surface-gate-B8fnNPpi.js → org-surface-gate-ByhWiCWt.js} +1 -1
- package/out/renderer/assets/{page-header-CZYftvB1.js → page-header-BOWTJ076.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-F8LZExN5.js → pieDiagram-IB7DONF6-Drj3ddXL.js} +1 -1
- package/out/renderer/assets/{projects-page-B1bg7q_9.js → projects-page-DCcYEsGC.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-rh4mk2d5.js → quadrantDiagram-7GDLP6J5-CHP3SLDm.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-DRGWqtCX.js → radar-MK3ICKWK-e3EbWCZZ.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-BYdsYX04.js → radient-auth-buttons-Ds18jmr_.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-BW_W32AN.js → requirementDiagram-KVF5MWMF-Cq1yN98-.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-Da2ruw_B.js → sankeyDiagram-QLVOVGJD-D_g_qQlA.js} +1 -1
- package/out/renderer/assets/{schedules-page-BgdW7H8B.js → schedules-page-C48pCgFG.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-DOFQE45B.js → sequenceDiagram-X6HHIX6F-r_6P-W36.js} +1 -1
- package/out/renderer/assets/{settings-page-DWD7khMF.js → settings-page-CZTW_x_x.js} +1 -1
- package/out/renderer/assets/{settings-section-Z6n84Hu2.js → settings-section-CRXOvEGl.js} +1 -1
- package/out/renderer/assets/{stateDiagram-DGXRK772-WWWtsm-s.js → stateDiagram-DGXRK772-DMQTP9ug.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-BYUUZzB_.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-97ZqUIJR.js → timeline-definition-BDJGKUSR-CDyPGOPJ.js} +1 -1
- package/out/renderer/assets/ui-preferences-store-04N-sf2V.js +1 -0
- package/out/renderer/assets/{use-download-agent-mutation-Cc1YKh4G.js → use-download-agent-mutation-BHLQqQza.js} +2 -2
- package/out/renderer/assets/{use-memberships-query-mPXx5KM6.js → use-memberships-query-CdEL1LZf.js} +1 -1
- package/out/renderer/assets/{use-session-snapshot-TukjnWYL.js → use-session-snapshot-D_cieFc8.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-g4T0Cb0k.js → xychartDiagram-VJFVF3MP-K0plj_QF.js} +1 -1
- package/out/renderer/console-capture.html +5 -5
- package/out/renderer/index.html +7 -7
- package/out/renderer/installer.html +4 -4
- package/out/renderer/mini.html +7 -7
- package/package.json +2 -2
- package/out/renderer/assets/browser-page-Vhnv-3jT.js +0 -1
- package/out/renderer/assets/channel-DsyGqe4G.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-BKxxjeHy.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-BKxxjeHy.js +0 -1
- package/out/renderer/assets/clone-CjIB6OC_.js +0 -1
- package/out/renderer/assets/index-oPdbJBXR.css +0 -1
- package/out/renderer/assets/installer-BaX_sXt9.js +0 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-SFa2-GCO.js +0 -1
- package/out/renderer/assets/ui-preferences-store-CC8aFabY.js +0 -1
- /package/out/renderer/assets/{index-mu0fzQM7.js → index-CxyiGw9S.js} +0 -0
package/out/main/index.js
CHANGED
|
@@ -4240,8 +4240,8 @@ const updateCheckVerdict = (channels) => ({
|
|
|
4240
4240
|
server: channels.server,
|
|
4241
4241
|
affirmation: updateCheckAffirmation(channels)
|
|
4242
4242
|
});
|
|
4243
|
-
const python = { "seedNamespace": "python-runtime-seed", "checkoutSeedNames": { "arm64": "python_aarch64", "x64": "python" } };
|
|
4244
|
-
const uv = { "namespace": "uv", "binaryNames": { "darwin": "uv", "linux": "uv", "win32": "uv.exe" }, "checkoutNames": { "arm64": "uv_aarch64", "x64": "uv" } };
|
|
4243
|
+
const python = { "version": "3.14.7", "buildDate": "20260901", "tkVersion": "9.0", "seedNamespace": "python-runtime-seed", "checkoutSeedNames": { "arm64": "python_aarch64", "x64": "python" }, "seedStdlibMarker": "lib/python{pyver}/encodings/__init__.py", "seedTkDir": "lib/tk{tkver}", "prunedSeedPaths": ["bin/idle3", "bin/idle3.{pyminor}", "bin/pydoc3", "bin/pydoc3.{pyminor}", "lib/python{pyver}/idlelib", "lib/python{pyver}/turtledemo", "lib/python{pyver}/pydoc_data"], "prunedSeedPathsOptional": ["lib/tk{tkver}/demos"] };
|
|
4244
|
+
const uv = { "version": "0.12.17", "namespace": "uv", "binaryNames": { "darwin": "uv", "linux": "uv", "win32": "uv.exe" }, "checkoutNames": { "arm64": "uv_aarch64", "x64": "uv" }, "releaseTriples": { "darwin": { "arm64": "aarch64-apple-darwin", "x64": "x86_64-apple-darwin" }, "linux": { "arm64": "aarch64-unknown-linux-gnu", "x64": "x86_64-unknown-linux-gnu" }, "win32": { "arm64": "aarch64-pc-windows-msvc", "x64": "x86_64-pc-windows-msvc" } }, "archiveExtension": { "darwin": "tar.gz", "linux": "tar.gz", "win32": "zip" }, "archiveMember": { "darwin": "uv-{triple}/uv", "linux": "uv-{triple}/uv", "win32": "uv.exe" } };
|
|
4245
4245
|
const architectures = ["arm64", "x64"];
|
|
4246
4246
|
const legacyResourceNames = ["python", "python_aarch64"];
|
|
4247
4247
|
const machOMagics = ["cffaedfe", "cefaedfe", "feedfacf", "feedface", "cafebabe", "bebafeca", "cafebabf", "bfbafeca"];
|
|
@@ -12180,12 +12180,35 @@ const INSTALL_PHASES = [
|
|
|
12180
12180
|
"components",
|
|
12181
12181
|
"verify"
|
|
12182
12182
|
];
|
|
12183
|
-
|
|
12184
|
-
|
|
12185
|
-
|
|
12186
|
-
|
|
12187
|
-
|
|
12183
|
+
function installPlatform(platform) {
|
|
12184
|
+
return platform === "darwin" || platform === "win32" ? platform : "linux";
|
|
12185
|
+
}
|
|
12186
|
+
const EMPTY_SUB_PROGRESS = {
|
|
12187
|
+
resolved: null,
|
|
12188
|
+
downloadsStarted: 0,
|
|
12189
|
+
downloadsDone: 0,
|
|
12190
|
+
collected: 0,
|
|
12191
|
+
installed: false
|
|
12188
12192
|
};
|
|
12193
|
+
const UV_RESOLVED = /^\s*Resolved (\d+) packages?\b/;
|
|
12194
|
+
const UV_DOWNLOADING = /^\s*Downloading \S+ \(\d/;
|
|
12195
|
+
const UV_DOWNLOADED = /^\s*Downloaded \S+\s*$/;
|
|
12196
|
+
const UV_INSTALLED = /^\s*Installed \d+ packages?\b/;
|
|
12197
|
+
const PIP_COLLECTING = /^\s*Collecting \S/;
|
|
12198
|
+
const PIP_INSTALLED = /^\s*Successfully installed\b/;
|
|
12199
|
+
function foldInstallLine(current, line) {
|
|
12200
|
+
const resolved = UV_RESOLVED.exec(line);
|
|
12201
|
+
if (resolved) return { ...current, resolved: Number(resolved[1]) };
|
|
12202
|
+
if (UV_DOWNLOADING.test(line))
|
|
12203
|
+
return { ...current, downloadsStarted: current.downloadsStarted + 1 };
|
|
12204
|
+
if (UV_DOWNLOADED.test(line))
|
|
12205
|
+
return { ...current, downloadsDone: current.downloadsDone + 1 };
|
|
12206
|
+
if (UV_INSTALLED.test(line) || PIP_INSTALLED.test(line))
|
|
12207
|
+
return { ...current, installed: true };
|
|
12208
|
+
if (PIP_COLLECTING.test(line))
|
|
12209
|
+
return { ...current, collected: current.collected + 1 };
|
|
12210
|
+
return current;
|
|
12211
|
+
}
|
|
12189
12212
|
const MARKER_PREFIX = "|LO";
|
|
12190
12213
|
const MARKER_VERSION = "1";
|
|
12191
12214
|
const INSTALL_MARKER_PREFIX = `${MARKER_PREFIX}${MARKER_VERSION}:`;
|
|
@@ -12200,9 +12223,15 @@ function splitLines(carry, chunk) {
|
|
|
12200
12223
|
const next = parts.pop() ?? "";
|
|
12201
12224
|
return { lines: parts, carry: next };
|
|
12202
12225
|
}
|
|
12226
|
+
const INSTALL_PHASE_FAILURE_CLAUSE = {
|
|
12227
|
+
python: "getting ready",
|
|
12228
|
+
environment: "setting up Local Operator",
|
|
12229
|
+
components: "downloading what it needs",
|
|
12230
|
+
verify: "starting Local Operator"
|
|
12231
|
+
};
|
|
12203
12232
|
function installFailureSentence(phase) {
|
|
12204
12233
|
if (phase === null) return "Setup stopped before it could finish.";
|
|
12205
|
-
return `Setup stopped while ${
|
|
12234
|
+
return `Setup stopped while ${INSTALL_PHASE_FAILURE_CLAUSE[phase]}.`;
|
|
12206
12235
|
}
|
|
12207
12236
|
const NOISE = [
|
|
12208
12237
|
/^\s*[-=]{3,}\s*$/,
|
|
@@ -12744,9 +12773,9 @@ function managedPythonOptions() {
|
|
|
12744
12773
|
arch: process.arch
|
|
12745
12774
|
};
|
|
12746
12775
|
}
|
|
12747
|
-
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 # ...AND THEN WHATEVER `PATH` OFFERS, which is the half this probe used to\n # ignore entirely.\n #\n # WHY IT MATTERS, stated as the user story rather than as a rule: Python 3.12\n # on Linux usually arrives from a version manager (pyenv, mise, asdf, uv) or a\n # Homebrew/Linuxbrew prefix, and every one of those puts its interpreter\n # somewhere this list does not name while putting it on `PATH`. The list above\n # is what the app\'s own CI runner has (Ubuntu\'s `/usr/bin/python3`), so the gap\n # was invisible there and the report on the user\'s machine was the wrong one:\n # "No suitable Python installation found (version 3.12 or higher required).\n # Please install Python 3.12 or higher" - about a machine that had 3.12\n # installed and working, sending them to reinstall what they already had.\n #\n # APPENDED, NOT PREPENDED. A version manager on `PATH` can point at any minor\n # version, and the fixed list above is the more predictable of the two where it\n # resolves at all; the version and architecture checks in the loop below are\n # unchanged and still decide, so an entry added here is a candidate and never\n # an answer.\n for python_name in python3.12 python3 python; do\n resolved_python="$(command -v "${python_name}" 2>/dev/null || true)"\n if [ -z "${resolved_python}" ]; then\n continue\n fi\n # Deduplicated by hand: a repeated candidate costs a python spawn each and\n # repeats its own "below the required version" line, which reads as several\n # separate problems rather than one machine being looked at three times.\n already_listed=false\n for listed_path in "${POSSIBLE_PYTHON_PATHS[@]}"; do\n if [ "${listed_path}" = "${resolved_python}" ]; then\n already_listed=true\n break\n fi\n done\n if [ "${already_listed}" = false ]; then\n POSSIBLE_PYTHON_PATHS+=("${resolved_python}")\n fi\n done\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 # WHAT WAS SEARCHED IS PART OF THE DIAGNOSIS, and printing it is the second\n # half of the fix above: the sentence this used to end with told a user who\n # already had 3.12 to install it. Naming the paths makes the two real cases\n # tell themselves apart - nothing is installed, or something is installed and\n # this process cannot see it - and each of those has its own remedy.\n log "No suitable Python 3.12+ was found. Checked: ${POSSIBLE_PYTHON_PATHS[*]}"\n error_exit "No Python 3.12+ found: not in the standard locations and not on this process\'s PATH. Install Python 3.12 or higher (https://www.python.org/downloads/), or make an existing 3.12+ visible on this process\'s PATH."\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 "|LO1:environment"\n echo "Creating virtual environment at $VENV_PATH..."\n # Remove any potentially corrupted virtual environment\n if [ -e "$VENV_PATH" ]; then\n echo "Removing existing but potentially corrupted venv directory..."\n rm -rf "$VENV_PATH"\n fi\n \n # Make sure parent directory exists and is writable\n mkdir -p "$(dirname "$VENV_PATH")"\n \n # Try different methods to create a virtual environment\n VENV_CREATE_STATUS=1\n \n # First try with venv if available\n if [ $VENV_AVAILABLE -eq 0 ] && [ $ENSUREPIP_AVAILABLE -eq 0 ]; then\n echo "Creating virtual environment using venv module..."\n "$PYTHON_BIN" -m venv "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n fi\n \n # If venv failed, try virtualenv\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n echo "venv creation failed with status $VENV_CREATE_STATUS. Falling back to virtualenv..."\n if command_exists virtualenv; then\n virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n elif "$PYTHON_BIN" -m pip list | grep -q virtualenv; then\n "$PYTHON_BIN" -m virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n else\n echo "ERROR: Both venv and virtualenv are unavailable or failed."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n echo "You can install Python 3.12 from https://www.python.org/downloads/"\n exit 1\n fi\n fi\n \n # Check if virtual environment creation was successful\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n log "ERROR: Failed to create virtual environment. Exit code: $VENV_CREATE_STATUS"\n log "Virtual environment path: $VENV_PATH"\n log "Python binary used: $PYTHON_BIN"\n ls -la "$(dirname "$VENV_PATH")" || true\n log "Python executable permissions:"\n ls -la "$PYTHON_BIN" || true\n \n # Check for common issues\n log "Checking for common virtual environment creation issues..."\n \n # Check disk space\n df -h "$(dirname "$VENV_PATH")" || true\n \n # Check if directory is writable\n if [ ! -w "$(dirname "$VENV_PATH")" ]; then\n log "ERROR: Directory $(dirname "$VENV_PATH") is not writable"\n fi\n \n # Try to create a minimal virtual environment manually as a last resort\n log "Attempting to create a minimal virtual environment manually..."\n mkdir -p "$VENV_PATH/bin" || error_exit "Could not create directory $VENV_PATH/bin"\n echo "#!/bin/bash" > "$VENV_PATH/bin/activate"\n echo "export VIRTUAL_ENV=\\"$VENV_PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "export PATH=\\"$VENV_PATH/bin:\\$PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "unset PYTHONHOME" >> "$VENV_PATH/bin/activate"\n chmod +x "$VENV_PATH/bin/activate" || error_exit "Could not set execute permissions on activate script"\n \n # Create symlinks to the system Python\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python" || log "WARNING: Failed to create symlink for python"\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python3" || log "WARNING: Failed to create symlink for python3"\n \n # Try to bootstrap pip\n log "Bootstrapping pip in the minimal virtual environment..."\n if command_exists curl; then\n curl -s --fail --connect-timeout 10 --max-time 60 https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py" || log "WARNING: Failed to download get-pip.py"\n elif command_exists wget; then\n wget -q --timeout=30 --tries=1 -O "$APP_DATA_DIR/get-pip.py" https://bootstrap.pypa.io/get-pip.py || log "WARNING: Failed to download get-pip.py"\n else\n log "ERROR: Neither curl nor wget available to download get-pip.py"\n error_exit "Installation cannot continue without being able to download pip"\n fi\n \n # shellcheck disable=SC1090\n source "$VENV_PATH/bin/activate" && python "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n error_exit "Failed to create even a minimal virtual environment."\n else\n log "Created a minimal virtual environment as a fallback."\n fi\n else\n log "Successfully created virtual environment"\n fi\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ]; then\n echo "Python executable missing in virtual environment, creating symlink..."\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python"\nfi\n\nif [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "pip missing in virtual environment, attempting to bootstrap it..."\n curl -s --fail --connect-timeout 10 --max-time 60 https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py"\n "$VENV_PATH/bin/python" "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Failed to bootstrap pip in the virtual environment"\n exit 1\n fi\nfi\n\necho "Virtual environment structure verified"\n\n# --- The package install: uv when there is one, pip otherwise ------------------\n#\n# Same shape as the macOS script, and for the same reasons: uv resolves and\n# fetches in parallel - measured there, cold cache, three runs each, same\n# interpreter: uv\'s package install is 12.8-16.1 s against pip\'s 33.0-40.7 s,\n# plus the 2.3-2.8 s pip self-upgrade this path skips, so 1.5-2.8x across two\n# operators rather than the "14.8 s against 128.9 s" quoted here before that\n# reading was withdrawn (`docs/BUILD.md` has the full set). The pip path below is\n# unchanged and runs whenever uv is absent or cannot do the job, and pip STAYS in\n# the venv because the app\'s backend-update path runs `<venv>/bin/python -m pip\n# install --upgrade local-operator` inside this same environment - which is why\n# the environment must keep pip, and NOT a reason to avoid `uv venv`: the note\n# that stood here said `uv venv` leaves no pip at all, which is true of bare\n# `uv venv` and false of `uv venv --seed`, and this script\'s own creation path is\n# still `python -m venv` only because moving it has been measured on macOS and\n# not on this platform yet (see the macOS script for the numbers and the proof).\n#\n# Nothing here searches PATH for a uv: an installed uv is a version and a\n# configuration nobody in this repository chose. `LOCAL_OPERATOR_UV_BIN` is the\n# app\'s own answer (`src/main/backend/uv-tool.ts`).\nUV_BIN="${LOCAL_OPERATOR_UV_BIN:-}"\n\n# Drop every UV_* variable the launching environment carried. Measured on uv\n# 0.12.17: a user-level `uv.toml` naming an unreachable index is obeyed by\n# `uv pip install` and ignored with `UV_NO_CONFIG=1`; an ambient `UV_INDEX_URL`\n# changes where packages come from, while `PIP_INDEX_URL` does not affect uv at\n# all. A name list would drift the day uv adds a variable - the namespace cannot.\n#\n# IT RUNS BEFORE THE SETTINGS BELOW ARE SET: a sweep after them takes them away,\n# and an empty `UV_CACHE_DIR` makes uv exit 2 with `a value is required for\n# \'--cache-dir <CACHE_DIR>\'` rather than falling back to a default.\nfor uv_ambient in $(env | sed -n \'s/^\\(UV_[A-Za-z0-9_]*\\)=.*/\\1/p\'); do\n unset "$uv_ambient"\ndone\n\n# The cache lives under the app\'s own support directory rather than the user\'s\n# shared `~/.cache/uv`, and is handed to uv per invocation rather than exported.\n# It persists (~118 MB for a full install, measured on macOS) and nothing else\n# reads it today; it is what makes a retry converge in seconds rather than tens\n# of seconds.\nUV_CACHE_DIR="${APP_DATA_DIR}/uv-cache"\n\n# Is the handed-down uv something we can actually run?\nuv_is_usable() {\n [ -n "${UV_BIN}" ] && [ -x "${UV_BIN}" ] && "${UV_BIN}" --version >/dev/null 2>&1\n}\n\n# UV_NO_CONFIG: never read `pyproject.toml`/`uv.toml`, wherever they are.\n# UV_PYTHON_DOWNLOADS=never: this install uses the interpreter it was handed and\n# never fetches another.\n# UV_SYSTEM_CERTS: trust the PLATFORM trust store, not only the root bundle uv\n# ships. Off by default, which is the default this line changes; `--system-certs`\n# is the same setting spelled as a flag in the bundled uv 0.12.17.\n#\n# WHY: on a network that inspects TLS the root lives in the platform store (here,\n# `/etc/ssl/certs`), and uv does not read that store by default - it fails the\n# handshake against the roots compiled into it. uv names this remedy itself when\n# it fails: "Consider enabling use of system TLS certificates with the\n# `--system-certs` command-line flag". Nothing here passed it, so the failure\n# went to the fallback and the user was told to check a network that was working\n# for every other application on the machine.\n#\n# AND WHY THE PIP FALLBACK IS HANDED NO CA SETTING: pip 24.2 and newer read the\n# platform store by default, in addition to the Mozilla bundle they ship\n# (`truststore`, "always on since 24.2"), and every pip these installs produce\n# is newer than that - uv\'s own seed and the bundled interpreter\'s ensurepip\n# alike - so a root added to `/etc/ssl/certs` is trusted by BOTH clients and no\n# `PIP_CERT`/`SSL_CERT_FILE` is needed. The note this replaces claimed the\n# opposite (the fallback "resolves against certifi"; "neither client" would see\n# the store), which was true only of pip before 24.2; corrected rather than\n# deleted so it is not restored (review round 1, R1-2). The one shape it does\n# not cover is a dev checkout on a system python old enough to predate\n# truststore.\n#\n# IT CANNOT MAKE THINGS WORSE: every uv call below is already followed by the pip\n# fallback on a non-zero exit, so a platform store that cannot be read costs one\n# failed uv attempt and then the path that shipped before uv was bundled.\nuv_run() {\n UV_NO_CONFIG=1 UV_PYTHON_DOWNLOADS=never UV_SYSTEM_CERTS=1 \\\n UV_CACHE_DIR="${UV_CACHE_DIR}" \\\n "${UV_BIN}" "$@"\n}\n\n# Activate virtual environment and install local-operator\necho "Installing local-operator in virtual environment..."\nsource "$VENV_PATH/bin/activate"\n\n# Check network connectivity to PyPI. A DIAGNOSTIC, not a gate: the install below\n# decides whether it can proceed.\n#\n# What this answers, stated precisely because an earlier version of this comment\n# claimed more than the flags buy: "did a TLS fetch to PyPI\'s JSON API complete,\n# and did the answer come back as JSON?". `--fail` turns an HTTP ERROR status\n# into a non-zero exit, and does NOT notice a captive portal answering 200 with\n# its own HTML page (measured: a portal-shaped 200 exits 0 with and without the\n# flag). Both spellings therefore check the CONTENT TYPE as well, since a portal\n# that reports "reachable" while pip is about to fail is the false negative this\n# warning exists to catch: curl through `-w \'%{content_type}\'`, and wget through\n# the response headers `-S` prints (`-S`/`--server-response` is in BusyBox\'s wget\n# since 2017 as well as GNU\'s, which is the wget a system without curl has).\n#\n# What the content type does NOT prove, stated so this paragraph is not read as\n# more than it says: a proxy answering 200 with `application/json` and an error\n# body (`{"detail":"blocked by proxy policy"}`) is silent here, because the\n# answer did come back as JSON. Only parsing the body - a fetch of PyPI\'s own\n# payload shape - would tell those apart, and a diagnostic that costs a parse is\n# not what stands in front of an install.\n#\n# Two bounds, two jobs: the connect bound ends a black-hole network, the total\n# stops a connected-but-stalled peer. Both must be POSITIVE - `--max-time 0` and\n# `--timeout=0` disable the bound rather than making it immediate on both curl and\n# wget. 30 rather than 10 because the total must not fire on a slow-but-working\n# link: a working endpoint that answered in 15s tripped a 10s bound and printed\n# this warning on an install that then succeeded.\necho "Checking network connectivity to PyPI..."\nif command_exists curl; then\n PYPI_PROBE_CONTENT_TYPE=$(curl -s --fail --connect-timeout 5 --max-time 30 -o /dev/null -w \'%{content_type}\' https://pypi.org/pypi/local-operator/json) || PYPI_PROBE_CONTENT_TYPE=""\n if [[ "${PYPI_PROBE_CONTENT_TYPE}" != application/json* ]]; then\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n fi\nelif command_exists wget; then\n # The headers are captured into a variable and matched with a here-string\n # rather than piped into `grep -q`: `-q` exits on the first match, the closed\n # pipe gives wget SIGPIPE, and the script runs under `set -o pipefail`, so a\n # WORKING link printed this warning whenever the match landed before wget had\n # finished writing its headers. The piped shape was this remediation\'s own - the\n # first fix piped it and was measured warning 1 time in 20 against real PyPI\n # (and 1 in 20 against a healthy local endpoint) while this shape warns 0 in 40\n # with the same flags - so the trap is recorded here because it is one line away\n # from being reintroduced, not because the released script ever shipped it.\n PYPI_PROBE_HEADERS=$(wget -q -S --spider --timeout=30 --tries=1 https://pypi.org/pypi/local-operator/json 2>&1) || PYPI_PROBE_HEADERS=""\n if ! grep -qi \'^ *content-type: application/json\' <<< "$PYPI_PROBE_HEADERS"; then\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n fi\nfi\n\n# --- Progress markers -------------------------------------------------------\n# One whole line per phase, read by the app and shown in the setup window. The\n# app matches the ENTIRE line (`|LO1:<phase>`, see src/shared/install-progress.ts)\n# and never a substring, so a marker has to stand alone: do not wrap it in\n# other text, do not re-indent it into a longer sentence, and do not emit one\n# for work this script does not actually do. A missing marker leaves the window\n# on its previous step, which is the honest failure; a marker a log line also\n# happens to produce is a wrong step presented as a measurement.\n#\n# WHY THE SCRIPT AND NOT ONLY THE APP: the install below is minutes of work on a\n# cold machine, and the app cannot see inside the venv it is about to create -\n# this is the only process that knows when the environment exists and when the\n# download starts.\necho "|LO1:components"\nUV_INSTALLED=false\nif uv_is_usable; then\n echo "Installing local-operator with uv ($("${UV_BIN}" --version 2>/dev/null || echo \'version unavailable\'))..."\n # No `pip install --upgrade pip` on this path: uv does not use pip.\n if uv_run pip install --python "${VENV_PATH}/bin/python" --upgrade local-operator; then\n UV_INSTALLED=true\n echo "local-operator installation with uv successful"\n else\n # The exit code matters here for the same reason it does on macOS: the\n # fallback is deliberately forgiving, so this line is the only evidence that\n # a bundled uv is present and failing for every user (QA Q2).\n UV_STATUS=$?\n echo "WARNING: the bundled uv is present but its install failed (exit ${UV_STATUS}); retrying with pip, which is what this script used before uv was bundled."\n fi\nelse\n if [ -n "${UV_BIN}" ]; then\n # Present but not runnable - the wrong architecture for this machine, a\n # truncated copy, a mount that lost the execute bit. Named apart from the\n # absent case below because the two are different facts about the install\n # (the Windows script has the same split, and this line\'s absence is how a\n # staged-for-the-wrong-arch uv reads as "nothing was staged"): both fall\n # back to pip, and only this one is a defect in what was delivered.\n echo "Bundled uv at ${UV_BIN} could not be run on this machine; installing with pip."\n else\n echo "Bundled uv not available (LOCAL_OPERATOR_UV_BIN=${UV_BIN:-unset}); installing with pip."\n fi\nfi\n\nif [ "${UV_INSTALLED}" != true ]; then\n echo "Upgrading pip..."\n python -m pip install --upgrade pip || {\n echo "WARNING: Failed to upgrade pip. Will try to continue with existing pip version."\n pip --version\n }\n\n echo "Installing local-operator package..."\n python -m pip install --verbose local-operator || {\n echo "ERROR: Failed to install local-operator package. Exit code: $?"\n echo "Python version:"\n python --version\n echo "pip version:"\n pip --version\n echo "Available pip packages:"\n pip list\n exit 1\n }\nfi\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n log "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator" || true\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n \n # Try to run the version command with full error output\n log "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n log "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n log "Binary details:"\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n log "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator" || true\n log ""\n log "This could be due to missing dependencies or architecture incompatibility."\n log "Try running the binary manually to see specific errors."\n \n # Don\'t exit here - just warn the user\n log "WARNING: Installation completed but binary verification failed."\n }\n \n # Create a summary of the installation\n SUMMARY_FILE="${APP_DATA_DIR}/installation-summary-${TIMESTAMP}.txt"\n {\n echo "=== Local Operator Backend Installation Summary ==="\n echo "Timestamp: $(date)"\n echo "Python Version: $("${PYTHON_BIN}" --version 2>&1)"\n echo "Virtual Environment: ${VENV_PATH}"\n echo "Binary Location: ${VENV_PATH}/bin/local-operator"\n echo "To activate the virtual environment, run: source ${VENV_PATH}/bin/activate"\n echo "To test the installation, run: ${VENV_PATH}/bin/local-operator --version"\n echo "Log file: ${LOG_FILE}"\n echo "=================================================="\n } > "${SUMMARY_FILE}"\n \n log "Installation summary saved to: ${SUMMARY_FILE}"\nelse\n log "Error: Failed to install Local Operator backend. Binary not found."\n log "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/" || true\n log "Check the log file for details: ${LOG_FILE}"\n error_exit "Installation failed. Binary not installed properly."\nfi\n\nlog "Installation completed successfully at $(date)"\n\n# Clean up temporary files now that installation is complete\ncleanup\n\n# Print final instructions\nlog ""\nlog "=== INSTALLATION COMPLETE ==="\nlog "You can now use the Local Operator backend by running: ${VENV_PATH}/bin/local-operator"\nlog "Installation directory: ${VENV_PATH}"\nlog "Log file: ${LOG_FILE}"\nlog "================================"\n';
|
|
12748
|
-
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# --- The installer this script prefers: the app\'s own bundled uv -----------------\n#\n# Resolved HERE, above the environment rather than beside the package install,\n# because uv builds the environment too (see the note at the creation below).\n#\n# The app hands the path of the pinned, bundled uv in `LOCAL_OPERATOR_UV_BIN`\n# (`src/main/backend/uv-tool.ts`). Nothing here searches PATH for a uv: an\n# installed uv is a version and a configuration nobody in this repository chose,\n# and the point of bundling one is that the install is the same for every user.\nUV_BIN="${LOCAL_OPERATOR_UV_BIN:-}"\n\n# Drop every UV_* variable the launching environment carried.\n#\n# WHY THE WHOLE NAMESPACE rather than a list of the dangerous ones: uv reads its\n# configuration from `UV_*` and from `uv.toml`, and both are the caller\'s, not\n# this app\'s. Measured on uv 0.12.17: with a user-level `uv.toml` naming an index\n# that is not reachable, `uv pip install --dry-run six` fails with `tcp connect\n# error`; `UV_NO_CONFIG=1` makes the same command resolve from PyPI. And an\n# ambient `UV_INDEX_URL` changes where packages come from - while `PIP_INDEX_URL`\n# does not affect uv at all (measured, both directions). Unsetting a name list\n# would drift the day uv adds a variable; unsetting the namespace cannot.\n#\n# IT RUNS BEFORE THE SETTINGS BELOW ARE SET, and that order is load-bearing: a\n# sweep after them takes them away, and an empty `UV_CACHE_DIR` is not "use the\n# default cache" - uv exits 2 with `a value is required for \'--cache-dir\n# <CACHE_DIR>\'`. Measured by running this script on the uv path, where the\n# failure first surfaced as a silent pip install, because the fallback below\n# caught it exactly as designed.\nfor uv_ambient in $(env | sed -n \'s/^\\(UV_[A-Za-z0-9_]*\\)=.*/\\1/p\'); do\n unset "$uv_ambient"\ndone\n\n# The cache lives under the app\'s own support directory rather than the user\'s\n# shared `~/.cache/uv`, so the install neither reads nor pollutes a cache that\n# another tool (or another version of uv) is maintaining. Written here, after the\n# sweep, and handed to uv per invocation rather than exported.\n#\n# IT PERSISTS, and that is worth knowing on a user\'s disk: a full install leaves\n# ~118 MB there (measured; pip\'s own cache for the same dependency set is ~40 MB\n# and it also persists). Nothing else reads it today - the app\'s backend-update\n# path installs with pip - so it is there for the next provisioning or repair,\n# and it is what makes a retry converge in ~1.5 s instead of ~20 s.\nUV_CACHE_DIR="$APP_DATA_DIR/uv-cache"\n\n# Is the handed-down uv something we can actually run?\nuv_is_usable() {\n [ -n "$UV_BIN" ] && [ -x "$UV_BIN" ] && "$UV_BIN" --version >/dev/null 2>&1\n}\n\n# UV_NO_CONFIG: never read `pyproject.toml`/`uv.toml`, wherever they are.\n# UV_PYTHON_DOWNLOADS=never: this install uses the interpreter it was handed and\n# may never fetch another, which is also what keeps it working offline.\n# UV_CACHE_DIR: this install\'s own cache, passed explicitly for the same reason.\n# UV_SYSTEM_CERTS: trust the PLATFORM trust store, not only the root bundle uv\n# ships. It is OFF by default, which is the default this line changes, and the\n# flag\'s own name in the bundled uv is `--system-certs`.\n#\n# WHY THIS LINE EXISTS AT ALL. A network that inspects TLS - a corporate proxy,\n# an admin- or profile-installed root - carries its root in the platform store,\n# and uv does NOT read that store by default: it fails the handshake against\n# the roots compiled into it. This flag is the whole of the difference between\n# a machine that cannot install at all and one that installs normally.\n#\n# AND WHY THE PIP FALLBACK IS HANDED NO CA SETTING: pip 24.2 and newer read the\n# platform store by default, in addition to the Mozilla bundle they ship\n# (`truststore`, "always on since 24.2"), and every pip these installs produce\n# is newer than that - uv\'s own seed and the bundled interpreter\'s ensurepip\n# alike - so a root added to the store is trusted by BOTH clients and no\n# `PIP_CERT`/`SSL_CERT_FILE` is needed. The note this replaces claimed the\n# opposite ("neither client" would see the store; the fallback "resolves\n# against certifi"), which was true only of pip before 24.2; corrected rather\n# than deleted so it is not restored (review round 1, R1-2). The one shape it\n# does not cover is a dev checkout on a system python old enough to predate\n# truststore.\n#\n# IT CANNOT MAKE THINGS WORSE, which is why it is set unconditionally rather than\n# probed for: every uv call below is already followed by the pip fallback on a\n# non-zero exit, so the cost of a machine whose platform store cannot be read is\n# one failed uv attempt and the path that shipped before uv was bundled.\nuv_run() {\n UV_NO_CONFIG=1 UV_PYTHON_DOWNLOADS=never UV_SYSTEM_CERTS=1 \\\n UV_CACHE_DIR="$UV_CACHE_DIR" \\\n "$UV_BIN" "$@"\n}\n\n# Create virtual environment if it doesn\'t exist\nif [ ! -d "$VENV_PATH" ]; then\n echo "|LO1:environment"\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 # THE ENVIRONMENT: uv first, the interpreter\'s own venv module second.\n #\n # WHY uv. `python -m venv` seeds pip by running the interpreter\'s own\n # `ensurepip`, and that is the slow half of creating an environment. Measured on\n # this machine against the bundled interpreter, cold, one run each: `python -m\n # venv` 4.74 s against `uv venv --seed` 1.22 s. Those are this box\'s numbers\n # under fleet load, so read the saving as 3.5 s HERE and not as a constant; the\n # scoping report measured 5.1 s on a quieter machine. uv produces the same\n # environment: the same `pyvenv.cfg` home pointing at the external runtime, a\n # `bin/python` symlinked at the interpreter it was handed.\n #\n # WHY `--seed`, and this is the part the note that used to stand here got\n # wrong. The app\'s backend-update path runs `<venv>/bin/python -m pip install\n # --upgrade local-operator` inside this environment (`update-service.ts`,\n # `buildPipUpgradeCommand`), so the environment needs the pip MODULE. The old\n # note said `uv venv` "produces an environment with no pip at all" - true of\n # bare `uv venv`, false of the command below: `--seed` installs pip into the\n # environment (measured: `pip 26.2.1` in the seeded venv against `pip 25.0.1`\n # from the ensurepip wheel). The stale reason is deleted rather than left\n # standing, because a comment asserting a constraint that no longer holds is\n # how the next reader avoids a correct change.\n # Verified end to end before this change, not argued: the app\'s own command was\n # run inside one of these seeded environments and installed `local-operator\n # 0.63.6`, after which `<venv>/bin/local-operator --version` answered.\n #\n # WHY THE FALLBACK IS NOT OPTIONAL. `uv venv --seed` fetches the pip wheel, so\n # it needs the network - a phase earlier than the package install does. A uv\n # that cannot run, or that fails this one step, therefore falls through to the\n # interpreter\'s own `venv`, which is the path that shipped until now. The `rm`\n # on that path removes only the directory THIS attempt just made: the guard\n # above proved the path did not exist a moment ago.\n VENV_CREATED=false\n if uv_is_usable; then\n echo "Creating virtual environment with uv ($("$UV_BIN" --version 2>/dev/null || echo \'version unavailable\'))..."\n if uv_run venv --seed --python "$PYTHON_BIN" "$VENV_PATH"; then\n VENV_CREATED=true\n else\n UV_VENV_STATUS=$?\n echo "WARNING: the bundled uv could not create the environment (exit ${UV_VENV_STATUS}); retrying with the interpreter\'s own venv module."\n rm -rf "$VENV_PATH"\n fi\n fi\n\n if [ "$VENV_CREATED" != true ]; then\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 fi\n echo "Successfully created virtual environment"\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ] || [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Virtual environment is missing critical components"\n echo "Contents of virtual environment directory:"\n ls -la "$VENV_PATH"\n if [ -d "$VENV_PATH/bin" ]; then\n echo "Contents of bin directory:"\n ls -la "$VENV_PATH/bin"\n fi\n exit 1\nfi\necho "Virtual environment structure verified"\n\n# --- The package install: uv when there is one, pip otherwise ------------------\n#\n# WHY UV. Installing the backend is the phase a user waits through on a first\n# run, and pip spends it resolving and fetching serially. Measured on this\n# machine, cold cache, three runs each, the same interpreter and dependency set:\n# pip\'s package install is 33.0-40.7 s against uv\'s 12.8-16.1 s, plus the\n# 2.3-2.8 s `pip install --upgrade pip` the uv path skips. QA\'s independent pair\n# on a quieter box was 33.9 s against 22.8 s, so read the ratio as 1.5-2.8x\n# ACROSS those two operators, and the seconds as this box\'s. Warm, a retry or a\n# repair: 15.9-33.8 s against 0.65-1.57 s.\n#\n# THE FIGURES THIS COMMENT USED TO QUOTE ARE WITHDRAWN, and this note is here so\n# they are not restored: "pip 128.9 s against uv 14.8 s, 178.3 s against 4.5 s\n# warm". The pip reading was taken at load ~90 and did not reproduce at five\n# further attempts. A comment that argues from a withdrawn number is how the next\n# maintainer decides on a ratio five times the measured one - and the decision it\n# argues is whether ~16-20 MiB per artifact earns its place, so the number is\n# load-bearing. `docs/BUILD.md` carries the full set.\n#\n# WHY A FALLBACK RATHER THAN UV ALONE. uv is a NEW resource in the bundle, and\n# every artifact built before this change has none. The pip path below is the one\n# that shipped until now, unchanged, and it runs whenever uv is absent or cannot\n# do the job - a dev checkout whose `pnpm setup-python` was never run, an older\n# artifact, a uv the platform refuses to spawn, a uv install that failed. A\n# fallback that has never been exercised is a claim rather than a feature, which\n# is why the CI install-script jobs run this script with no uv at all.\n#\n# THE ENVIRONMENT ABOVE ALREADY HAS PIP IN IT, and that is load-bearing: the app\'s\n# backend-update path runs `<venv>/bin/python -m pip install --upgrade\n# local-operator` inside this same environment (`update-service.ts`,\n# `buildPipUpgradeCommand`). Both creation paths provide it - `uv venv --seed`\n# installs pip, `python -m venv` seeds it from `ensurepip` - and the structural\n# check above (`"$VENV_PATH/bin/pip"`) is what holds that on both.\n#\n# `UV_BIN`, the ambient-`UV_*` sweep, `UV_CACHE_DIR` and `uv_run` are defined\n# beside that creation, above, because uv builds the environment as well as\n# installing into it.\n\n# Activate virtual environment and install local-operator\necho "Installing local-operator in virtual environment..."\nsource "$VENV_PATH/bin/activate"\n\n# Check network connectivity to PyPI. A DIAGNOSTIC, not a gate: the install below\n# decides whether it can proceed.\n#\n# What this answers, stated precisely because an earlier version of this comment\n# claimed more than the flags buy: "did a TLS fetch to PyPI\'s JSON API complete,\n# and did the answer come back as JSON?". `--fail` turns an HTTP ERROR status\n# into a non-zero exit - a proxy\'s 403/407, any 4xx/5xx - and does NOT notice a\n# captive portal answering 200 with its own HTML page (measured: a portal-shaped\n# 200 returns exit 0 with AND without the flag). `-o /dev/null` cannot tell a\n# portal\'s page from PyPI\'s JSON either, so the content type is what\n# discriminates, and on a captive network a probe that reports "reachable" while\n# pip is about to fail is the false negative this warning exists to catch.\n#\n# What the content type does NOT prove, stated so this paragraph is not read as\n# more than it says: a proxy answering 200 with `application/json` and an error\n# body (`{"detail":"blocked by proxy policy"}`) is silent here, because the\n# answer did come back as JSON. Only parsing the body - a fetch of PyPI\'s own\n# payload shape - would tell those apart, and a diagnostic that costs a parse is\n# not what stands in front of an install.\n#\n# Two bounds, two jobs: `--connect-timeout 5` ends a black-hole network (a\n# connect that never completes), `--max-time 30` stops a connected-but-stalled\n# peer. Both must be POSITIVE: `--max-time 0` and `--connect-timeout 0` disable\n# the bound rather than making it immediate, which is why the test beside this\n# script requires `[1-9]`. 30 rather than 10 because the total must not fire on a\n# slow-but-working link: a working endpoint that answered in 15s tripped a 10s\n# total bound and printed this warning on an install that then succeeded, and a\n# warning that cries wolf is one users learn to ignore.\necho "Checking network connectivity to PyPI..."\nPYPI_PROBE_CONTENT_TYPE=$(curl -s --fail --connect-timeout 5 --max-time 30 -o /dev/null -w \'%{content_type}\' https://pypi.org/pypi/local-operator/json) || PYPI_PROBE_CONTENT_TYPE=""\nif [[ "$PYPI_PROBE_CONTENT_TYPE" != application/json* ]]; then\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n echo "Attempting to ping common domains to diagnose network issues:"\n ping -c 1 -W 2000 google.com || echo "Cannot ping google.com"\n ping -c 1 -W 2000 pypi.org || echo "Cannot ping pypi.org"\nfi\n\n# --- Progress markers -------------------------------------------------------\n# One whole line per phase, read by the app and shown in the setup window. The\n# app matches the ENTIRE line (`|LO1:<phase>`, see src/shared/install-progress.ts)\n# and never a substring, so a marker has to stand alone: do not wrap it in\n# other text, do not re-indent it into a longer sentence, and do not emit one\n# for work this script does not actually do. A missing marker leaves the window\n# on its previous step, which is the honest failure; a marker a log line also\n# happens to produce is a wrong step presented as a measurement.\n#\n# WHY THE SCRIPT AND NOT ONLY THE APP: the install below is minutes of work on a\n# cold machine, and the app cannot see inside the venv it is about to create -\n# this is the only process that knows when the environment exists and when the\n# download starts.\necho "|LO1:components"\nUV_INSTALLED=false\nif uv_is_usable; then\n echo "Installing local-operator with uv ($("$UV_BIN" --version 2>/dev/null || echo \'version unavailable\'))..."\n # No `pip install --upgrade pip` on this path: uv does not use pip, so the\n # upgrade would be a whole extra network round trip that changes nothing about\n # the result.\n if uv_run pip install --python "$VENV_PATH/bin/python" --upgrade local-operator; then\n UV_INSTALLED=true\n echo "local-operator installation with uv successful"\n else\n # WHY THE EXIT CODE IS PRINTED (review round 1, QA Q2): this fallback has to\n # be forgiving - an install must not fail because uv did - but "a bundled uv\n # is present and fails" is a defect rather than a degraded path, and this line\n # is the only place it shows up: the exit code is 0 and the UI is unchanged.\n # `uv_is_usable` passing and a uv install SUCCEEDING are two different facts.\n UV_STATUS=$?\n echo "WARNING: the bundled uv is present but its install failed (exit ${UV_STATUS}); retrying with pip, which is what this script used before uv was bundled."\n fi\nelse\n echo "Bundled uv not available (LOCAL_OPERATOR_UV_BIN=${UV_BIN:-unset}); installing with pip."\nfi\n\nif [ "$UV_INSTALLED" != true ]; then\n echo "Upgrading pip..."\n python -m pip install --upgrade pip || {\n echo "ERROR: Failed to upgrade pip. Exit code: $?"\n echo "pip version before failing:"\n pip --version\n exit 1\n }\n echo "pip upgrade successful:"\n pip --version\n\n echo "Installing local-operator package..."\n python -m pip install --upgrade --verbose local-operator || {\n echo "ERROR: Failed to install local-operator package. Exit code: $?"\n echo "Python version:"\n python --version\n echo "pip version:"\n pip --version\n echo "Available pip packages:"\n pip list\n echo "Pip config:"\n pip config list\n echo "Network diagnosis:"\n curl -sI --fail --connect-timeout 5 --max-time 30 https://pypi.org || echo "Cannot reach PyPI server"\n exit 1\n }\nfi\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n echo "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator"\n file "$VENV_PATH/bin/local-operator"\n \n # Try to run the version command with full error output\n echo "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n echo "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n echo "Binary details:"\n file "$VENV_PATH/bin/local-operator"\n echo "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator"\n echo "Dependencies:"\n if command -v otool >/dev/null; then\n otool -L "$VENV_PATH/bin/local-operator" || echo "Could not get dependencies with otool"\n fi\n exit 1\n }\nelse\n echo "Error: Failed to install Local Operator backend. Binary not found."\n echo "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/"\n exit 1\nfi\n\necho "$(date): Installation completed successfully."\n';
|
|
12749
|
-
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 "|LO1:environment"\n Write-Output "Creating virtual environment at $VenvPath..."\n \n # Ensure the directory exists\n if (-not (Test-Path $AppDataDir)) {\n New-Item -ItemType Directory -Path $AppDataDir -Force | Out-Null\n Write-Output "Created directory: $AppDataDir"\n }\n \n # Use the full path to python from pyenv\n $PythonExe = "$PyenvDir\\\\pyenv-win\\\\versions\\\\$PythonVersion\\\\python.exe"\n \n if (Test-Path $PythonExe) {\n Write-Output "Using Python at: $PythonExe"\n & $PythonExe -m venv $VenvPath\n } else {\n Write-Output "Using system Python"\n & python -m venv $VenvPath\n }\n \n if ($LASTEXITCODE -ne 0) {\n Write-Error "Failed to create virtual environment"\n exit 1\n }\n}\n\n# Verify the virtual environment was created\nif (-not (Test-Path "$VenvPath\\\\Scripts\\\\Activate.ps1")) {\n Write-Error "Virtual environment activation script not found at $VenvPath\\\\Scripts\\\\Activate.ps1"\n exit 1\n}\n\n# --- The package install: uv when there is one, pip otherwise ------------------\n#\n# Same shape as the macOS and Linux scripts, and for the same reasons: uv resolves\n# and fetches in parallel - measured on macOS, cold cache, three runs each, same\n# interpreter: uv\'s package install is 12.8-16.1 s against pip\'s 33.0-40.7 s,\n# plus the 2.3-2.8 s pip self-upgrade this path skips, so 1.5-2.8x across two\n# operators rather than the "14.8 s against 128.9 s" quoted here before that\n# reading was withdrawn (`docs/BUILD.md` has the full set). The pip path below is\n# unchanged and runs whenever uv is absent or cannot do the job, and pip STAYS in\n# the venv because the app\'s backend-update path runs `pip install --upgrade\n# local-operator` inside this same environment - which is why the environment must\n# keep pip, and NOT a reason to avoid `uv venv`: the note that stood here said\n# `uv venv` leaves no pip at all, which is true of bare `uv venv` and false of\n# `uv venv --seed`, and this script\'s creation path is still `python -m venv` only\n# because moving it has been measured on macOS and not on this platform yet (see\n# the macOS script for the numbers and the proof).\n#\n# Nothing here searches PATH for a uv: an installed uv is a version and a\n# configuration nobody in this repository chose. `LOCAL_OPERATOR_UV_BIN` is the\n# app\'s own answer (`src/main/backend/uv-tool.ts`).\n$UvBin = $env:LOCAL_OPERATOR_UV_BIN\n$UvCacheDir = "$AppDataDir\\uv-cache"\n\nfunction Test-UvUsable {\n if (-not $UvBin) { return $false }\n if (-not (Test-Path $UvBin)) { return $false }\n try {\n & $UvBin --version | Out-Null\n return ($LASTEXITCODE -eq 0)\n } catch {\n return $false\n }\n}\n\n# Drop every UV_* variable the launching environment carried, then set the four\n# settings this install depends on. Measured on uv 0.12.17: a user-level\n# `uv.toml` naming an unreachable index is obeyed by `uv pip install` and ignored\n# with `UV_NO_CONFIG=1`; an ambient `UV_INDEX_URL` changes where packages come\n# from, while `PIP_INDEX_URL` does not affect uv at all. A name list would drift\n# the day uv adds a variable - the namespace cannot.\n#\n# The names are MATERIALISED first (`@(...)` over a property projection):\n# removing entries of a collection that is still being enumerated is the shape\n# that throws `Collection was modified`.\n$uvAmbientNames = @(\n Get-ChildItem env: |\n Where-Object { $_.Name -like \'UV_*\' } |\n Select-Object -ExpandProperty Name\n)\nforeach ($uvAmbientName in $uvAmbientNames) {\n Remove-Item "env:$uvAmbientName" -ErrorAction SilentlyContinue\n}\n\n# UV_NO_CONFIG: never read `pyproject.toml`/`uv.toml`, wherever they are.\n# UV_PYTHON_DOWNLOADS=never: this install uses the interpreter it was handed and\n# never fetches another.\n# UV_CACHE_DIR: under the app\'s own support directory rather than the user\'s\n# shared uv cache.\n#\n# UV_SYSTEM_CERTS: trust the PLATFORM trust store (the Windows certificate\n# stores), not only the root bundle uv ships - off by default, which is the\n# default this line changes; `--system-certs` is the same setting spelled as a\n# flag in the bundled uv 0.12.17.\n#\n# WHY: on a network that inspects TLS the root is installed in the platform\n# store, and uv does not read that store by default - it fails the handshake\n# against the roots compiled into it. uv names the remedy itself when it fails:\n# "Consider enabling use of system TLS certificates with the `--system-certs`\n# command-line flag". Nothing here passed it.\n#\n# AND WHY THE PIP FALLBACK IS HANDED NO CA SETTING: pip 24.2 and newer read the\n# platform store by default, in addition to the Mozilla bundle they ship\n# (`truststore`, "always on since 24.2"), and every pip these installs produce\n# is newer than that - uv\'s own seed and the bundled interpreter\'s ensurepip\n# alike - so a root added to the Windows certificate stores is trusted by BOTH\n# clients and no `PIP_CERT`/`SSL_CERT_FILE` is needed. The note this replaces\n# claimed the opposite (the fallback "resolves against certifi"), which was\n# true only of pip before 24.2; corrected rather than deleted so it is not\n# restored (review round 1, R1-2). The one shape it does not cover is a dev\n# checkout on a system python old enough to predate truststore.\n#\n# IT CANNOT MAKE THINGS WORSE: every uv call below is already followed by the pip\n# fallback on a non-zero exit, so a platform store that cannot be read costs one\n# failed uv attempt and then the path that shipped before uv was bundled.\n$env:UV_NO_CONFIG = "1"\n$env:UV_PYTHON_DOWNLOADS = "never"\n$env:UV_CACHE_DIR = $UvCacheDir\n$env:UV_SYSTEM_CERTS = "1"\n\n# Activate virtual environment and install local-operator\n# --- Progress markers -------------------------------------------------------\n# One whole line per phase, read by the app and shown in the setup window. The\n# app matches the ENTIRE line (`|LO1:<phase>`, see src/shared/install-progress.ts)\n# and never a substring, so a marker has to stand alone: do not wrap it in\n# other text, do not re-indent it into a longer sentence, and do not emit one\n# for work this script does not actually do. A missing marker leaves the window\n# on its previous step, which is the honest failure; a marker a log line also\n# happens to produce is a wrong step presented as a measurement.\n#\n# WHY THE SCRIPT AND NOT ONLY THE APP: the install below is minutes of work on a\n# cold machine, and the app cannot see inside the venv it is about to create -\n# this is the only process that knows when the environment exists and when the\n# download starts.\nWrite-Output "|LO1:components"\nWrite-Output "Installing local-operator in virtual environment..."\n# Use PowerShell to run the activation script\n& "$VenvPath\\\\Scripts\\\\Activate.ps1"\n\n$UvInstalled = $false\nif (Test-UvUsable) {\n Write-Output "Installing local-operator with uv..."\n # No `pip install --upgrade pip` on this path: uv does not use pip.\n & $UvBin pip install --python "$VenvPath\\Scripts\\python.exe" --upgrade local-operator\n if ($LASTEXITCODE -eq 0) {\n $UvInstalled = $true\n Write-Output "local-operator installation with uv successful"\n } else {\n # The exit code is printed for the same reason as on macOS and Linux: the\n # fallback is deliberately forgiving, so this line is the only evidence\n # that a bundled uv is present and failing for every user (QA Q2).\n Write-Output "WARNING: the bundled uv is present but its install failed (exit $LASTEXITCODE); retrying with pip, which is what this script used before uv was bundled."\n }\n} else {\n if ($UvBin) {\n Write-Output "Bundled uv at $UvBin could not be run on this machine; installing with pip."\n } else {\n Write-Output "Bundled uv not available (LOCAL_OPERATOR_UV_BIN unset); installing with pip."\n }\n}\n\nif (-not $UvInstalled) {\n & python -m pip install --upgrade pip\n & python -m pip install --upgrade local-operator\n\n if ($LASTEXITCODE -ne 0) {\n Write-Error "Failed to install packages in virtual environment"\n exit 1\n }\n}\n\n# Verify installation\n$LocalOperatorInstalled = $false\ntry {\n # First try to run local-operator directly (it should be in PATH from the activated venv)\n $Version = & local-operator --version\n $LocalOperatorInstalled = $true\n Write-Output "Local Operator backend installed successfully!"\n Write-Output $Version\n} catch {\n Write-Output "Could not run local-operator directly, trying with full path..."\n try {\n # Try with explicit path\n $LocalOperatorExe = "$VenvPath\\\\Scripts\\\\local-operator.exe"\n if (Test-Path $LocalOperatorExe) {\n $Version = & $LocalOperatorExe --version\n $LocalOperatorInstalled = $true\n Write-Output "Local Operator backend installed successfully at $LocalOperatorExe!"\n Write-Output $Version\n } else {\n # Try without .exe extension\n $LocalOperatorCmd = "$VenvPath\\\\Scripts\\\\local-operator"\n if (Test-Path $LocalOperatorCmd) {\n $Version = & $LocalOperatorCmd --version\n $LocalOperatorInstalled = $true\n Write-Output "Local Operator backend installed successfully at $LocalOperatorCmd!"\n Write-Output $Version\n } else {\n Write-Error "Error: local-operator executable not found in expected locations."\n \n # Create a symlink to make it more accessible\n Write-Output "Attempting to create a symlink for local-operator..."\n $PythonModule = "$VenvPath\\\\Lib\\\\site-packages\\\\local_operator"\n if (Test-Path $PythonModule) {\n Write-Output "Found local_operator module at $PythonModule"\n \n # Create a batch file that runs the module\n $BatchContent = "@echo off`npython -m local_operator %*"\n Set-Content -Path "$VenvPath\\\\Scripts\\\\local-operator.bat" -Value $BatchContent\n \n # Test the batch file\n if (Test-Path "$VenvPath\\\\Scripts\\\\local-operator.bat") {\n Write-Output "Created local-operator.bat in Scripts directory"\n $LocalOperatorInstalled = $true\n } else {\n Write-Error "Failed to create local-operator.bat"\n exit 1\n }\n } else {\n Write-Error "local_operator module not found in site-packages"\n exit 1\n }\n }\n }\n } catch {\n Write-Error "Error: Failed to install Local Operator backend."\n Write-Error $_.Exception.Message\n exit 1\n }\n}\n\n# If we got here, installation was successful\nif ($LocalOperatorInstalled) {\n Write-Output "Local Operator backend installation verified."\n} else {\n Write-Error "Error: Failed to verify Local Operator backend installation."\n exit 1\n}\n\nWrite-Output "$(Get-Date): Installation completed successfully."\n\n# Properly close the transcript and ensure it\'s released\ntry {\n Write-Output "Finalizing installation and releasing resources..."\n # Flush any pending output\n [System.Console]::Out.Flush()\n # Stop transcript properly\n Stop-Transcript\n \n # Add a small delay to ensure file handles are released\n Start-Sleep -Seconds 1\n \n # Explicitly release any COM objects\n [System.GC]::Collect()\n [System.GC]::WaitForPendingFinalizers()\n \n Write-Output "Installation completed and resources released."\n} catch {\n Write-Error "Error during cleanup: $_"\n}\n';
|
|
12776
|
+
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# The `python` stage (code review round 1, R1): this script owns the same stage\n# macOS gets from `managed-python.ts` and Windows from its own marker - finding\n# (and, where there is none, refusing to guess about) the interpreter everything\n# below runs on. Without it the panel opened on "Step 2 of 4" with the rail\'s\n# first row never lit. `|LO1:` lines are the app\'s milestone vocabulary\n# (`src/shared/install-progress.ts`), matched on the WHOLE line.\n#\n# THE MARKER PRECEDES THE CONNECTIVITY PROBE (code review round 2, N-2, whose\n# whole complaint was that the stage is announced after it): that probe can spend\n# up to 2 x 30 s on a dead network, and it is the first work this stage does - so\n# the panel says "Getting ready" while it waits instead of showing a step with no\n# clock and no estimate for a minute.\necho "|LO1:python"\ncheck_connectivity\n\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 # ...AND THEN WHATEVER `PATH` OFFERS, which is the half this probe used to\n # ignore entirely.\n #\n # WHY IT MATTERS, stated as the user story rather than as a rule: Python 3.12\n # on Linux usually arrives from a version manager (pyenv, mise, asdf, uv) or a\n # Homebrew/Linuxbrew prefix, and every one of those puts its interpreter\n # somewhere this list does not name while putting it on `PATH`. The list above\n # is what the app\'s own CI runner has (Ubuntu\'s `/usr/bin/python3`), so the gap\n # was invisible there and the report on the user\'s machine was the wrong one:\n # "No suitable Python installation found (version 3.12 or higher required).\n # Please install Python 3.12 or higher" - about a machine that had 3.12\n # installed and working, sending them to reinstall what they already had.\n #\n # APPENDED, NOT PREPENDED. A version manager on `PATH` can point at any minor\n # version, and the fixed list above is the more predictable of the two where it\n # resolves at all; the version and architecture checks in the loop below are\n # unchanged and still decide, so an entry added here is a candidate and never\n # an answer.\n for python_name in python3.12 python3 python; do\n resolved_python="$(command -v "${python_name}" 2>/dev/null || true)"\n if [ -z "${resolved_python}" ]; then\n continue\n fi\n # Deduplicated by hand: a repeated candidate costs a python spawn each and\n # repeats its own "below the required version" line, which reads as several\n # separate problems rather than one machine being looked at three times.\n already_listed=false\n for listed_path in "${POSSIBLE_PYTHON_PATHS[@]}"; do\n if [ "${listed_path}" = "${resolved_python}" ]; then\n already_listed=true\n break\n fi\n done\n if [ "${already_listed}" = false ]; then\n POSSIBLE_PYTHON_PATHS+=("${resolved_python}")\n fi\n done\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 # WHAT WAS SEARCHED IS PART OF THE DIAGNOSIS, and printing it is the second\n # half of the fix above: the sentence this used to end with told a user who\n # already had 3.12 to install it. Naming the paths makes the two real cases\n # tell themselves apart - nothing is installed, or something is installed and\n # this process cannot see it - and each of those has its own remedy.\n log "No suitable Python 3.12+ was found. Checked: ${POSSIBLE_PYTHON_PATHS[*]}"\n error_exit "No Python 3.12+ found: not in the standard locations and not on this process\'s PATH. Install Python 3.12 or higher (https://www.python.org/downloads/), or make an existing 3.12+ visible on this process\'s PATH."\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# --- The installer this script prefers: the app\'s own bundled uv ---------------\n#\n# Same shape as the macOS script, and for the same reasons: uv resolves and\n# fetches in parallel - measured there, cold cache, three runs each, same\n# interpreter: uv\'s package install is 12.8-16.1 s against pip\'s 33.0-40.7 s,\n# plus the 2.3-2.8 s pip self-upgrade this path skips, so 1.5-2.8x across two\n# operators rather than the "14.8 s against 128.9 s" quoted here before that\n# reading was withdrawn (`docs/BUILD.md` has the full set). The pip path below is\n# unchanged and runs whenever uv is absent or cannot do the job, and pip STAYS in\n# the venv because the app\'s backend-update path runs `<venv>/bin/python -m pip\n# install --upgrade local-operator` inside this same environment - which is why\n# the environment must keep pip, and NOT a reason to avoid `uv venv`: bare\n# `uv venv` leaves no pip at all, `uv venv --seed` does, and `--seed` is what the\n# creation below passes.\n#\n# RESOLVED HERE, ABOVE THE ENVIRONMENT, because uv builds the environment too\n# now (first-run onboarding, Q13), exactly as it already did on macOS: the\n# creation below tries `uv venv --seed` first and falls through to the venv\n# module, then virtualenv, then the minimal bootstrap - every path that shipped\n# before is still behind it.\n#\n# Nothing here searches PATH for a uv: an installed uv is a version and a\n# configuration nobody in this repository chose. `LOCAL_OPERATOR_UV_BIN` is the\n# app\'s own answer (`src/main/backend/uv-tool.ts`).\nUV_BIN="${LOCAL_OPERATOR_UV_BIN:-}"\n\n# Drop every UV_* variable the launching environment carried. Measured on uv\n# 0.12.17: a user-level `uv.toml` naming an unreachable index is obeyed by\n# `uv pip install` and ignored with `UV_NO_CONFIG=1`; an ambient `UV_INDEX_URL`\n# changes where packages come from, while `PIP_INDEX_URL` does not affect uv at\n# all. A name list would drift the day uv adds a variable - the namespace cannot.\n#\n# IT RUNS BEFORE THE SETTINGS BELOW ARE SET: a sweep after them takes them away,\n# and an empty `UV_CACHE_DIR` makes uv exit 2 with `a value is required for\n# \'--cache-dir <CACHE_DIR>\'` rather than falling back to a default.\nfor uv_ambient in $(env | sed -n \'s/^\\(UV_[A-Za-z0-9_]*\\)=.*/\\1/p\'); do\n unset "$uv_ambient"\ndone\n\n# The cache lives under the app\'s own support directory rather than the user\'s\n# shared `~/.cache/uv`, and is handed to uv per invocation rather than exported.\n# It persists (~118 MB for a full install, measured on macOS) and nothing else\n# reads it today; it is what makes a retry converge in seconds rather than tens\n# of seconds.\nUV_CACHE_DIR="${APP_DATA_DIR}/uv-cache"\n\n# Is the handed-down uv something we can actually run?\nuv_is_usable() {\n [ -n "${UV_BIN}" ] && [ -x "${UV_BIN}" ] && "${UV_BIN}" --version >/dev/null 2>&1\n}\n\n# UV_NO_CONFIG: never read `pyproject.toml`/`uv.toml`, wherever they are.\n# UV_PYTHON_DOWNLOADS=never: this install uses the interpreter it was handed and\n# never fetches another.\n# UV_SYSTEM_CERTS: trust the PLATFORM trust store, not only the root bundle uv\n# ships. Off by default, which is the default this line changes; `--system-certs`\n# is the same setting spelled as a flag in the bundled uv 0.12.17.\n#\n# WHY: on a network that inspects TLS the root lives in the platform store (here,\n# `/etc/ssl/certs`), and uv does not read that store by default - it fails the\n# handshake against the roots compiled into it. uv names this remedy itself when\n# it fails: "Consider enabling use of system TLS certificates with the\n# `--system-certs` command-line flag". Nothing here passed it, so the failure\n# went to the fallback and the user was told to check a network that was working\n# for every other application on the machine.\n#\n# AND WHY THE PIP FALLBACK IS HANDED NO CA SETTING: pip 24.2 and newer read the\n# platform store by default, in addition to the Mozilla bundle they ship\n# (`truststore`, "always on since 24.2"), and every pip these installs produce\n# is newer than that - uv\'s own seed and the bundled interpreter\'s ensurepip\n# alike - so a root added to `/etc/ssl/certs` is trusted by BOTH clients and no\n# `PIP_CERT`/`SSL_CERT_FILE` is needed. The note this replaces claimed the\n# opposite (the fallback "resolves against certifi"; "neither client" would see\n# the store), which was true only of pip before 24.2; corrected rather than\n# deleted so it is not restored (review round 1, R1-2). The one shape it does\n# not cover is a dev checkout on a system python old enough to predate\n# truststore.\n#\n# IT CANNOT MAKE THINGS WORSE: every uv call below is already followed by the pip\n# fallback on a non-zero exit, so a platform store that cannot be read costs one\n# failed uv attempt and then the path that shipped before uv was bundled.\nuv_run() {\n UV_NO_CONFIG=1 UV_PYTHON_DOWNLOADS=never UV_SYSTEM_CERTS=1 \\\n UV_CACHE_DIR="${UV_CACHE_DIR}" \\\n "${UV_BIN}" "$@"\n}\n\n# Create virtual environment if it doesn\'t exist\nif [ ! -d "$VENV_PATH" ]; then\n echo "|LO1:environment"\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 # uv first (first-run onboarding, Q13): `uv venv --seed` builds the\n # environment AND seeds pip from uv\'s cache in about a second, where the venv\n # module runs ensurepip\'s bundled wheel through a full pip install. Measured on\n # macOS with the same pinned uv: 1.2 s for the whole environment phase. The\n # interpreter is the one this script already validated, and `uv_run` keeps\n # UV_PYTHON_DOWNLOADS=never, so uv cannot swap in a Python of its own.\n # A uv that cannot run or fails falls through to every path below, unchanged;\n # the `rm` removes only what this attempt made (the guard above proved the\n # path absent a moment ago).\n if uv_is_usable; then\n echo "Creating virtual environment with uv ($("${UV_BIN}" --version 2>/dev/null || echo \'version unavailable\'))..."\n if uv_run venv --seed --python "$PYTHON_BIN" "$VENV_PATH"; then\n VENV_CREATE_STATUS=0\n else\n echo "WARNING: the bundled uv could not create the environment (exit $?); retrying with the interpreter\'s own venv module."\n rm -rf "$VENV_PATH"\n fi\n fi\n\n # Then the venv module, if available\n if [ $VENV_CREATE_STATUS -ne 0 ] && [ $VENV_AVAILABLE -eq 0 ] && [ $ENSUREPIP_AVAILABLE -eq 0 ]; then\n echo "Creating virtual environment using venv module..."\n "$PYTHON_BIN" -m venv "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n fi\n \n # If venv failed, try virtualenv\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n echo "venv creation failed with status $VENV_CREATE_STATUS. Falling back to virtualenv..."\n if command_exists virtualenv; then\n virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n elif "$PYTHON_BIN" -m pip list | grep -q virtualenv; then\n "$PYTHON_BIN" -m virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n else\n echo "ERROR: Both venv and virtualenv are unavailable or failed."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n echo "You can install Python 3.12 from https://www.python.org/downloads/"\n exit 1\n fi\n fi\n \n # Check if virtual environment creation was successful\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n log "ERROR: Failed to create virtual environment. Exit code: $VENV_CREATE_STATUS"\n log "Virtual environment path: $VENV_PATH"\n log "Python binary used: $PYTHON_BIN"\n ls -la "$(dirname "$VENV_PATH")" || true\n log "Python executable permissions:"\n ls -la "$PYTHON_BIN" || true\n \n # Check for common issues\n log "Checking for common virtual environment creation issues..."\n \n # Check disk space\n df -h "$(dirname "$VENV_PATH")" || true\n \n # Check if directory is writable\n if [ ! -w "$(dirname "$VENV_PATH")" ]; then\n log "ERROR: Directory $(dirname "$VENV_PATH") is not writable"\n fi\n \n # Try to create a minimal virtual environment manually as a last resort\n log "Attempting to create a minimal virtual environment manually..."\n mkdir -p "$VENV_PATH/bin" || error_exit "Could not create directory $VENV_PATH/bin"\n echo "#!/bin/bash" > "$VENV_PATH/bin/activate"\n echo "export VIRTUAL_ENV=\\"$VENV_PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "export PATH=\\"$VENV_PATH/bin:\\$PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "unset PYTHONHOME" >> "$VENV_PATH/bin/activate"\n chmod +x "$VENV_PATH/bin/activate" || error_exit "Could not set execute permissions on activate script"\n \n # Create symlinks to the system Python\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python" || log "WARNING: Failed to create symlink for python"\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python3" || log "WARNING: Failed to create symlink for python3"\n \n # Try to bootstrap pip\n log "Bootstrapping pip in the minimal virtual environment..."\n if command_exists curl; then\n curl -s --fail --connect-timeout 10 --max-time 60 https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py" || log "WARNING: Failed to download get-pip.py"\n elif command_exists wget; then\n wget -q --timeout=30 --tries=1 -O "$APP_DATA_DIR/get-pip.py" https://bootstrap.pypa.io/get-pip.py || log "WARNING: Failed to download get-pip.py"\n else\n log "ERROR: Neither curl nor wget available to download get-pip.py"\n error_exit "Installation cannot continue without being able to download pip"\n fi\n \n # shellcheck disable=SC1090\n source "$VENV_PATH/bin/activate" && python "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n error_exit "Failed to create even a minimal virtual environment."\n else\n log "Created a minimal virtual environment as a fallback."\n fi\n else\n log "Successfully created virtual environment"\n fi\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ]; then\n echo "Python executable missing in virtual environment, creating symlink..."\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python"\nfi\n\nif [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "pip missing in virtual environment, attempting to bootstrap it..."\n curl -s --fail --connect-timeout 10 --max-time 60 https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py"\n "$VENV_PATH/bin/python" "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Failed to bootstrap pip in the virtual environment"\n exit 1\n fi\nfi\n\necho "Virtual environment structure verified"\n\n# --- The package install: uv when there is one, pip otherwise ------------------\n#\n# uv was resolved above the environment (see there); pip is the fallback.\n# Activate virtual environment and install local-operator\necho "Installing local-operator in virtual environment..."\nsource "$VENV_PATH/bin/activate"\n\n# Check network connectivity to PyPI. A DIAGNOSTIC, not a gate: the install below\n# decides whether it can proceed.\n#\n# What this answers, stated precisely because an earlier version of this comment\n# claimed more than the flags buy: "did a TLS fetch to PyPI\'s JSON API complete,\n# and did the answer come back as JSON?". `--fail` turns an HTTP ERROR status\n# into a non-zero exit, and does NOT notice a captive portal answering 200 with\n# its own HTML page (measured: a portal-shaped 200 exits 0 with and without the\n# flag). Both spellings therefore check the CONTENT TYPE as well, since a portal\n# that reports "reachable" while pip is about to fail is the false negative this\n# warning exists to catch: curl through `-w \'%{content_type}\'`, and wget through\n# the response headers `-S` prints (`-S`/`--server-response` is in BusyBox\'s wget\n# since 2017 as well as GNU\'s, which is the wget a system without curl has).\n#\n# What the content type does NOT prove, stated so this paragraph is not read as\n# more than it says: a proxy answering 200 with `application/json` and an error\n# body (`{"detail":"blocked by proxy policy"}`) is silent here, because the\n# answer did come back as JSON. Only parsing the body - a fetch of PyPI\'s own\n# payload shape - would tell those apart, and a diagnostic that costs a parse is\n# not what stands in front of an install.\n#\n# Two bounds, two jobs: the connect bound ends a black-hole network, the total\n# stops a connected-but-stalled peer. Both must be POSITIVE - `--max-time 0` and\n# `--timeout=0` disable the bound rather than making it immediate on both curl and\n# wget. 30 rather than 10 because the total must not fire on a slow-but-working\n# link: a working endpoint that answered in 15s tripped a 10s bound and printed\n# this warning on an install that then succeeded.\necho "Checking network connectivity to PyPI..."\nif command_exists curl; then\n PYPI_PROBE_CONTENT_TYPE=$(curl -s --fail --connect-timeout 5 --max-time 30 -o /dev/null -w \'%{content_type}\' https://pypi.org/pypi/local-operator/json) || PYPI_PROBE_CONTENT_TYPE=""\n if [[ "${PYPI_PROBE_CONTENT_TYPE}" != application/json* ]]; then\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n fi\nelif command_exists wget; then\n # The headers are captured into a variable and matched with a here-string\n # rather than piped into `grep -q`: `-q` exits on the first match, the closed\n # pipe gives wget SIGPIPE, and the script runs under `set -o pipefail`, so a\n # WORKING link printed this warning whenever the match landed before wget had\n # finished writing its headers. The piped shape was this remediation\'s own - the\n # first fix piped it and was measured warning 1 time in 20 against real PyPI\n # (and 1 in 20 against a healthy local endpoint) while this shape warns 0 in 40\n # with the same flags - so the trap is recorded here because it is one line away\n # from being reintroduced, not because the released script ever shipped it.\n PYPI_PROBE_HEADERS=$(wget -q -S --spider --timeout=30 --tries=1 https://pypi.org/pypi/local-operator/json 2>&1) || PYPI_PROBE_HEADERS=""\n if ! grep -qi \'^ *content-type: application/json\' <<< "$PYPI_PROBE_HEADERS"; then\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n fi\nfi\n\n# --- Progress markers -------------------------------------------------------\n# One whole line per phase, read by the app and shown in the setup window. The\n# app matches the ENTIRE line (`|LO1:<phase>`, see src/shared/install-progress.ts)\n# and never a substring, so a marker has to stand alone: do not wrap it in\n# other text, do not re-indent it into a longer sentence, and do not emit one\n# for work this script does not actually do. A missing marker leaves the window\n# on its previous step, which is the honest failure; a marker a log line also\n# happens to produce is a wrong step presented as a measurement.\n#\n# WHY THE SCRIPT AND NOT ONLY THE APP: the install below is minutes of work on a\n# cold machine, and the app cannot see inside the venv it is about to create -\n# this is the only process that knows when the environment exists and when the\n# download starts.\necho "|LO1:components"\nUV_INSTALLED=false\nif uv_is_usable; then\n echo "Installing local-operator with uv ($("${UV_BIN}" --version 2>/dev/null || echo \'version unavailable\'))..."\n # No `pip install --upgrade pip` on this path: uv does not use pip.\n if uv_run pip install --python "${VENV_PATH}/bin/python" --upgrade local-operator; then\n UV_INSTALLED=true\n echo "local-operator installation with uv successful"\n else\n # The exit code matters here for the same reason it does on macOS: the\n # fallback is deliberately forgiving, so this line is the only evidence that\n # a bundled uv is present and failing for every user (QA Q2).\n UV_STATUS=$?\n echo "WARNING: the bundled uv is present but its install failed (exit ${UV_STATUS}); retrying with pip, which is what this script used before uv was bundled."\n fi\nelse\n if [ -n "${UV_BIN}" ]; then\n # Present but not runnable - the wrong architecture for this machine, a\n # truncated copy, a mount that lost the execute bit. Named apart from the\n # absent case below because the two are different facts about the install\n # (the Windows script has the same split, and this line\'s absence is how a\n # staged-for-the-wrong-arch uv reads as "nothing was staged"): both fall\n # back to pip, and only this one is a defect in what was delivered.\n echo "Bundled uv at ${UV_BIN} could not be run on this machine; installing with pip."\n else\n echo "Bundled uv not available (LOCAL_OPERATOR_UV_BIN=${UV_BIN:-unset}); installing with pip."\n fi\nfi\n\nif [ "${UV_INSTALLED}" != true ]; then\n # No pip self-upgrade and no `--verbose`, for the reasons the macOS script\n # gives beside the same lines (first-run onboarding, Q4/Q13): an extra\n # resolve-and-download that changes nothing, and output nobody reads.\n echo "Installing local-operator package..."\n python -m pip install --upgrade local-operator || {\n echo "ERROR: Failed to install local-operator package. Exit code: $?"\n echo "Python version:"\n python --version\n echo "pip version:"\n pip --version\n echo "Available pip packages:"\n pip list\n exit 1\n }\nfi\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n log "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator" || true\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n \n # Try to run the version command with full error output\n log "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n log "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n log "Binary details:"\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n log "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator" || true\n log ""\n log "This could be due to missing dependencies or architecture incompatibility."\n log "Try running the binary manually to see specific errors."\n \n # Don\'t exit here - just warn the user\n log "WARNING: Installation completed but binary verification failed."\n }\n \n # Create a summary of the installation\n SUMMARY_FILE="${APP_DATA_DIR}/installation-summary-${TIMESTAMP}.txt"\n {\n echo "=== Local Operator Backend Installation Summary ==="\n echo "Timestamp: $(date)"\n echo "Python Version: $("${PYTHON_BIN}" --version 2>&1)"\n echo "Virtual Environment: ${VENV_PATH}"\n echo "Binary Location: ${VENV_PATH}/bin/local-operator"\n echo "To activate the virtual environment, run: source ${VENV_PATH}/bin/activate"\n echo "To test the installation, run: ${VENV_PATH}/bin/local-operator --version"\n echo "Log file: ${LOG_FILE}"\n echo "=================================================="\n } > "${SUMMARY_FILE}"\n \n log "Installation summary saved to: ${SUMMARY_FILE}"\nelse\n log "Error: Failed to install Local Operator backend. Binary not found."\n log "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/" || true\n log "Check the log file for details: ${LOG_FILE}"\n error_exit "Installation failed. Binary not installed properly."\nfi\n\nlog "Installation completed successfully at $(date)"\n\n# Clean up temporary files now that installation is complete\ncleanup\n\n# Print final instructions\nlog ""\nlog "=== INSTALLATION COMPLETE ==="\nlog "You can now use the Local Operator backend by running: ${VENV_PATH}/bin/local-operator"\nlog "Installation directory: ${VENV_PATH}"\nlog "Log file: ${LOG_FILE}"\nlog "================================"\n';
|
|
12777
|
+
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# --- The installer this script prefers: the app\'s own bundled uv -----------------\n#\n# Resolved HERE, above the environment rather than beside the package install,\n# because uv builds the environment too (see the note at the creation below).\n#\n# The app hands the path of the pinned, bundled uv in `LOCAL_OPERATOR_UV_BIN`\n# (`src/main/backend/uv-tool.ts`). Nothing here searches PATH for a uv: an\n# installed uv is a version and a configuration nobody in this repository chose,\n# and the point of bundling one is that the install is the same for every user.\nUV_BIN="${LOCAL_OPERATOR_UV_BIN:-}"\n\n# Drop every UV_* variable the launching environment carried.\n#\n# WHY THE WHOLE NAMESPACE rather than a list of the dangerous ones: uv reads its\n# configuration from `UV_*` and from `uv.toml`, and both are the caller\'s, not\n# this app\'s. Measured on uv 0.12.17: with a user-level `uv.toml` naming an index\n# that is not reachable, `uv pip install --dry-run six` fails with `tcp connect\n# error`; `UV_NO_CONFIG=1` makes the same command resolve from PyPI. And an\n# ambient `UV_INDEX_URL` changes where packages come from - while `PIP_INDEX_URL`\n# does not affect uv at all (measured, both directions). Unsetting a name list\n# would drift the day uv adds a variable; unsetting the namespace cannot.\n#\n# IT RUNS BEFORE THE SETTINGS BELOW ARE SET, and that order is load-bearing: a\n# sweep after them takes them away, and an empty `UV_CACHE_DIR` is not "use the\n# default cache" - uv exits 2 with `a value is required for \'--cache-dir\n# <CACHE_DIR>\'`. Measured by running this script on the uv path, where the\n# failure first surfaced as a silent pip install, because the fallback below\n# caught it exactly as designed.\nfor uv_ambient in $(env | sed -n \'s/^\\(UV_[A-Za-z0-9_]*\\)=.*/\\1/p\'); do\n unset "$uv_ambient"\ndone\n\n# The cache lives under the app\'s own support directory rather than the user\'s\n# shared `~/.cache/uv`, so the install neither reads nor pollutes a cache that\n# another tool (or another version of uv) is maintaining. Written here, after the\n# sweep, and handed to uv per invocation rather than exported.\n#\n# IT PERSISTS, and that is worth knowing on a user\'s disk: a full install leaves\n# ~118 MB there (measured; pip\'s own cache for the same dependency set is ~40 MB\n# and it also persists). Nothing else reads it today - the app\'s backend-update\n# path installs with pip - so it is there for the next provisioning or repair,\n# and it is what makes a retry converge in ~1.5 s instead of ~20 s.\nUV_CACHE_DIR="$APP_DATA_DIR/uv-cache"\n\n# Is the handed-down uv something we can actually run?\nuv_is_usable() {\n [ -n "$UV_BIN" ] && [ -x "$UV_BIN" ] && "$UV_BIN" --version >/dev/null 2>&1\n}\n\n# UV_NO_CONFIG: never read `pyproject.toml`/`uv.toml`, wherever they are.\n# UV_PYTHON_DOWNLOADS=never: this install uses the interpreter it was handed and\n# may never fetch another, which is also what keeps it working offline.\n# UV_CACHE_DIR: this install\'s own cache, passed explicitly for the same reason.\n# UV_SYSTEM_CERTS: trust the PLATFORM trust store, not only the root bundle uv\n# ships. It is OFF by default, which is the default this line changes, and the\n# flag\'s own name in the bundled uv is `--system-certs`.\n#\n# WHY THIS LINE EXISTS AT ALL. A network that inspects TLS - a corporate proxy,\n# an admin- or profile-installed root - carries its root in the platform store,\n# and uv does NOT read that store by default: it fails the handshake against\n# the roots compiled into it. This flag is the whole of the difference between\n# a machine that cannot install at all and one that installs normally.\n#\n# AND WHY THE PIP FALLBACK IS HANDED NO CA SETTING: pip 24.2 and newer read the\n# platform store by default, in addition to the Mozilla bundle they ship\n# (`truststore`, "always on since 24.2"), and every pip these installs produce\n# is newer than that - uv\'s own seed and the bundled interpreter\'s ensurepip\n# alike - so a root added to the store is trusted by BOTH clients and no\n# `PIP_CERT`/`SSL_CERT_FILE` is needed. The note this replaces claimed the\n# opposite ("neither client" would see the store; the fallback "resolves\n# against certifi"), which was true only of pip before 24.2; corrected rather\n# than deleted so it is not restored (review round 1, R1-2). The one shape it\n# does not cover is a dev checkout on a system python old enough to predate\n# truststore.\n#\n# IT CANNOT MAKE THINGS WORSE, which is why it is set unconditionally rather than\n# probed for: every uv call below is already followed by the pip fallback on a\n# non-zero exit, so the cost of a machine whose platform store cannot be read is\n# one failed uv attempt and the path that shipped before uv was bundled.\nuv_run() {\n UV_NO_CONFIG=1 UV_PYTHON_DOWNLOADS=never UV_SYSTEM_CERTS=1 \\\n UV_CACHE_DIR="$UV_CACHE_DIR" \\\n "$UV_BIN" "$@"\n}\n\n# Create virtual environment if it doesn\'t exist\nif [ ! -d "$VENV_PATH" ]; then\n echo "|LO1:environment"\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 # THE ENVIRONMENT: uv first, the interpreter\'s own venv module second.\n #\n # WHY uv. `python -m venv` seeds pip by running the interpreter\'s own\n # `ensurepip`, and that is the slow half of creating an environment. Measured on\n # this machine against the bundled interpreter, cold, one run each: `python -m\n # venv` 4.74 s against `uv venv --seed` 1.22 s. Those are this box\'s numbers\n # under fleet load, so read the saving as 3.5 s HERE and not as a constant; the\n # scoping report measured 5.1 s on a quieter machine. uv produces the same\n # environment: the same `pyvenv.cfg` home pointing at the external runtime, a\n # `bin/python` symlinked at the interpreter it was handed.\n #\n # WHY `--seed`, and this is the part the note that used to stand here got\n # wrong. The app\'s backend-update path runs `<venv>/bin/python -m pip install\n # --upgrade local-operator` inside this environment (`update-service.ts`,\n # `buildPipUpgradeCommand`), so the environment needs the pip MODULE. The old\n # note said `uv venv` "produces an environment with no pip at all" - true of\n # bare `uv venv`, false of the command below: `--seed` installs pip into the\n # environment (measured: `pip 26.2.1` in the seeded venv against `pip 25.0.1`\n # from the ensurepip wheel). The stale reason is deleted rather than left\n # standing, because a comment asserting a constraint that no longer holds is\n # how the next reader avoids a correct change.\n # Verified end to end before this change, not argued: the app\'s own command was\n # run inside one of these seeded environments and installed `local-operator\n # 0.63.6`, after which `<venv>/bin/local-operator --version` answered.\n #\n # WHY THE FALLBACK IS NOT OPTIONAL. `uv venv --seed` fetches the pip wheel, so\n # it needs the network - a phase earlier than the package install does. A uv\n # that cannot run, or that fails this one step, therefore falls through to the\n # interpreter\'s own `venv`, which is the path that shipped until now. The `rm`\n # on that path removes only the directory THIS attempt just made: the guard\n # above proved the path did not exist a moment ago.\n VENV_CREATED=false\n if uv_is_usable; then\n echo "Creating virtual environment with uv ($("$UV_BIN" --version 2>/dev/null || echo \'version unavailable\'))..."\n if uv_run venv --seed --python "$PYTHON_BIN" "$VENV_PATH"; then\n VENV_CREATED=true\n else\n UV_VENV_STATUS=$?\n echo "WARNING: the bundled uv could not create the environment (exit ${UV_VENV_STATUS}); retrying with the interpreter\'s own venv module."\n rm -rf "$VENV_PATH"\n fi\n fi\n\n if [ "$VENV_CREATED" != true ]; then\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 fi\n echo "Successfully created virtual environment"\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ] || [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Virtual environment is missing critical components"\n echo "Contents of virtual environment directory:"\n ls -la "$VENV_PATH"\n if [ -d "$VENV_PATH/bin" ]; then\n echo "Contents of bin directory:"\n ls -la "$VENV_PATH/bin"\n fi\n exit 1\nfi\necho "Virtual environment structure verified"\n\n# --- The package install: uv when there is one, pip otherwise ------------------\n#\n# WHY UV. Installing the backend is the phase a user waits through on a first\n# run, and pip spends it resolving and fetching serially. Measured on this\n# machine, cold cache, three runs each, the same interpreter and dependency set:\n# pip\'s package install is 33.0-40.7 s against uv\'s 12.8-16.1 s, plus the\n# 2.3-2.8 s `pip install --upgrade pip` the uv path skips. QA\'s independent pair\n# on a quieter box was 33.9 s against 22.8 s, so read the ratio as 1.5-2.8x\n# ACROSS those two operators, and the seconds as this box\'s. Warm, a retry or a\n# repair: 15.9-33.8 s against 0.65-1.57 s.\n#\n# THE FIGURES THIS COMMENT USED TO QUOTE ARE WITHDRAWN, and this note is here so\n# they are not restored: "pip 128.9 s against uv 14.8 s, 178.3 s against 4.5 s\n# warm". The pip reading was taken at load ~90 and did not reproduce at five\n# further attempts. A comment that argues from a withdrawn number is how the next\n# maintainer decides on a ratio five times the measured one - and the decision it\n# argues is whether ~16-20 MiB per artifact earns its place, so the number is\n# load-bearing. `docs/BUILD.md` carries the full set.\n#\n# WHY A FALLBACK RATHER THAN UV ALONE. uv is a NEW resource in the bundle, and\n# every artifact built before this change has none. The pip path below is the one\n# that shipped until now, unchanged, and it runs whenever uv is absent or cannot\n# do the job - a dev checkout whose `pnpm setup-python` was never run, an older\n# artifact, a uv the platform refuses to spawn, a uv install that failed. A\n# fallback that has never been exercised is a claim rather than a feature, which\n# is why the CI install-script jobs run this script with no uv at all.\n#\n# THE ENVIRONMENT ABOVE ALREADY HAS PIP IN IT, and that is load-bearing: the app\'s\n# backend-update path runs `<venv>/bin/python -m pip install --upgrade\n# local-operator` inside this same environment (`update-service.ts`,\n# `buildPipUpgradeCommand`). Both creation paths provide it - `uv venv --seed`\n# installs pip, `python -m venv` seeds it from `ensurepip` - and the structural\n# check above (`"$VENV_PATH/bin/pip"`) is what holds that on both.\n#\n# `UV_BIN`, the ambient-`UV_*` sweep, `UV_CACHE_DIR` and `uv_run` are defined\n# beside that creation, above, because uv builds the environment as well as\n# installing into it.\n\n# Activate virtual environment and install local-operator\necho "Installing local-operator in virtual environment..."\nsource "$VENV_PATH/bin/activate"\n\n# Check network connectivity to PyPI. A DIAGNOSTIC, not a gate: the install below\n# decides whether it can proceed.\n#\n# What this answers, stated precisely because an earlier version of this comment\n# claimed more than the flags buy: "did a TLS fetch to PyPI\'s JSON API complete,\n# and did the answer come back as JSON?". `--fail` turns an HTTP ERROR status\n# into a non-zero exit - a proxy\'s 403/407, any 4xx/5xx - and does NOT notice a\n# captive portal answering 200 with its own HTML page (measured: a portal-shaped\n# 200 returns exit 0 with AND without the flag). `-o /dev/null` cannot tell a\n# portal\'s page from PyPI\'s JSON either, so the content type is what\n# discriminates, and on a captive network a probe that reports "reachable" while\n# pip is about to fail is the false negative this warning exists to catch.\n#\n# What the content type does NOT prove, stated so this paragraph is not read as\n# more than it says: a proxy answering 200 with `application/json` and an error\n# body (`{"detail":"blocked by proxy policy"}`) is silent here, because the\n# answer did come back as JSON. Only parsing the body - a fetch of PyPI\'s own\n# payload shape - would tell those apart, and a diagnostic that costs a parse is\n# not what stands in front of an install.\n#\n# Two bounds, two jobs: `--connect-timeout 5` ends a black-hole network (a\n# connect that never completes), `--max-time 30` stops a connected-but-stalled\n# peer. Both must be POSITIVE: `--max-time 0` and `--connect-timeout 0` disable\n# the bound rather than making it immediate, which is why the test beside this\n# script requires `[1-9]`. 30 rather than 10 because the total must not fire on a\n# slow-but-working link: a working endpoint that answered in 15s tripped a 10s\n# total bound and printed this warning on an install that then succeeded, and a\n# warning that cries wolf is one users learn to ignore.\necho "Checking network connectivity to PyPI..."\nPYPI_PROBE_CONTENT_TYPE=$(curl -s --fail --connect-timeout 5 --max-time 30 -o /dev/null -w \'%{content_type}\' https://pypi.org/pypi/local-operator/json) || PYPI_PROBE_CONTENT_TYPE=""\nif [[ "$PYPI_PROBE_CONTENT_TYPE" != application/json* ]]; then\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n echo "Attempting to ping common domains to diagnose network issues:"\n ping -c 1 -W 2000 google.com || echo "Cannot ping google.com"\n ping -c 1 -W 2000 pypi.org || echo "Cannot ping pypi.org"\nfi\n\n# --- Progress markers -------------------------------------------------------\n# One whole line per phase, read by the app and shown in the setup window. The\n# app matches the ENTIRE line (`|LO1:<phase>`, see src/shared/install-progress.ts)\n# and never a substring, so a marker has to stand alone: do not wrap it in\n# other text, do not re-indent it into a longer sentence, and do not emit one\n# for work this script does not actually do. A missing marker leaves the window\n# on its previous step, which is the honest failure; a marker a log line also\n# happens to produce is a wrong step presented as a measurement.\n#\n# WHY THE SCRIPT AND NOT ONLY THE APP: the install below is minutes of work on a\n# cold machine, and the app cannot see inside the venv it is about to create -\n# this is the only process that knows when the environment exists and when the\n# download starts.\necho "|LO1:components"\nUV_INSTALLED=false\nif uv_is_usable; then\n echo "Installing local-operator with uv ($("$UV_BIN" --version 2>/dev/null || echo \'version unavailable\'))..."\n # No `pip install --upgrade pip` on this path: uv does not use pip, so the\n # upgrade would be a whole extra network round trip that changes nothing about\n # the result.\n if uv_run pip install --python "$VENV_PATH/bin/python" --upgrade local-operator; then\n UV_INSTALLED=true\n echo "local-operator installation with uv successful"\n else\n # WHY THE EXIT CODE IS PRINTED (review round 1, QA Q2): this fallback has to\n # be forgiving - an install must not fail because uv did - but "a bundled uv\n # is present and fails" is a defect rather than a degraded path, and this line\n # is the only place it shows up: the exit code is 0 and the UI is unchanged.\n # `uv_is_usable` passing and a uv install SUCCEEDING are two different facts.\n UV_STATUS=$?\n echo "WARNING: the bundled uv is present but its install failed (exit ${UV_STATUS}); retrying with pip, which is what this script used before uv was bundled."\n fi\nelse\n echo "Bundled uv not available (LOCAL_OPERATOR_UV_BIN=${UV_BIN:-unset}); installing with pip."\nfi\n\nif [ "$UV_INSTALLED" != true ]; then\n # NO `pip install --upgrade pip` AND NO `--verbose` (first-run onboarding,\n # Q4/Q13). The self-upgrade is a whole extra resolve-and-download (2.3-2.8 s\n # measured, see the uv note above) that changes nothing about the result: the\n # environment\'s pip is already 24.2+ on both creation paths, which is the\n # version this script\'s TLS note depends on. `--verbose` multiplied the output\n # the app parses line by line and the log a user attaches, and none of it was\n # read: the failure branch below prints its own diagnosis.\n echo "Installing local-operator package..."\n python -m pip install --upgrade local-operator || {\n echo "ERROR: Failed to install local-operator package. Exit code: $?"\n echo "Python version:"\n python --version\n echo "pip version:"\n pip --version\n echo "Available pip packages:"\n pip list\n echo "Pip config:"\n pip config list\n echo "Network diagnosis:"\n curl -sI --fail --connect-timeout 5 --max-time 30 https://pypi.org || echo "Cannot reach PyPI server"\n exit 1\n }\nfi\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n echo "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator"\n file "$VENV_PATH/bin/local-operator"\n \n # Try to run the version command with full error output\n echo "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n echo "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n echo "Binary details:"\n file "$VENV_PATH/bin/local-operator"\n echo "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator"\n echo "Dependencies:"\n if command -v otool >/dev/null; then\n otool -L "$VENV_PATH/bin/local-operator" || echo "Could not get dependencies with otool"\n fi\n exit 1\n }\nelse\n echo "Error: Failed to install Local Operator backend. Binary not found."\n echo "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/"\n exit 1\nfi\n\necho "$(date): Installation completed successfully."\n';
|
|
12778
|
+
const windowsInstallScriptRaw = '# Local Operator Backend Installation Script for Windows\n# This script finds or installs Python 3.12+ inside the app\'s own folder and sets up a virtual environment for the Local Operator backend.\n\n# Configuration\n$AppName = "Local Operator"\n$VenvName = "local-operator-venv"\n$AppDataDir = "$env:APPDATA\\\\$AppName"\n$VenvPath = "$AppDataDir\\\\$VenvName"\n$LogFile = "$AppDataDir\\\\backend-install-shell.log"\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. The script itself names no download URL at all now:\n# the one third-party fetch it used to make, pyenv-win\'s unpinned source archive,\n# is gone (see "Which Python the environment is built on" below), and\n# `scripts/install-scripts-network.test.mjs` keeps it that way.\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# --- Which Python the environment is built on ---------------------------------\n#\n# WHAT THIS REPLACED (first-run onboarding, Q4). The script used to download\n# pyenv-win\'s `master.zip` - an UNPINNED branch head, so every install ran\n# whatever that repository\'s default branch said that day - install Python\n# 3.12.0 through it, and persist User-scope `PYENV`/`PYENV_HOME` plus two `PATH`\n# prepends on the user\'s account: changes to the machine outside this app\'s own\n# folder, before the step most likely to fail. It also ignored `PYTHON_BIN`, the\n# interpreter the app resolves and hands every platform\'s script. It cost ~77 s\n# of the Windows CI run before the package install started.\n#\n# THE ORDER NOW, first usable wins, and every arm stays inside this app\'s folder:\n#\n# 1. `PYTHON_BIN` - the app\'s own answer (`findPython` in backend-installer.ts),\n# honoured exactly as the macOS and Linux scripts honour it. On an existing\n# install that is the pyenv-win 3.12.0 it already has, so an upgrade does not\n# move an environment it does not need to.\n# 2. The bundled uv\'s own managed Python, at the version the app pins\n# (`LOCAL_OPERATOR_PYTHON_VERSION`, from `bundled-runtime-layout.json` - the\n# same definition the macOS seed is built from, so the two platforms run the\n# same interpreter). uv verifies the download against checksums compiled into\n# the pinned uv, installs it under `$AppDataDir\\python`, and `--no-bin`\n# `--no-registry` keep it off PATH and out of the registry: nothing outside\n# the folder changes.\n# 3. A Python 3.12+ already on this process\'s PATH (`py -3`, then `python`) - the\n# shape of a dev checkout or an artifact built before uv was bundled.\n#\n# None of them writes to the user\'s environment variables.\n# THE VERSION IS NOT SPELLED HERE (code review round 1, R5). It used to fall\n# back to a literal "3.14.7", which is a second copy of\n# `src/shared/bundled-runtime-layout.json`\'s `python.version` - exactly the\n# duplication this file\'s own note above forbids, and nothing would have caught\n# it drifting. The app always sets the variable; a hand-run that does not gets a\n# loud sentence naming what to set, inside the branch that actually needs it (a\n# machine whose PYTHON_BIN or PATH Python answers never reads it at all).\n$PythonVersion = $env:LOCAL_OPERATOR_PYTHON_VERSION\n$ManagedPythonDir = "$AppDataDir\\python"\n\n# Is this a Python 3.12+ executable we can run? The floor is local-operator\'s own\n# `requires-python`; an older interpreter fails the install much later with\n# pip\'s "from versions: none", which is the least helpful place to learn it.\nfunction Test-PythonUsable {\n param ($Candidate)\n if (-not $Candidate) { return $false }\n try {\n & $Candidate -c "import sys; sys.exit(0 if sys.version_info >= (3, 12) else 1)" 2>$null | Out-Null\n return ($LASTEXITCODE -eq 0)\n } catch {\n return $false\n }\n}\n\n# THE `python` STAGE IS ANNOUNCED HERE, ON THIS PLATFORM TOO (code review round 1,\n# R1). macOS gets it from `managed-python.ts` when it copies the managed runtime;\n# nothing ever announced it on win32, so the panel painted with no step, no clock\n# and no estimate through the whole CPython download and the first line a Windows\n# user saw was "Step 2 of 4" - while the rail still showed four rows and the\n# win32 baseline this PR measured was dead input.\n#\n# IT BRACKETS THE STAGE, NOT ONLY ITS uv BRANCH: this is where the interpreter is\n# found, and fetched only if it is not already here. Announcing inside\n# provisioning alone would leave a machine whose PYTHON_BIN or PATH Python\n# answers without any phase 1 at all, which is the same hole one branch over. The\n# phase\'s own words are "Getting ready / Finding the copy of Python Local\n# Operator runs on", so finding is what it means - and `$PythonVersion` above is\n# read inside the branch that needs it, never to decide this marker.\nWrite-Output "|LO1:python"\n$PythonExe = $null\nif ($env:PYTHON_BIN -and (Test-Path $env:PYTHON_BIN) -and (Test-PythonUsable $env:PYTHON_BIN)) {\n $PythonExe = $env:PYTHON_BIN\n Write-Output "Using Python provided by the app: $PythonExe"\n} elseif ($env:PYTHON_BIN) {\n Write-Output "WARNING: PYTHON_BIN ($env:PYTHON_BIN) is not a runnable Python 3.12+; looking for another."\n}\n\n# --- The installer this script prefers: the app\'s own bundled uv ---------------\n#\n# Same shape as the macOS and Linux scripts, and for the same reasons: uv resolves\n# and fetches in parallel - measured on macOS, cold cache, three runs each, same\n# interpreter: uv\'s package install is 12.8-16.1 s against pip\'s 33.0-40.7 s,\n# plus the 2.3-2.8 s pip self-upgrade this path skips, so 1.5-2.8x across two\n# operators rather than the "14.8 s against 128.9 s" quoted here before that\n# reading was withdrawn (`docs/BUILD.md` has the full set). The pip path below is\n# unchanged and runs whenever uv is absent or cannot do the job, and pip STAYS in\n# the venv because the app\'s backend-update path runs `pip install --upgrade\n# local-operator` inside this same environment - which is why the environment must\n# keep pip, and NOT a reason to avoid `uv venv`: bare `uv venv` leaves no pip,\n# `uv venv --seed` does, and `--seed` is what the creation below passes.\n#\n# RESOLVED ABOVE THE ENVIRONMENT, because uv now provisions the interpreter and\n# builds the environment too (first-run onboarding, Q4/Q13).\n#\n# Nothing here searches PATH for a uv: an installed uv is a version and a\n# configuration nobody in this repository chose. `LOCAL_OPERATOR_UV_BIN` is the\n# app\'s own answer (`src/main/backend/uv-tool.ts`).\n$UvBin = $env:LOCAL_OPERATOR_UV_BIN\n$UvCacheDir = "$AppDataDir\\uv-cache"\n\nfunction Test-UvUsable {\n if (-not $UvBin) { return $false }\n if (-not (Test-Path $UvBin)) { return $false }\n try {\n & $UvBin --version | Out-Null\n return ($LASTEXITCODE -eq 0)\n } catch {\n return $false\n }\n}\n\n# Drop every UV_* variable the launching environment carried, then set the four\n# settings this install depends on. Measured on uv 0.12.17: a user-level\n# `uv.toml` naming an unreachable index is obeyed by `uv pip install` and ignored\n# with `UV_NO_CONFIG=1`; an ambient `UV_INDEX_URL` changes where packages come\n# from, while `PIP_INDEX_URL` does not affect uv at all. A name list would drift\n# the day uv adds a variable - the namespace cannot.\n#\n# The names are MATERIALISED first (`@(...)` over a property projection):\n# removing entries of a collection that is still being enumerated is the shape\n# that throws `Collection was modified`.\n$uvAmbientNames = @(\n Get-ChildItem env: |\n Where-Object { $_.Name -like \'UV_*\' } |\n Select-Object -ExpandProperty Name\n)\nforeach ($uvAmbientName in $uvAmbientNames) {\n Remove-Item "env:$uvAmbientName" -ErrorAction SilentlyContinue\n}\n\n# UV_NO_CONFIG: never read `pyproject.toml`/`uv.toml`, wherever they are.\n# UV_PYTHON_DOWNLOADS=never: this install uses the interpreter it was handed and\n# never fetches another.\n# UV_CACHE_DIR: under the app\'s own support directory rather than the user\'s\n# shared uv cache.\n#\n# UV_SYSTEM_CERTS: trust the PLATFORM trust store (the Windows certificate\n# stores), not only the root bundle uv ships - off by default, which is the\n# default this line changes; `--system-certs` is the same setting spelled as a\n# flag in the bundled uv 0.12.17.\n#\n# WHY: on a network that inspects TLS the root is installed in the platform\n# store, and uv does not read that store by default - it fails the handshake\n# against the roots compiled into it. uv names the remedy itself when it fails:\n# "Consider enabling use of system TLS certificates with the `--system-certs`\n# command-line flag". Nothing here passed it.\n#\n# AND WHY THE PIP FALLBACK IS HANDED NO CA SETTING: pip 24.2 and newer read the\n# platform store by default, in addition to the Mozilla bundle they ship\n# (`truststore`, "always on since 24.2"), and every pip these installs produce\n# is newer than that - uv\'s own seed and the bundled interpreter\'s ensurepip\n# alike - so a root added to the Windows certificate stores is trusted by BOTH\n# clients and no `PIP_CERT`/`SSL_CERT_FILE` is needed. The note this replaces\n# claimed the opposite (the fallback "resolves against certifi"), which was\n# true only of pip before 24.2; corrected rather than deleted so it is not\n# restored (review round 1, R1-2). The one shape it does not cover is a dev\n# checkout on a system python old enough to predate truststore.\n#\n# IT CANNOT MAKE THINGS WORSE: every uv call below is already followed by the pip\n# fallback on a non-zero exit, so a platform store that cannot be read costs one\n# failed uv attempt and then the path that shipped before uv was bundled.\n$env:UV_NO_CONFIG = "1"\n$env:UV_PYTHON_DOWNLOADS = "never"\n$env:UV_CACHE_DIR = $UvCacheDir\n$env:UV_SYSTEM_CERTS = "1"\n\nif (-not $PythonExe -and (Test-UvUsable)) {\n if (-not $PythonVersion) {\n Write-Error "ERROR: no Python version was handed down (LOCAL_OPERATOR_PYTHON_VERSION is unset), so the bundled uv has nothing to install. Launch setup from the app, or set that variable to the version src/shared/bundled-runtime-layout.json pins."\n exit 1\n }\n Write-Output "Installing Python $PythonVersion with uv..."\n # `UV_PYTHON_DOWNLOADS=manual` for THIS call alone: the global `never` above is\n # what keeps every later uv call on the interpreter it was handed, and an\n # explicit `python install` is the one place a download is the point.\n $env:UV_PYTHON_DOWNLOADS = "manual"\n & $UvBin python install $PythonVersion --install-dir $ManagedPythonDir --no-bin --no-registry\n $UvPythonStatus = $LASTEXITCODE\n $env:UV_PYTHON_DOWNLOADS = "never"\n if ($UvPythonStatus -eq 0) {\n $env:UV_PYTHON_INSTALL_DIR = $ManagedPythonDir\n $Found = (& $UvBin python find --managed-python $PythonVersion 2>$null | Select-Object -First 1)\n Remove-Item env:UV_PYTHON_INSTALL_DIR -ErrorAction SilentlyContinue\n if ($Found -and (Test-PythonUsable $Found)) {\n $PythonExe = $Found.Trim()\n Write-Output "Using Python installed by uv: $PythonExe"\n }\n }\n if (-not $PythonExe) {\n Write-Output "WARNING: the bundled uv could not install Python $PythonVersion (exit $UvPythonStatus); looking for one on PATH."\n }\n}\n\nif (-not $PythonExe) {\n foreach ($Candidate in @("py", "python")) {\n if (-not (Test-CommandExists $Candidate)) { continue }\n if ($Candidate -eq "py") {\n $Resolved = (& py -3 -c "import sys; print(sys.executable)" 2>$null | Select-Object -First 1)\n } else {\n $Resolved = (& python -c "import sys; print(sys.executable)" 2>$null | Select-Object -First 1)\n }\n if ($Resolved -and (Test-PythonUsable $Resolved.Trim())) {\n $PythonExe = $Resolved.Trim()\n Write-Output "Using Python found on PATH: $PythonExe"\n break\n }\n }\n}\n\nif (-not $PythonExe) {\n Write-Error "ERROR: No Python 3.12+ is available: the app handed none, the bundled uv could not install one, and none is on PATH. Install Python 3.12 or newer from https://www.python.org/downloads/ and try again."\n exit 1\n}\n\n# Create virtual environment if it doesn\'t exist\nif (-not (Test-Path $VenvPath)) {\n Write-Output "|LO1:environment"\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 # uv first, `--seed` so the environment keeps pip (the app\'s backend-update\n # path runs `pip install --upgrade local-operator` inside it); the venv\n # module is the fallback, as on macOS and Linux. Removing a failed attempt is\n # safe: the guard above proved the path absent a moment ago.\n $VenvCreated = $false\n if (Test-UvUsable) {\n & $UvBin venv --seed --python $PythonExe $VenvPath\n if ($LASTEXITCODE -eq 0) {\n $VenvCreated = $true\n } else {\n Write-Output "WARNING: the bundled uv could not create the environment (exit $LASTEXITCODE); retrying with the interpreter\'s own venv module."\n Remove-Item -Path $VenvPath -Recurse -Force -ErrorAction SilentlyContinue\n }\n }\n if (-not $VenvCreated) {\n & $PythonExe -m venv $VenvPath\n if ($LASTEXITCODE -ne 0) {\n Write-Error "Failed to create virtual environment"\n exit 1\n }\n }\n}\n\n# Verify the virtual environment was created\nif (-not (Test-Path "$VenvPath\\\\Scripts\\\\Activate.ps1")) {\n Write-Error "Virtual environment activation script not found at $VenvPath\\\\Scripts\\\\Activate.ps1"\n exit 1\n}\n\n# --- The package install: uv when there is one, pip otherwise ------------------\n\n# Activate virtual environment and install local-operator\n# --- Progress markers -------------------------------------------------------\n# One whole line per phase, read by the app and shown in the setup window. The\n# app matches the ENTIRE line (`|LO1:<phase>`, see src/shared/install-progress.ts)\n# and never a substring, so a marker has to stand alone: do not wrap it in\n# other text, do not re-indent it into a longer sentence, and do not emit one\n# for work this script does not actually do. A missing marker leaves the window\n# on its previous step, which is the honest failure; a marker a log line also\n# happens to produce is a wrong step presented as a measurement.\n#\n# WHY THE SCRIPT AND NOT ONLY THE APP: the install below is minutes of work on a\n# cold machine, and the app cannot see inside the venv it is about to create -\n# this is the only process that knows when the environment exists and when the\n# download starts.\nWrite-Output "|LO1:components"\nWrite-Output "Installing local-operator in virtual environment..."\n# Use PowerShell to run the activation script\n& "$VenvPath\\\\Scripts\\\\Activate.ps1"\n\n$UvInstalled = $false\nif (Test-UvUsable) {\n Write-Output "Installing local-operator with uv..."\n # No `pip install --upgrade pip` on this path: uv does not use pip.\n & $UvBin pip install --python "$VenvPath\\Scripts\\python.exe" --upgrade local-operator\n if ($LASTEXITCODE -eq 0) {\n $UvInstalled = $true\n Write-Output "local-operator installation with uv successful"\n } else {\n # The exit code is printed for the same reason as on macOS and Linux: the\n # fallback is deliberately forgiving, so this line is the only evidence\n # that a bundled uv is present and failing for every user (QA Q2).\n Write-Output "WARNING: the bundled uv is present but its install failed (exit $LASTEXITCODE); retrying with pip, which is what this script used before uv was bundled."\n }\n} else {\n if ($UvBin) {\n Write-Output "Bundled uv at $UvBin could not be run on this machine; installing with pip."\n } else {\n Write-Output "Bundled uv not available (LOCAL_OPERATOR_UV_BIN unset); installing with pip."\n }\n}\n\nif (-not $UvInstalled) {\n # No pip self-upgrade (first-run onboarding, Q13): an extra resolve and\n # download that changes nothing, since the environment\'s pip is already\n # 24.2+ on both creation paths. The venv\'s own interpreter by path, not\n # `python` off PATH: activation in a child `&` call is not guaranteed to have\n # put the venv first, and a PATH python would install into the wrong place.\n & "$VenvPath\\Scripts\\python.exe" -m pip install --upgrade local-operator\n\n if ($LASTEXITCODE -ne 0) {\n Write-Error "Failed to install packages in virtual environment"\n exit 1\n }\n}\n\n# Verify installation\n$LocalOperatorInstalled = $false\ntry {\n # First try to run local-operator directly (it should be in PATH from the activated venv)\n $Version = & local-operator --version\n $LocalOperatorInstalled = $true\n Write-Output "Local Operator backend installed successfully!"\n Write-Output $Version\n} catch {\n Write-Output "Could not run local-operator directly, trying with full path..."\n try {\n # Try with explicit path\n $LocalOperatorExe = "$VenvPath\\\\Scripts\\\\local-operator.exe"\n if (Test-Path $LocalOperatorExe) {\n $Version = & $LocalOperatorExe --version\n $LocalOperatorInstalled = $true\n Write-Output "Local Operator backend installed successfully at $LocalOperatorExe!"\n Write-Output $Version\n } else {\n # Try without .exe extension\n $LocalOperatorCmd = "$VenvPath\\\\Scripts\\\\local-operator"\n if (Test-Path $LocalOperatorCmd) {\n $Version = & $LocalOperatorCmd --version\n $LocalOperatorInstalled = $true\n Write-Output "Local Operator backend installed successfully at $LocalOperatorCmd!"\n Write-Output $Version\n } else {\n Write-Error "Error: local-operator executable not found in expected locations."\n \n # Create a symlink to make it more accessible\n Write-Output "Attempting to create a symlink for local-operator..."\n $PythonModule = "$VenvPath\\\\Lib\\\\site-packages\\\\local_operator"\n if (Test-Path $PythonModule) {\n Write-Output "Found local_operator module at $PythonModule"\n \n # Create a batch file that runs the module\n $BatchContent = "@echo off`npython -m local_operator %*"\n Set-Content -Path "$VenvPath\\\\Scripts\\\\local-operator.bat" -Value $BatchContent\n \n # Test the batch file\n if (Test-Path "$VenvPath\\\\Scripts\\\\local-operator.bat") {\n Write-Output "Created local-operator.bat in Scripts directory"\n $LocalOperatorInstalled = $true\n } else {\n Write-Error "Failed to create local-operator.bat"\n exit 1\n }\n } else {\n Write-Error "local_operator module not found in site-packages"\n exit 1\n }\n }\n }\n } catch {\n Write-Error "Error: Failed to install Local Operator backend."\n Write-Error $_.Exception.Message\n exit 1\n }\n}\n\n# If we got here, installation was successful\nif ($LocalOperatorInstalled) {\n Write-Output "Local Operator backend installation verified."\n} else {\n Write-Error "Error: Failed to verify Local Operator backend installation."\n exit 1\n}\n\nWrite-Output "$(Get-Date): Installation completed successfully."\n\n# Properly close the transcript and ensure it\'s released\ntry {\n Write-Output "Finalizing installation and releasing resources..."\n # Flush any pending output\n [System.Console]::Out.Flush()\n # Stop transcript properly\n Stop-Transcript\n \n # Add a small delay to ensure file handles are released\n Start-Sleep -Seconds 1\n \n # Explicitly release any COM objects\n [System.GC]::Collect()\n [System.GC]::WaitForPendingFinalizers()\n \n Write-Output "Installation completed and resources released."\n} catch {\n Write-Error "Error during cleanup: $_"\n}\n';
|
|
12750
12779
|
const macosInstallScript = macosInstallScriptRaw;
|
|
12751
12780
|
const linuxInstallScript = linuxInstallScriptRaw;
|
|
12752
12781
|
const windowsInstallScript = windowsInstallScriptRaw;
|
|
@@ -13029,6 +13058,18 @@ class BackendInstaller {
|
|
|
13029
13058
|
*/
|
|
13030
13059
|
lastPhase = null;
|
|
13031
13060
|
failureOnScreen = false;
|
|
13061
|
+
/*
|
|
13062
|
+
* The clock and the sub-progress the window's step line is computed from
|
|
13063
|
+
* (first-run onboarding, U8/Q5/D9): when this attempt started, when the
|
|
13064
|
+
* current phase was announced, and what the install child has said inside
|
|
13065
|
+
* it. Main-process instants rather than the renderer's, because the window
|
|
13066
|
+
* mounts AFTER the first phase on macOS and would otherwise time the run from
|
|
13067
|
+
* its own first paint. Reset per attempt (`startAttemptClock`), so a Retry's
|
|
13068
|
+
* elapsed starts at zero rather than counting the failed run.
|
|
13069
|
+
*/
|
|
13070
|
+
attemptStartedAt = Date.now();
|
|
13071
|
+
phaseStartedAt = Date.now();
|
|
13072
|
+
subProgress = null;
|
|
13032
13073
|
/**
|
|
13033
13074
|
* Record a phase announced by the preparation layer, and pass it on when there is
|
|
13034
13075
|
* a window to paint it in.
|
|
@@ -13043,7 +13084,41 @@ class BackendInstaller {
|
|
|
13043
13084
|
rememberInstallPhase(phase) {
|
|
13044
13085
|
if (phase === this.lastPhase) return;
|
|
13045
13086
|
this.lastPhase = phase;
|
|
13046
|
-
this.
|
|
13087
|
+
this.phaseStartedAt = Date.now();
|
|
13088
|
+
this.subProgress = null;
|
|
13089
|
+
this.sendPhase();
|
|
13090
|
+
}
|
|
13091
|
+
/** The current phase with its timing, the one shape every phase send takes. */
|
|
13092
|
+
sendPhase(phase = this.lastPhase) {
|
|
13093
|
+
this.sendInstallProgress({
|
|
13094
|
+
kind: "phase",
|
|
13095
|
+
phase,
|
|
13096
|
+
startedAt: this.attemptStartedAt,
|
|
13097
|
+
phaseStartedAt: this.phaseStartedAt,
|
|
13098
|
+
platform: installPlatform(process.platform),
|
|
13099
|
+
sub: this.subProgress
|
|
13100
|
+
});
|
|
13101
|
+
}
|
|
13102
|
+
/** A new attempt: the first run, or a Retry from the failure state. */
|
|
13103
|
+
startAttemptClock() {
|
|
13104
|
+
this.attemptStartedAt = Date.now();
|
|
13105
|
+
this.phaseStartedAt = this.attemptStartedAt;
|
|
13106
|
+
this.subProgress = null;
|
|
13107
|
+
}
|
|
13108
|
+
/**
|
|
13109
|
+
* Fold the install child's output into the `components` sub-progress, and
|
|
13110
|
+
* send only when a line actually moved it. Read from BOTH streams: uv writes
|
|
13111
|
+
* its `Resolved`/`Downloading` narration to stderr, pip writes `Collecting` to
|
|
13112
|
+
* stdout. Outside `components` nothing is counted - a `Resolved` line from
|
|
13113
|
+
* `uv venv --seed` is the seed, not the package install.
|
|
13114
|
+
*/
|
|
13115
|
+
foldSubProgress(lines) {
|
|
13116
|
+
if (this.lastPhase !== "components") return;
|
|
13117
|
+
let next = this.subProgress ?? EMPTY_SUB_PROGRESS;
|
|
13118
|
+
for (const line of lines) next = foldInstallLine(next, line);
|
|
13119
|
+
if (next === (this.subProgress ?? EMPTY_SUB_PROGRESS)) return;
|
|
13120
|
+
this.subProgress = next;
|
|
13121
|
+
this.sendPhase();
|
|
13047
13122
|
}
|
|
13048
13123
|
/**
|
|
13049
13124
|
* Whether the setup window is up and can be told things.
|
|
@@ -13293,6 +13368,7 @@ ${error instanceof Error ? error.message : String(error ?? "")}`;
|
|
|
13293
13368
|
* in-window failure state.
|
|
13294
13369
|
*/
|
|
13295
13370
|
async install(show) {
|
|
13371
|
+
this.startAttemptClock();
|
|
13296
13372
|
try {
|
|
13297
13373
|
for (; ; ) {
|
|
13298
13374
|
this.runActive = true;
|
|
@@ -13650,7 +13726,7 @@ ${result.stdout}`,
|
|
|
13650
13726
|
const listeners = [];
|
|
13651
13727
|
const replayHandler = () => {
|
|
13652
13728
|
if (this.lastPhase === null) return;
|
|
13653
|
-
this.
|
|
13729
|
+
this.sendPhase();
|
|
13654
13730
|
};
|
|
13655
13731
|
const retryHandler = () => {
|
|
13656
13732
|
if (!this.decisionPending || this.attemptInFlight) return;
|
|
@@ -13659,10 +13735,8 @@ ${result.stdout}`,
|
|
|
13659
13735
|
LogFileType.INSTALLER
|
|
13660
13736
|
);
|
|
13661
13737
|
this.failureOnScreen = false;
|
|
13662
|
-
this.
|
|
13663
|
-
|
|
13664
|
-
phase: this.lastPhase ?? "python"
|
|
13665
|
-
});
|
|
13738
|
+
this.startAttemptClock();
|
|
13739
|
+
this.sendPhase(this.lastPhase ?? "python");
|
|
13666
13740
|
this.retryFromWindow();
|
|
13667
13741
|
};
|
|
13668
13742
|
const cancelHandler = () => {
|
|
@@ -13844,6 +13918,7 @@ ${result.stdout}`,
|
|
|
13844
13918
|
let captured = "";
|
|
13845
13919
|
let capturedErrors = "";
|
|
13846
13920
|
let markerCarry = "";
|
|
13921
|
+
let subCarry = "";
|
|
13847
13922
|
const settle2 = (ok, exitCode) => {
|
|
13848
13923
|
if (settled) return;
|
|
13849
13924
|
settled = true;
|
|
@@ -13856,6 +13931,7 @@ ${result.stdout}`,
|
|
|
13856
13931
|
);
|
|
13857
13932
|
env.ELECTRON_RESOURCE_PATH = this.resourcesPath;
|
|
13858
13933
|
env[VENV_PATH_ENV] = venvPath;
|
|
13934
|
+
env.LOCAL_OPERATOR_PYTHON_VERSION = RUNTIME_LAYOUT.python.version;
|
|
13859
13935
|
env.HOME = electron.app.getPath("home");
|
|
13860
13936
|
env.LOCAL_OPERATOR_SUPPORT_PATH = process.platform === "darwin" ? this.managedOptions().support : this.appDataPath;
|
|
13861
13937
|
logger.info(
|
|
@@ -13903,6 +13979,7 @@ ${result.stdout}`,
|
|
|
13903
13979
|
const phase = parseInstallMarker(line);
|
|
13904
13980
|
if (phase !== null) this.rememberInstallPhase(phase);
|
|
13905
13981
|
}
|
|
13982
|
+
this.foldSubProgress(split.lines);
|
|
13906
13983
|
console.log(`Installation stdout: ${output}`);
|
|
13907
13984
|
logger.info(
|
|
13908
13985
|
`Installation stdout: ${output}`,
|
|
@@ -13914,6 +13991,9 @@ ${result.stdout}`,
|
|
|
13914
13991
|
this.installProcess.stderr.on("data", (data) => {
|
|
13915
13992
|
const output = data.toString();
|
|
13916
13993
|
capturedErrors += output;
|
|
13994
|
+
const split = splitLines(subCarry, output);
|
|
13995
|
+
subCarry = split.carry;
|
|
13996
|
+
this.foldSubProgress(split.lines);
|
|
13917
13997
|
console.error(`Installation stderr: ${output}`);
|
|
13918
13998
|
logger.error(
|
|
13919
13999
|
`Installation stderr: ${output}`,
|