local-operator-ui 0.19.0 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/out/main/index.js +414 -30
  2. package/out/renderer/assets/{_basePickBy-MfDCo2Uh.js → _basePickBy-ykG0NPFf.js} +1 -1
  3. package/out/renderer/assets/{_baseUniq-Cpz-frgt.js → _baseUniq-zF370IN-.js} +1 -1
  4. package/out/renderer/assets/{agent-details-page-BvHXXiUb.js → agent-details-page-CFeQgmpk.js} +1 -1
  5. package/out/renderer/assets/{agent-hub-page-XWqph1fJ.js → agent-hub-page-WTDOpgeF.js} +1 -1
  6. package/out/renderer/assets/{agents-page-BQKGRvX9.js → agents-page-BcpOVq5W.js} +2 -2
  7. package/out/renderer/assets/{architectureDiagram-IEHRJDOE-CcAlBJ35.js → architectureDiagram-IEHRJDOE-BgE-rKS0.js} +1 -1
  8. package/out/renderer/assets/{blockDiagram-JOT3LUYC-B0OCfiqo.js → blockDiagram-JOT3LUYC-fcZbaNLm.js} +1 -1
  9. package/out/renderer/assets/{c4Diagram-VJAJSXHY-Doz1qDn4.js → c4Diagram-VJAJSXHY-DECLed3f.js} +1 -1
  10. package/out/renderer/assets/channel-CsUT-bgQ.js +1 -0
  11. package/out/renderer/assets/{chunk-4BMEZGHF-CU2CNrGP.js → chunk-4BMEZGHF-CCYSb0Fh.js} +1 -1
  12. package/out/renderer/assets/{chunk-A2AXSNBT-CAMatFhx.js → chunk-A2AXSNBT-CjRtRikA.js} +1 -1
  13. package/out/renderer/assets/{chunk-AEK57VVT-BZ-bO6ec.js → chunk-AEK57VVT-eVeRvEm8.js} +1 -1
  14. package/out/renderer/assets/{chunk-D6G4REZN-CxhbOyZR.js → chunk-D6G4REZN-BN7tXfjV.js} +1 -1
  15. package/out/renderer/assets/{chunk-RZ5BOZE2-CWa1WtXF.js → chunk-RZ5BOZE2-hZ_ZoDPs.js} +1 -1
  16. package/out/renderer/assets/{chunk-XZIHB7SX-CG0Minxk.js → chunk-XZIHB7SX-LDikEfMz.js} +1 -1
  17. package/out/renderer/assets/{classDiagram-GIVACNV2-QRAx18rR.js → classDiagram-GIVACNV2-8XRUpU40.js} +1 -1
  18. package/out/renderer/assets/{classDiagram-v2-COTLJTTW-QRAx18rR.js → classDiagram-v2-COTLJTTW-8XRUpU40.js} +1 -1
  19. package/out/renderer/assets/clone-BTzCuJ8z.js +1 -0
  20. package/out/renderer/assets/{compact-pagination-DID-3WmW.js → compact-pagination-DqiXMGmM.js} +1 -1
  21. package/out/renderer/assets/{dagre-OKDRZEBW-DqJ5M5Av.js → dagre-OKDRZEBW-f4DSNbNt.js} +1 -1
  22. package/out/renderer/assets/{diagram-SSKATNLV-C3xzoBqy.js → diagram-SSKATNLV-DFy9Kjn5.js} +1 -1
  23. package/out/renderer/assets/{diagram-VNBRO52H-DwzP1mk9.js → diagram-VNBRO52H-DxfeD-Xq.js} +1 -1
  24. package/out/renderer/assets/{erDiagram-Q7BY3M3F-CAYEsR0C.js → erDiagram-Q7BY3M3F-BKIiiDiS.js} +1 -1
  25. package/out/renderer/assets/{flowDiagram-4HSFHLVR-Br6uHcfK.js → flowDiagram-4HSFHLVR-bPQ65Vad.js} +1 -1
  26. package/out/renderer/assets/{ganttDiagram-APWFNJXF-C85JWXKh.js → ganttDiagram-APWFNJXF-B4QAAaiA.js} +1 -1
  27. package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-Fj8dCS1a.js → gitGraphDiagram-7IBYFJ6S-CKw3yJdA.js} +1 -1
  28. package/out/renderer/assets/{graph-crAb0JBI.js → graph-BQE_YPzn.js} +1 -1
  29. package/out/renderer/assets/{index-DgfB0f4a.js → index-Byb6o6Kr.js} +282 -282
  30. package/out/renderer/assets/{index-CZfhJNkc.js → index-Cv1VxdCo.js} +1 -1
  31. package/out/renderer/assets/{index-DQsZQ882.js → index-lnppTSlu.js} +1 -1
  32. package/out/renderer/assets/{infoDiagram-PH2N3AL5-BzOoP2K2.js → infoDiagram-PH2N3AL5-dZAW4pJY.js} +1 -1
  33. package/out/renderer/assets/{journeyDiagram-U35MCT3I-rzeK8RKJ.js → journeyDiagram-U35MCT3I-Bfaldm3O.js} +1 -1
  34. package/out/renderer/assets/{kanban-definition-NDS4AKOZ-ngZfGkvv.js → kanban-definition-NDS4AKOZ-BihrMyrx.js} +1 -1
  35. package/out/renderer/assets/{layout-DoujDOcd.js → layout-BY0TgmPG.js} +1 -1
  36. package/out/renderer/assets/{legacy-agents-page-UwDNPEqi.js → legacy-agents-page-nTvYG2R1.js} +1 -1
  37. package/out/renderer/assets/{mermaid.core-CEj-gugH.js → mermaid.core-C_9cdmTh.js} +5 -5
  38. package/out/renderer/assets/{mindmap-definition-ALO5MXBD-qkNEqED3.js → mindmap-definition-ALO5MXBD-DGcjfOea.js} +1 -1
  39. package/out/renderer/assets/{parseISO-DFQYXDmJ.js → parseISO-Nf075j_I.js} +1 -1
  40. package/out/renderer/assets/{pieDiagram-IB7DONF6-BpHYrnjZ.js → pieDiagram-IB7DONF6-CLQQsy4S.js} +1 -1
  41. package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-jf2EIRrp.js → quadrantDiagram-7GDLP6J5-BW18K7e-.js} +1 -1
  42. package/out/renderer/assets/{radar-MK3ICKWK-B_Zwcolm.js → radar-MK3ICKWK-D5qSNCES.js} +1 -1
  43. package/out/renderer/assets/{radient-auth-buttons-BzSb8qfY.js → radient-auth-buttons-C68jw6Rq.js} +1 -1
  44. package/out/renderer/assets/{requirementDiagram-KVF5MWMF-CDT8zCqY.js → requirementDiagram-KVF5MWMF-BGO78UpF.js} +1 -1
  45. package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-CDyX6T2C.js → sankeyDiagram-QLVOVGJD-D10GCyqA.js} +1 -1
  46. package/out/renderer/assets/{schedules-page-BeC2UhOv.js → schedules-page-BhoAmrxA.js} +1 -1
  47. package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-IXPlMsm3.js → sequenceDiagram-X6HHIX6F-DJ0J92CL.js} +1 -1
  48. package/out/renderer/assets/{settings-page-Z2OLjBKF.js → settings-page-7W7M_FIE.js} +1 -1
  49. package/out/renderer/assets/{stateDiagram-DGXRK772-DwnKmcwl.js → stateDiagram-DGXRK772-BRFGcjMT.js} +1 -1
  50. package/out/renderer/assets/{stateDiagram-v2-YXO3MK2T-DvubQewt.js → stateDiagram-v2-YXO3MK2T-BYuk7Mif.js} +1 -1
  51. package/out/renderer/assets/{timeline-definition-BDJGKUSR-CjzEHtD7.js → timeline-definition-BDJGKUSR-V2afOmNy.js} +1 -1
  52. package/out/renderer/assets/{use-agent-like-mutation-DbSbfmyO.js → use-agent-like-mutation-CS4DoZ5N.js} +1 -1
  53. package/out/renderer/assets/{xychartDiagram-VJFVF3MP-DTe8TwT9.js → xychartDiagram-VJFVF3MP-DSF1yRSG.js} +1 -1
  54. package/out/renderer/index.html +1 -1
  55. package/package.json +4 -2
  56. package/out/renderer/assets/channel-dajMwelj.js +0 -1
  57. package/out/renderer/assets/clone-CNUTfmCA.js +0 -1
package/out/main/index.js CHANGED
@@ -1323,6 +1323,19 @@ function trustedDesktopFrame(actual, expected) {
1323
1323
  return false;
1324
1324
  }
1325
1325
  }
1326
+ const PYTHON_BYTECODE_CACHE_DIR_NAME = "python-bytecode-cache";
1327
+ const PATH_SEPARATORS = /[\\/]/;
1328
+ function insideAppBundle(path2) {
1329
+ return path2.split(PATH_SEPARATORS).some((segment) => segment.length > 4 && segment.endsWith(".app"));
1330
+ }
1331
+ function pythonBytecodeCacheDir(userDataDir) {
1332
+ return path.join(userDataDir, PYTHON_BYTECODE_CACHE_DIR_NAME);
1333
+ }
1334
+ function withPythonBytecodeCache(env, userDataDir) {
1335
+ const existing = env.PYTHONPYCACHEPREFIX?.trim();
1336
+ if (existing && !insideAppBundle(existing)) return { ...env };
1337
+ return { ...env, PYTHONPYCACHEPREFIX: pythonBytecodeCacheDir(userDataDir) };
1338
+ }
1326
1339
  var main = { exports: {} };
1327
1340
  const version = "16.5.0";
1328
1341
  const require$$4 = {
@@ -1989,6 +2002,7 @@ class BackendServiceManager {
1989
2002
  `Shell environment variables loaded successfully. PATH: ${this.shellEnv.PATH || this.shellEnv.Path || "(not set)"}`,
1990
2003
  LogFileType.BACKEND
1991
2004
  );
2005
+ this.shellEnv = withPythonBytecodeCache(this.shellEnv, this.appDataPath);
1992
2006
  } catch (error) {
1993
2007
  logger.error(
1994
2008
  "Error loading shell environment variables:",
@@ -1997,6 +2011,34 @@ class BackendServiceManager {
1997
2011
  );
1998
2012
  }
1999
2013
  }
2014
+ /**
2015
+ * The environment the backend process is spawned with.
2016
+ *
2017
+ * The prefix is applied HERE, at the point the environment is handed to the
2018
+ * spawn, rather than only inside `loadShellEnvironment()`. That load is
2019
+ * started from the constructor without `await` (a constructor cannot wait)
2020
+ * and `start()` has nothing sequencing it, so a shell rc that takes seconds
2021
+ * to source - nvm, pyenv, conda init all do - leaves `shellEnv` at its
2022
+ * `{ ...process.env }` seed when the first spawn happens: no prefix, and the
2023
+ * exact pre-fix configuration, on the machines whose rc is slowest. Applying
2024
+ * it here makes the guarantee structural instead of ordering-dependent, so
2025
+ * no spawn path can miss it.
2026
+ *
2027
+ * These spawns are the ones whose `python` is the interpreter we ship:
2028
+ * CPython writes `__pycache__/*.pyc` beside the sources it imports, those
2029
+ * sources are inside the code-sealed `.app`, and every such write breaks the
2030
+ * signature ShipIt validates before an in-place update.
2031
+ */
2032
+ backendSpawnEnv() {
2033
+ return {
2034
+ ...withPythonBytecodeCache(this.shellEnv, this.appDataPath),
2035
+ // Managed starts rotate the token; set after the spread so the spawn
2036
+ // can never inherit a stale one from the shell environment. `start()`
2037
+ // mints it before it can reach a spawn site, so the `??` only satisfies
2038
+ // the field's nullable seed from `process.env`.
2039
+ LOCAL_OPERATOR_DESKTOP_TOKEN: this.desktopToken ?? void 0
2040
+ };
2041
+ }
2000
2042
  /**
2001
2043
  * Load environment variables from macOS shell configuration files
2002
2044
  */
@@ -2315,10 +2357,7 @@ class BackendServiceManager {
2315
2357
  detached: false,
2316
2358
  // Ensure process is not detached from parent
2317
2359
  stdio: "pipe",
2318
- env: {
2319
- ...this.shellEnv,
2320
- LOCAL_OPERATOR_DESKTOP_TOKEN: this.desktopToken
2321
- },
2360
+ env: this.backendSpawnEnv(),
2322
2361
  // On Windows, we need to create a new process group to ensure proper termination
2323
2362
  ...process.platform === "win32" ? { windowsHide: true } : {}
2324
2363
  });
@@ -2381,10 +2420,7 @@ class BackendServiceManager {
2381
2420
  detached: false,
2382
2421
  // Ensure process is not detached from parent
2383
2422
  stdio: "pipe",
2384
- env: {
2385
- ...this.shellEnv,
2386
- LOCAL_OPERATOR_DESKTOP_TOKEN: this.desktopToken
2387
- },
2423
+ env: this.backendSpawnEnv(),
2388
2424
  // On Windows, we need to create a new process group to ensure proper termination
2389
2425
  ...process.platform === "win32" ? { windowsHide: true } : {}
2390
2426
  });
@@ -2818,7 +2854,7 @@ class BackendServiceManager {
2818
2854
  return success;
2819
2855
  }
2820
2856
  }
2821
- 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# Create app data directory if it doesn\'t exist\nif ! mkdir -p "${APP_DATA_DIR}"; then\n echo "ERROR: Unable to create app data directory at ${APP_DATA_DIR}"\n echo "Please check permissions and try again."\n exit 1\nfi\n\n# Handle cleanup on script exit\ncleanup() {\n # Remove any temporary files created during execution\n if [ -f "${APP_DATA_DIR}/get-pip.py" ]; then\n rm -f "${APP_DATA_DIR}/get-pip.py"\n fi\n if [ -n "${FFMPEG_TEMP_ARCHIVE:-}" ] && [ -f "${FFMPEG_TEMP_ARCHIVE}" ]; then\n log "Removing temporary FFmpeg archive: ${FFMPEG_TEMP_ARCHIVE}"\n rm -f "${FFMPEG_TEMP_ARCHIVE}"\n fi\n echo "$(date): Cleanup completed."\n}\n\n# Set trap only for manual interruption (SIGINT, SIGTERM), not normal exits\ntrap cleanup SIGINT SIGTERM\n\n\n# Start logging\nexec > >(tee -a "${LOG_FILE}") 2>&1\necho "[${TIMESTAMP}]: Starting Local Operator backend installation..."\necho "[${TIMESTAMP}]: System information: $(uname -a)"\n\n# Function to log messages with timestamp (defined later, but used by FFmpeg section)\n_log_internal() {\n local internal_timestamp\n internal_timestamp=$(date +"%Y-%m-%d %H:%M:%S")\n echo "[${internal_timestamp}] $1"\n}\n\n# --- FFmpeg Installation ---\nBIN_DIR="${APP_DATA_DIR}/bin"\nFFMPEG_BIN="${BIN_DIR}/ffmpeg" # Path to ffmpeg binary\n\n_log_internal "Ensuring bin directory exists: ${BIN_DIR}"\nif ! mkdir -p "${BIN_DIR}"; then\n _log_internal "ERROR: Unable to create bin directory at ${BIN_DIR}"\n exit 1\nfi\n\n# Check if FFmpeg is already installed and executable\nif [ -f "${FFMPEG_BIN}" ] && [ -x "${FFMPEG_BIN}" ]; then\n _log_internal "FFmpeg already installed at ${FFMPEG_BIN}. Skipping download."\nelse\n _log_internal "FFmpeg not found or not executable. Attempting to download and install FFmpeg..."\n\n FFMPEG_DOWNLOAD_URL=""\n\n LINUX_ARCH=$(uname -m)\n if [[ "${LINUX_ARCH}" == "x86_64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-x64"\n elif [[ "${LINUX_ARCH}" == "aarch64" ]] || [[ "${LINUX_ARCH}" == "arm64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-arm64"\n else\n _log_internal "Error: Unsupported CPU architecture for FFmpeg download: ${LINUX_ARCH}"\n exit 1\n fi\n\n _log_internal "Downloading FFmpeg from: ${FFMPEG_DOWNLOAD_URL}"\n if command -v curl >/dev/null 2>&1; then\n if ! curl -L "${FFMPEG_DOWNLOAD_URL}" -o "${FFMPEG_BIN}"; then\n _log_internal "Error: Failed to download FFmpeg using curl from ${FFMPEG_DOWNLOAD_URL}"\n exit 1\n fi\n elif command -v wget >/dev/null 2>&1; then\n if ! wget -O "${FFMPEG_BIN}" "${FFMPEG_DOWNLOAD_URL}"; then\n _log_internal "Error: Failed to download FFmpeg using wget from ${FFMPEG_DOWNLOAD_URL}"\n exit 1\n fi\n else\n _log_internal "Error: Neither curl nor wget found. Cannot download FFmpeg."\n exit 1\n fi\n _log_internal "FFmpeg binary downloaded successfully to ${FFMPEG_BIN}"\n\n chmod +x "${FFMPEG_BIN}"\n _log_internal "Set executable permissions for ${FFMPEG_BIN}"\n\n # Verify FFmpeg is executable after download\n if [ ! -f "${FFMPEG_BIN}" ] || [ ! -x "${FFMPEG_BIN}" ]; then\n _log_internal "Error: FFmpeg binary not found or not executable after download."\n exit 1\n fi\nfi\n\n_log_internal "FFmpeg installation complete. FFmpeg binary is at: ${FFMPEG_BIN}"\n\n# Function to check if a command exists\ncommand_exists() {\n command -v "$1" >/dev/null 2>&1\n}\n\n# Function to log messages with timestamp\nlog() {\n local timestamp\n timestamp=$(date +"%Y-%m-%d %H:%M:%S")\n echo "[${timestamp}] $1"\n}\n\n# Function to display error messages and exit\nerror_exit() {\n log "ERROR: $1" >&2\n exit "${2:-1}"\n}\n\n# Function to check if Python binary is valid for current architecture\nis_valid_python_binary() {\n local python_path="$1"\n \n # First check if file exists and is executable\n if [ ! -f "${python_path}" ] || [ ! -x "${python_path}" ]; then\n return 1\n fi\n \n # Try to run a simple Python command to verify it works on current architecture\n "${python_path}" -c "print(\'Testing Python executable\')" >/dev/null 2>&1\n return $?\n}\n\n# Check for network connectivity to key servers\ncheck_connectivity() {\n log "Checking network connectivity..."\n local servers=("pypi.org" "bootstrap.pypa.io")\n local has_connectivity=false\n \n for server in "${servers[@]}"; do\n if command_exists curl; then\n if curl --connect-timeout 5 -s "https://${server}" -o /dev/null; then\n has_connectivity=true\n break\n fi\n elif command_exists wget; then\n if wget --timeout=5 -q --spider "https://${server}"; then\n has_connectivity=true\n break\n fi\n fi\n done\n \n if [ "$has_connectivity" = false ]; then\n log "WARNING: Network connectivity issues detected. This may affect installation."\n else\n log "Network connectivity confirmed."\n fi\n}\n\n# Call connectivity check\ncheck_connectivity\n\n# Check if PYTHON_BIN is already set by the installer\nif [ -n "${PYTHON_BIN:-}" ]; then\n log "Using Python executable provided by installer: ${PYTHON_BIN}"\n # Verify the provided Python binary works on this architecture\n if ! is_valid_python_binary "${PYTHON_BIN}"; then\n log "Warning: The provided Python binary is not compatible with this system architecture."\n log "Will attempt to find a system Python installation instead."\n PYTHON_BIN=""\n fi\nfi\n\n# If PYTHON_BIN is empty or invalid, try to find a suitable Python installation\nif [ -z "${PYTHON_BIN:-}" ] || ! is_valid_python_binary "${PYTHON_BIN:-}"; then\n # Try to find a suitable Python installation\n log "Looking for a suitable Python installation..."\n \n # Try multiple possible locations to find Python\n POSSIBLE_PYTHON_PATHS=(\n # System paths first (more likely to be compatible with current architecture)\n "/usr/local/bin/python3.12"\n "/usr/bin/python3.12"\n "/usr/local/bin/python3"\n "/usr/bin/python3"\n # From environment variable (set by the installer)\n "${ELECTRON_RESOURCE_PATH:-}/python/bin/python3"\n # Development paths\n "$(dirname "$0")/../../../resources/python/bin/python3"\n "$(pwd)/resources/python/bin/python3"\n )\n\n # Find the first Python that exists and is version 3.12+ and works on this architecture\n PYTHON_BIN=""\n for path in "${POSSIBLE_PYTHON_PATHS[@]}"; do\n # Skip empty paths that might come from unset environment variables\n if [ -z "${path}" ]; then\n continue\n fi\n \n if is_valid_python_binary "${path}"; then\n # Check Python version\n if PY_VERSION=$("${path}" -c "import sys; print(f\'{sys.version_info.major}.{sys.version_info.minor}\')" 2>/dev/null); then\n MAJOR=$(echo "${PY_VERSION}" | cut -d. -f1)\n MINOR=$(echo "${PY_VERSION}" | cut -d. -f2)\n if [ "${MAJOR}" -eq 3 ] && [ "${MINOR}" -ge 12 ]; then\n PYTHON_BIN="${path}"\n log "Found suitable Python ${PY_VERSION} at ${path}"\n break\n else\n log "Python at ${path} is version ${PY_VERSION}, which is below the required 3.12+"\n fi\n fi\n else\n if [ -f "${path}" ]; then\n log "Python at ${path} exists but is not compatible with this system architecture"\n fi\n fi\n done\n\n # If we couldn\'t find a suitable Python, exit with error\n if [ -z "${PYTHON_BIN:-}" ]; then\n error_exit "No suitable Python installation found (version 3.12 or higher required).\\nPlease install Python 3.12 or higher before running this script.\\nYou can install Python from https://www.python.org/downloads/"\n fi\nfi\n\necho "Using Python: $PYTHON_BIN"\n"$PYTHON_BIN" --version || {\n echo "Error: Failed to run Python. Please ensure it is executable and accessible."\n echo "System architecture: $(uname -m)"\n echo "Python binary architecture: $(file "$PYTHON_BIN" 2>/dev/null || echo \'Unable to determine\')"\n exit 1\n}\n\n# Detect the Linux distribution to provide specific instructions\nif command_exists lsb_release; then\n DISTRO=$(lsb_release -is)\n DISTRO_VERSION=$(lsb_release -rs)\n echo "Detected Linux distribution: $DISTRO $DISTRO_VERSION"\nelif [ -f /etc/os-release ]; then\n DISTRO=$(grep -oP \'(?<=^ID=).+\' /etc/os-release | tr -d \'"\')\n DISTRO_VERSION=$(grep -oP \'(?<=^VERSION_ID=).+\' /etc/os-release | tr -d \'"\')\n echo "Detected Linux distribution: $DISTRO $DISTRO_VERSION"\nelse\n DISTRO="unknown"\n echo "Unable to detect Linux distribution"\nfi\n\n# Check if Python has venv module and ensurepip available\nlog "Checking for venv module and ensurepip availability..."\nVENV_AVAILABLE=0\nENSUREPIP_AVAILABLE=0\n\n"${PYTHON_BIN}" -c "import venv" > /dev/null 2>&1 || VENV_AVAILABLE=1\n"${PYTHON_BIN}" -c "import ensurepip" > /dev/null 2>&1 || ENSUREPIP_AVAILABLE=1\n\n# If venv is still not available, try to use virtualenv as a fallback\nif [ $VENV_AVAILABLE -ne 0 ]; then\n echo "venv module is not available. Checking for virtualenv as an alternative..."\n \n # Check if virtualenv is already installed\n if "$PYTHON_BIN" -c "import virtualenv" > /dev/null 2>&1; then\n echo "virtualenv is available, will use it instead of venv"\n else\n echo "ERROR: Neither venv nor virtualenv is available."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n exit 1\n fi\nfi\n\n# Create virtual environment if it doesn\'t exist\nif [ ! -d "$VENV_PATH" ]; then\n echo "Creating virtual environment at $VENV_PATH..."\n # Remove any potentially corrupted virtual environment\n if [ -e "$VENV_PATH" ]; then\n echo "Removing existing but potentially corrupted venv directory..."\n rm -rf "$VENV_PATH"\n fi\n \n # Make sure parent directory exists and is writable\n mkdir -p "$(dirname "$VENV_PATH")"\n \n # Try different methods to create a virtual environment\n VENV_CREATE_STATUS=1\n \n # First try with venv if available\n if [ $VENV_AVAILABLE -eq 0 ] && [ $ENSUREPIP_AVAILABLE -eq 0 ]; then\n echo "Creating virtual environment using venv module..."\n "$PYTHON_BIN" -m venv "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n fi\n \n # If venv failed, try virtualenv\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n echo "venv creation failed with status $VENV_CREATE_STATUS. Falling back to virtualenv..."\n if command_exists virtualenv; then\n virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n elif "$PYTHON_BIN" -m pip list | grep -q virtualenv; then\n "$PYTHON_BIN" -m virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n else\n echo "ERROR: Both venv and virtualenv are unavailable or failed."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n echo "You can install Python 3.12 from https://www.python.org/downloads/"\n exit 1\n fi\n fi\n \n # Check if virtual environment creation was successful\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n log "ERROR: Failed to create virtual environment. Exit code: $VENV_CREATE_STATUS"\n log "Virtual environment path: $VENV_PATH"\n log "Python binary used: $PYTHON_BIN"\n ls -la "$(dirname "$VENV_PATH")" || true\n log "Python executable permissions:"\n ls -la "$PYTHON_BIN" || true\n \n # Check for common issues\n log "Checking for common virtual environment creation issues..."\n \n # Check disk space\n df -h "$(dirname "$VENV_PATH")" || true\n \n # Check if directory is writable\n if [ ! -w "$(dirname "$VENV_PATH")" ]; then\n log "ERROR: Directory $(dirname "$VENV_PATH") is not writable"\n fi\n \n # Try to create a minimal virtual environment manually as a last resort\n log "Attempting to create a minimal virtual environment manually..."\n mkdir -p "$VENV_PATH/bin" || error_exit "Could not create directory $VENV_PATH/bin"\n echo "#!/bin/bash" > "$VENV_PATH/bin/activate"\n echo "export VIRTUAL_ENV=\\"$VENV_PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "export PATH=\\"$VENV_PATH/bin:\\$PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "unset PYTHONHOME" >> "$VENV_PATH/bin/activate"\n chmod +x "$VENV_PATH/bin/activate" || error_exit "Could not set execute permissions on activate script"\n \n # Create symlinks to the system Python\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python" || log "WARNING: Failed to create symlink for python"\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python3" || log "WARNING: Failed to create symlink for python3"\n \n # Try to bootstrap pip\n log "Bootstrapping pip in the minimal virtual environment..."\n if command_exists curl; then\n curl -s https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py" || log "WARNING: Failed to download get-pip.py"\n elif command_exists wget; then\n wget -q -O "$APP_DATA_DIR/get-pip.py" https://bootstrap.pypa.io/get-pip.py || log "WARNING: Failed to download get-pip.py"\n else\n log "ERROR: Neither curl nor wget available to download get-pip.py"\n error_exit "Installation cannot continue without being able to download pip"\n fi\n \n # shellcheck disable=SC1090\n source "$VENV_PATH/bin/activate" && python "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n error_exit "Failed to create even a minimal virtual environment."\n else\n log "Created a minimal virtual environment as a fallback."\n fi\n else\n log "Successfully created virtual environment"\n fi\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ]; then\n echo "Python executable missing in virtual environment, creating symlink..."\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python"\nfi\n\nif [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "pip missing in virtual environment, attempting to bootstrap it..."\n curl -s https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py"\n "$VENV_PATH/bin/python" "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Failed to bootstrap pip in the virtual environment"\n exit 1\n fi\nfi\n\necho "Virtual environment structure verified"\n\n# Activate virtual environment and install local-operator\necho "Installing local-operator in virtual environment..."\nsource "$VENV_PATH/bin/activate"\n\necho "Upgrading pip..."\npython -m pip install --upgrade pip || {\n echo "WARNING: Failed to upgrade pip. Will try to continue with existing pip version."\n pip --version\n}\n\n# Check network connectivity to PyPI\necho "Checking network connectivity to PyPI..."\nif command_exists curl; then\n curl -s https://pypi.org/pypi/local-operator/json -o /dev/null || {\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n }\nelif command_exists wget; then\n wget -q --spider https://pypi.org/pypi/local-operator/json || {\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n }\nfi\n\necho "Installing local-operator package..."\npython -m pip install --verbose local-operator || {\n echo "ERROR: Failed to install local-operator package. Exit code: $?"\n echo "Python version:"\n python --version\n echo "pip version:"\n pip --version\n echo "Available pip packages:"\n pip list\n exit 1\n}\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n log "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator" || true\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n \n # Try to run the version command with full error output\n log "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n log "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n log "Binary details:"\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n log "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator" || true\n log ""\n log "This could be due to missing dependencies or architecture incompatibility."\n log "Try running the binary manually to see specific errors."\n \n # Don\'t exit here - just warn the user\n log "WARNING: Installation completed but binary verification failed."\n }\n \n # Create a summary of the installation\n SUMMARY_FILE="${APP_DATA_DIR}/installation-summary-${TIMESTAMP}.txt"\n {\n echo "=== Local Operator Backend Installation Summary ==="\n echo "Timestamp: $(date)"\n echo "Python Version: $("${PYTHON_BIN}" --version 2>&1)"\n echo "Virtual Environment: ${VENV_PATH}"\n echo "Binary Location: ${VENV_PATH}/bin/local-operator"\n echo "To activate the virtual environment, run: source ${VENV_PATH}/bin/activate"\n echo "To test the installation, run: ${VENV_PATH}/bin/local-operator --version"\n echo "Log file: ${LOG_FILE}"\n echo "=================================================="\n } > "${SUMMARY_FILE}"\n \n log "Installation summary saved to: ${SUMMARY_FILE}"\nelse\n log "Error: Failed to install Local Operator backend. Binary not found."\n log "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/" || true\n log "Check the log file for details: ${LOG_FILE}"\n error_exit "Installation failed. Binary not installed properly."\nfi\n\nlog "Installation completed successfully at $(date)"\n\n# Clean up temporary files now that installation is complete\ncleanup\n\n# Print final instructions\nlog ""\nlog "=== INSTALLATION COMPLETE ==="\nlog "You can now use the Local Operator backend by running: ${VENV_PATH}/bin/local-operator"\nlog "Installation directory: ${VENV_PATH}"\nlog "Log file: ${LOG_FILE}"\nlog "================================"\n';
2857
+ 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# 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# Create app data directory if it doesn\'t exist\nif ! mkdir -p "${APP_DATA_DIR}"; then\n echo "ERROR: Unable to create app data directory at ${APP_DATA_DIR}"\n echo "Please check permissions and try again."\n exit 1\nfi\n\n# Handle cleanup on script exit\ncleanup() {\n # Remove any temporary files created during execution\n if [ -f "${APP_DATA_DIR}/get-pip.py" ]; then\n rm -f "${APP_DATA_DIR}/get-pip.py"\n fi\n if [ -n "${FFMPEG_TEMP_ARCHIVE:-}" ] && [ -f "${FFMPEG_TEMP_ARCHIVE}" ]; then\n log "Removing temporary FFmpeg archive: ${FFMPEG_TEMP_ARCHIVE}"\n rm -f "${FFMPEG_TEMP_ARCHIVE}"\n fi\n echo "$(date): Cleanup completed."\n}\n\n# Set trap only for manual interruption (SIGINT, SIGTERM), not normal exits\ntrap cleanup SIGINT SIGTERM\n\n\n# Start logging\nexec > >(tee -a "${LOG_FILE}") 2>&1\necho "[${TIMESTAMP}]: Starting Local Operator backend installation..."\necho "[${TIMESTAMP}]: System information: $(uname -a)"\n\n# Function to log messages with timestamp (defined later, but used by FFmpeg section)\n_log_internal() {\n local internal_timestamp\n internal_timestamp=$(date +"%Y-%m-%d %H:%M:%S")\n echo "[${internal_timestamp}] $1"\n}\n\n# --- FFmpeg Installation ---\nBIN_DIR="${APP_DATA_DIR}/bin"\nFFMPEG_BIN="${BIN_DIR}/ffmpeg" # Path to ffmpeg binary\n\n_log_internal "Ensuring bin directory exists: ${BIN_DIR}"\nif ! mkdir -p "${BIN_DIR}"; then\n _log_internal "ERROR: Unable to create bin directory at ${BIN_DIR}"\n exit 1\nfi\n\n# Check if FFmpeg is already installed and executable\nif [ -f "${FFMPEG_BIN}" ] && [ -x "${FFMPEG_BIN}" ]; then\n _log_internal "FFmpeg already installed at ${FFMPEG_BIN}. Skipping download."\nelse\n _log_internal "FFmpeg not found or not executable. Attempting to download and install FFmpeg..."\n\n FFMPEG_DOWNLOAD_URL=""\n\n LINUX_ARCH=$(uname -m)\n if [[ "${LINUX_ARCH}" == "x86_64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-x64"\n elif [[ "${LINUX_ARCH}" == "aarch64" ]] || [[ "${LINUX_ARCH}" == "arm64" ]]; then\n FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-arm64"\n else\n _log_internal "Error: Unsupported CPU architecture for FFmpeg download: ${LINUX_ARCH}"\n exit 1\n fi\n\n _log_internal "Downloading FFmpeg from: ${FFMPEG_DOWNLOAD_URL}"\n if command -v curl >/dev/null 2>&1; then\n if ! curl -L "${FFMPEG_DOWNLOAD_URL}" -o "${FFMPEG_BIN}"; then\n _log_internal "Error: Failed to download FFmpeg using curl from ${FFMPEG_DOWNLOAD_URL}"\n exit 1\n fi\n elif command -v wget >/dev/null 2>&1; then\n if ! wget -O "${FFMPEG_BIN}" "${FFMPEG_DOWNLOAD_URL}"; then\n _log_internal "Error: Failed to download FFmpeg using wget from ${FFMPEG_DOWNLOAD_URL}"\n exit 1\n fi\n else\n _log_internal "Error: Neither curl nor wget found. Cannot download FFmpeg."\n exit 1\n fi\n _log_internal "FFmpeg binary downloaded successfully to ${FFMPEG_BIN}"\n\n chmod +x "${FFMPEG_BIN}"\n _log_internal "Set executable permissions for ${FFMPEG_BIN}"\n\n # Verify FFmpeg is executable after download\n if [ ! -f "${FFMPEG_BIN}" ] || [ ! -x "${FFMPEG_BIN}" ]; then\n _log_internal "Error: FFmpeg binary not found or not executable after download."\n exit 1\n fi\nfi\n\n_log_internal "FFmpeg installation complete. FFmpeg binary is at: ${FFMPEG_BIN}"\n\n# Function to check if a command exists\ncommand_exists() {\n command -v "$1" >/dev/null 2>&1\n}\n\n# Function to log messages with timestamp\nlog() {\n local timestamp\n timestamp=$(date +"%Y-%m-%d %H:%M:%S")\n echo "[${timestamp}] $1"\n}\n\n# Function to display error messages and exit\nerror_exit() {\n log "ERROR: $1" >&2\n exit "${2:-1}"\n}\n\n# Function to check if Python binary is valid for current architecture\nis_valid_python_binary() {\n local python_path="$1"\n \n # First check if file exists and is executable\n if [ ! -f "${python_path}" ] || [ ! -x "${python_path}" ]; then\n return 1\n fi\n \n # Try to run a simple Python command to verify it works on current architecture\n "${python_path}" -c "print(\'Testing Python executable\')" >/dev/null 2>&1\n return $?\n}\n\n# Check for network connectivity to key servers\ncheck_connectivity() {\n log "Checking network connectivity..."\n local servers=("pypi.org" "bootstrap.pypa.io")\n local has_connectivity=false\n \n for server in "${servers[@]}"; do\n if command_exists curl; then\n if curl --connect-timeout 5 -s "https://${server}" -o /dev/null; then\n has_connectivity=true\n break\n fi\n elif command_exists wget; then\n if wget --timeout=5 -q --spider "https://${server}"; then\n has_connectivity=true\n break\n fi\n fi\n done\n \n if [ "$has_connectivity" = false ]; then\n log "WARNING: Network connectivity issues detected. This may affect installation."\n else\n log "Network connectivity confirmed."\n fi\n}\n\n# Call connectivity check\ncheck_connectivity\n\n# Check if PYTHON_BIN is already set by the installer\nif [ -n "${PYTHON_BIN:-}" ]; then\n log "Using Python executable provided by installer: ${PYTHON_BIN}"\n # Verify the provided Python binary works on this architecture\n if ! is_valid_python_binary "${PYTHON_BIN}"; then\n log "Warning: The provided Python binary is not compatible with this system architecture."\n log "Will attempt to find a system Python installation instead."\n PYTHON_BIN=""\n fi\nfi\n\n# If PYTHON_BIN is empty or invalid, try to find a suitable Python installation\nif [ -z "${PYTHON_BIN:-}" ] || ! is_valid_python_binary "${PYTHON_BIN:-}"; then\n # Try to find a suitable Python installation\n log "Looking for a suitable Python installation..."\n \n # Try multiple possible locations to find Python\n POSSIBLE_PYTHON_PATHS=(\n # System paths first (more likely to be compatible with current architecture)\n "/usr/local/bin/python3.12"\n "/usr/bin/python3.12"\n "/usr/local/bin/python3"\n "/usr/bin/python3"\n # From environment variable (set by the installer)\n "${ELECTRON_RESOURCE_PATH:-}/python/bin/python3"\n # Development paths\n "$(dirname "$0")/../../../resources/python/bin/python3"\n "$(pwd)/resources/python/bin/python3"\n )\n\n # Find the first Python that exists and is version 3.12+ and works on this architecture\n PYTHON_BIN=""\n for path in "${POSSIBLE_PYTHON_PATHS[@]}"; do\n # Skip empty paths that might come from unset environment variables\n if [ -z "${path}" ]; then\n continue\n fi\n \n if is_valid_python_binary "${path}"; then\n # Check Python version\n if PY_VERSION=$("${path}" -c "import sys; print(f\'{sys.version_info.major}.{sys.version_info.minor}\')" 2>/dev/null); then\n MAJOR=$(echo "${PY_VERSION}" | cut -d. -f1)\n MINOR=$(echo "${PY_VERSION}" | cut -d. -f2)\n if [ "${MAJOR}" -eq 3 ] && [ "${MINOR}" -ge 12 ]; then\n PYTHON_BIN="${path}"\n log "Found suitable Python ${PY_VERSION} at ${path}"\n break\n else\n log "Python at ${path} is version ${PY_VERSION}, which is below the required 3.12+"\n fi\n fi\n else\n if [ -f "${path}" ]; then\n log "Python at ${path} exists but is not compatible with this system architecture"\n fi\n fi\n done\n\n # If we couldn\'t find a suitable Python, exit with error\n if [ -z "${PYTHON_BIN:-}" ]; then\n error_exit "No suitable Python installation found (version 3.12 or higher required).\\nPlease install Python 3.12 or higher before running this script.\\nYou can install Python from https://www.python.org/downloads/"\n fi\nfi\n\necho "Using Python: $PYTHON_BIN"\n"$PYTHON_BIN" --version || {\n echo "Error: Failed to run Python. Please ensure it is executable and accessible."\n echo "System architecture: $(uname -m)"\n echo "Python binary architecture: $(file "$PYTHON_BIN" 2>/dev/null || echo \'Unable to determine\')"\n exit 1\n}\n\n# Detect the Linux distribution to provide specific instructions\nif command_exists lsb_release; then\n DISTRO=$(lsb_release -is)\n DISTRO_VERSION=$(lsb_release -rs)\n echo "Detected Linux distribution: $DISTRO $DISTRO_VERSION"\nelif [ -f /etc/os-release ]; then\n DISTRO=$(grep -oP \'(?<=^ID=).+\' /etc/os-release | tr -d \'"\')\n DISTRO_VERSION=$(grep -oP \'(?<=^VERSION_ID=).+\' /etc/os-release | tr -d \'"\')\n echo "Detected Linux distribution: $DISTRO $DISTRO_VERSION"\nelse\n DISTRO="unknown"\n echo "Unable to detect Linux distribution"\nfi\n\n# Check if Python has venv module and ensurepip available\nlog "Checking for venv module and ensurepip availability..."\nVENV_AVAILABLE=0\nENSUREPIP_AVAILABLE=0\n\n"${PYTHON_BIN}" -c "import venv" > /dev/null 2>&1 || VENV_AVAILABLE=1\n"${PYTHON_BIN}" -c "import ensurepip" > /dev/null 2>&1 || ENSUREPIP_AVAILABLE=1\n\n# If venv is still not available, try to use virtualenv as a fallback\nif [ $VENV_AVAILABLE -ne 0 ]; then\n echo "venv module is not available. Checking for virtualenv as an alternative..."\n \n # Check if virtualenv is already installed\n if "$PYTHON_BIN" -c "import virtualenv" > /dev/null 2>&1; then\n echo "virtualenv is available, will use it instead of venv"\n else\n echo "ERROR: Neither venv nor virtualenv is available."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n exit 1\n fi\nfi\n\n# Create virtual environment if it doesn\'t exist\nif [ ! -d "$VENV_PATH" ]; then\n echo "Creating virtual environment at $VENV_PATH..."\n # Remove any potentially corrupted virtual environment\n if [ -e "$VENV_PATH" ]; then\n echo "Removing existing but potentially corrupted venv directory..."\n rm -rf "$VENV_PATH"\n fi\n \n # Make sure parent directory exists and is writable\n mkdir -p "$(dirname "$VENV_PATH")"\n \n # Try different methods to create a virtual environment\n VENV_CREATE_STATUS=1\n \n # First try with venv if available\n if [ $VENV_AVAILABLE -eq 0 ] && [ $ENSUREPIP_AVAILABLE -eq 0 ]; then\n echo "Creating virtual environment using venv module..."\n "$PYTHON_BIN" -m venv "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n fi\n \n # If venv failed, try virtualenv\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n echo "venv creation failed with status $VENV_CREATE_STATUS. Falling back to virtualenv..."\n if command_exists virtualenv; then\n virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n elif "$PYTHON_BIN" -m pip list | grep -q virtualenv; then\n "$PYTHON_BIN" -m virtualenv -p "$PYTHON_BIN" "$VENV_PATH"\n VENV_CREATE_STATUS=$?\n else\n echo "ERROR: Both venv and virtualenv are unavailable or failed."\n echo "Python 3.12 and venv should be installed as package dependencies."\n echo "Please ensure Python 3.12 and venv are properly installed on your system."\n echo "You can install Python 3.12 from https://www.python.org/downloads/"\n exit 1\n fi\n fi\n \n # Check if virtual environment creation was successful\n if [ $VENV_CREATE_STATUS -ne 0 ]; then\n log "ERROR: Failed to create virtual environment. Exit code: $VENV_CREATE_STATUS"\n log "Virtual environment path: $VENV_PATH"\n log "Python binary used: $PYTHON_BIN"\n ls -la "$(dirname "$VENV_PATH")" || true\n log "Python executable permissions:"\n ls -la "$PYTHON_BIN" || true\n \n # Check for common issues\n log "Checking for common virtual environment creation issues..."\n \n # Check disk space\n df -h "$(dirname "$VENV_PATH")" || true\n \n # Check if directory is writable\n if [ ! -w "$(dirname "$VENV_PATH")" ]; then\n log "ERROR: Directory $(dirname "$VENV_PATH") is not writable"\n fi\n \n # Try to create a minimal virtual environment manually as a last resort\n log "Attempting to create a minimal virtual environment manually..."\n mkdir -p "$VENV_PATH/bin" || error_exit "Could not create directory $VENV_PATH/bin"\n echo "#!/bin/bash" > "$VENV_PATH/bin/activate"\n echo "export VIRTUAL_ENV=\\"$VENV_PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "export PATH=\\"$VENV_PATH/bin:\\$PATH\\"" >> "$VENV_PATH/bin/activate"\n echo "unset PYTHONHOME" >> "$VENV_PATH/bin/activate"\n chmod +x "$VENV_PATH/bin/activate" || error_exit "Could not set execute permissions on activate script"\n \n # Create symlinks to the system Python\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python" || log "WARNING: Failed to create symlink for python"\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python3" || log "WARNING: Failed to create symlink for python3"\n \n # Try to bootstrap pip\n log "Bootstrapping pip in the minimal virtual environment..."\n if command_exists curl; then\n curl -s https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py" || log "WARNING: Failed to download get-pip.py"\n elif command_exists wget; then\n wget -q -O "$APP_DATA_DIR/get-pip.py" https://bootstrap.pypa.io/get-pip.py || log "WARNING: Failed to download get-pip.py"\n else\n log "ERROR: Neither curl nor wget available to download get-pip.py"\n error_exit "Installation cannot continue without being able to download pip"\n fi\n \n # shellcheck disable=SC1090\n source "$VENV_PATH/bin/activate" && python "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n error_exit "Failed to create even a minimal virtual environment."\n else\n log "Created a minimal virtual environment as a fallback."\n fi\n else\n log "Successfully created virtual environment"\n fi\nfi\n\n# Verify the virtual environment structure\necho "Verifying virtual environment structure..."\nif [ ! -f "$VENV_PATH/bin/python" ]; then\n echo "Python executable missing in virtual environment, creating symlink..."\n ln -sf "$PYTHON_BIN" "$VENV_PATH/bin/python"\nfi\n\nif [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "pip missing in virtual environment, attempting to bootstrap it..."\n curl -s https://bootstrap.pypa.io/get-pip.py -o "$APP_DATA_DIR/get-pip.py"\n "$VENV_PATH/bin/python" "$APP_DATA_DIR/get-pip.py" --no-warn-script-location\n \n if [ ! -f "$VENV_PATH/bin/pip" ]; then\n echo "ERROR: Failed to bootstrap pip in the virtual environment"\n exit 1\n fi\nfi\n\necho "Virtual environment structure verified"\n\n# Activate virtual environment and install local-operator\necho "Installing local-operator in virtual environment..."\nsource "$VENV_PATH/bin/activate"\n\necho "Upgrading pip..."\npython -m pip install --upgrade pip || {\n echo "WARNING: Failed to upgrade pip. Will try to continue with existing pip version."\n pip --version\n}\n\n# Check network connectivity to PyPI\necho "Checking network connectivity to PyPI..."\nif command_exists curl; then\n curl -s https://pypi.org/pypi/local-operator/json -o /dev/null || {\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n }\nelif command_exists wget; then\n wget -q --spider https://pypi.org/pypi/local-operator/json || {\n echo "WARNING: Could not reach PyPI. Network connectivity issues might prevent installation."\n }\nfi\n\necho "Installing local-operator package..."\npython -m pip install --verbose local-operator || {\n echo "ERROR: Failed to install local-operator package. Exit code: $?"\n echo "Python version:"\n python --version\n echo "pip version:"\n pip --version\n echo "Available pip packages:"\n pip list\n exit 1\n}\necho "local-operator installation successful"\n\n# Verify installation\nif [ -f "$VENV_PATH/bin/local-operator" ]; then\n log "Local Operator backend installed successfully!"\n \n # Show more information about the installed binary\n ls -la "$VENV_PATH/bin/local-operator" || true\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n \n # Try to run the version command with full error output\n log "Testing local-operator binary..."\n "$VENV_PATH/bin/local-operator" --version || {\n log "ERROR: local-operator binary exists but failed to execute. Exit code: $?"\n log "Binary details:"\n file "$VENV_PATH/bin/local-operator" 2>/dev/null || log "Note: \'file\' command not available"\n log "Binary permissions:"\n ls -la "$VENV_PATH/bin/local-operator" || true\n log ""\n log "This could be due to missing dependencies or architecture incompatibility."\n log "Try running the binary manually to see specific errors."\n \n # Don\'t exit here - just warn the user\n log "WARNING: Installation completed but binary verification failed."\n }\n \n # Create a summary of the installation\n SUMMARY_FILE="${APP_DATA_DIR}/installation-summary-${TIMESTAMP}.txt"\n {\n echo "=== Local Operator Backend Installation Summary ==="\n echo "Timestamp: $(date)"\n echo "Python Version: $("${PYTHON_BIN}" --version 2>&1)"\n echo "Virtual Environment: ${VENV_PATH}"\n echo "Binary Location: ${VENV_PATH}/bin/local-operator"\n echo "To activate the virtual environment, run: source ${VENV_PATH}/bin/activate"\n echo "To test the installation, run: ${VENV_PATH}/bin/local-operator --version"\n echo "Log file: ${LOG_FILE}"\n echo "=================================================="\n } > "${SUMMARY_FILE}"\n \n log "Installation summary saved to: ${SUMMARY_FILE}"\nelse\n log "Error: Failed to install Local Operator backend. Binary not found."\n log "Contents of bin directory:"\n ls -la "$VENV_PATH/bin/" || true\n log "Check the log file for details: ${LOG_FILE}"\n error_exit "Installation failed. Binary not installed properly."\nfi\n\nlog "Installation completed successfully at $(date)"\n\n# Clean up temporary files now that installation is complete\ncleanup\n\n# Print final instructions\nlog ""\nlog "=== INSTALLATION COMPLETE ==="\nlog "You can now use the Local Operator backend by running: ${VENV_PATH}/bin/local-operator"\nlog "Installation directory: ${VENV_PATH}"\nlog "Log file: ${LOG_FILE}"\nlog "================================"\n';
2822
2858
  const macosInstallScriptRaw = `#!/bin/bash
2823
2859
  # Local Operator Backend Installation Script for macOS
2824
2860
  # This script uses the bundled standalone Python and sets up a virtual environment for the Local Operator backend.
@@ -2832,6 +2868,22 @@ APP_DATA_DIR="$HOME/Library/Application Support/$APP_NAME"
2832
2868
  VENV_PATH="$APP_DATA_DIR/$VENV_NAME"
2833
2869
  LOG_FILE="$APP_DATA_DIR/backend-install.log"
2834
2870
 
2871
+ # Keep CPython's bytecode cache out of the application bundle.
2872
+ #
2873
+ # The bundled interpreter writes __pycache__/*.pyc beside the stdlib sources it
2874
+ # imports, and those sources live inside the code-signed .app - which is the
2875
+ # build's own extraResource. Every such write is a change to a sealed resource:
2876
+ # measured on an installed 0.17.3, \`codesign --verify --deep\` reported 308
2877
+ # \`file added:\` violations, all of them .pyc, and ShipIt then refuses the
2878
+ # in-place update with -67028 errSecCSBadBundleFormat. A file codesign reports
2879
+ # as *added* can be deleted and the seal comes back; a *modified* one cannot,
2880
+ # which is why nothing may ship a .pyc at all. The app sets this variable when
2881
+ # it spawns us; defaulting it here means a standalone run of this script cannot
2882
+ # pollute the bundle either. $HOME is the right place for the same reason the
2883
+ # venv lives there: it is never inside the thing that gets signed and swapped.
2884
+ : "\${PYTHONPYCACHEPREFIX:=$APP_DATA_DIR/python-bytecode-cache}"
2885
+ export PYTHONPYCACHEPREFIX
2886
+
2835
2887
  # Determine CPU Architecture and Python Directory Name
2836
2888
  ARCH=$(uname -m)
2837
2889
  PYTHON_DIR_NAME="python" # Default for x86_64
@@ -3102,6 +3154,19 @@ $VenvPath = "$AppDataDir\\\\$VenvName"
3102
3154
  $LogFile = "$AppDataDir\\\\backend-install-shell.log"
3103
3155
  $PyenvDir = "$env:USERPROFILE\\\\.pyenv"
3104
3156
 
3157
+ # Keep CPython's bytecode cache out of the application directory.
3158
+ #
3159
+ # Same rule as the macOS script, and stated here for the same reason: an
3160
+ # interpreter writes __pycache__/*.pyc beside the stdlib sources it imports. On
3161
+ # Windows those sources are the bundled python directory rather than a
3162
+ # code-signed bundle, so this is hygiene rather than the load-bearing fix it is
3163
+ # on macOS - but the app and the script must not disagree about where bytecode
3164
+ # goes. $AppDataDir is used for the same reason the venv is: it is never inside
3165
+ # the installed application tree.
3166
+ if (-not $env:PYTHONPYCACHEPREFIX) {
3167
+ $env:PYTHONPYCACHEPREFIX = "$AppDataDir\\\\python-bytecode-cache"
3168
+ }
3169
+
3105
3170
  # Create app data directory if it doesn't exist
3106
3171
  if (-not (Test-Path $AppDataDir)) {
3107
3172
  New-Item -ItemType Directory -Path $AppDataDir -Force | Out-Null
@@ -3672,7 +3737,7 @@ class BackendInstaller {
3672
3737
  electron.app.once("before-quit", appQuitHandler);
3673
3738
  const result = await new Promise((resolve) => {
3674
3739
  try {
3675
- const env = { ...process.env };
3740
+ const env = withPythonBytecodeCache({ ...process.env }, this.appDataPath);
3676
3741
  env.ELECTRON_RESOURCE_PATH = this.resourcesPath;
3677
3742
  logger.info(
3678
3743
  `Setting ELECTRON_RESOURCE_PATH to ${this.resourcesPath}`,
@@ -3982,6 +4047,22 @@ function registerDesktopIPC(window, expectedUrl, request, streams, media, notifi
3982
4047
  });
3983
4048
  }
3984
4049
  }
4050
+ function presentWindow(window, show) {
4051
+ if (show === "never") return;
4052
+ if (window.isMinimized()) window.restore();
4053
+ if (show === "inactive") window.showInactive();
4054
+ else window.show();
4055
+ }
4056
+ function raiseWindow(window, show) {
4057
+ if (show === "never") return;
4058
+ if (window.isMinimized()) window.restore();
4059
+ if (show === "inactive") {
4060
+ window.showInactive();
4061
+ return;
4062
+ }
4063
+ window.show();
4064
+ window.focus();
4065
+ }
3985
4066
  const NOTIFY_TTL_MS = 10 * 60 * 1e3;
3986
4067
  const MAX_DEDUPE_KEYS = 2048;
3987
4068
  const CONTRACT_PROBE_ATTEMPTS = 3;
@@ -4000,9 +4081,10 @@ function gateBody(kind, gateTitle, gateDetail) {
4000
4081
  return subject || GATE_BODIES[kind] || GATE_BODIES.approval;
4001
4082
  }
4002
4083
  class DesktopNotifier {
4003
- constructor(window, request) {
4084
+ constructor(window, request, windowRaise = "focus") {
4004
4085
  this.window = window;
4005
4086
  this.request = request;
4087
+ this.windowRaise = windowRaise;
4006
4088
  }
4007
4089
  delivered = /* @__PURE__ */ new Map();
4008
4090
  /** Last known frontend epoch per session, to key dedupe on owner epoch. */
@@ -4191,6 +4273,7 @@ class DesktopNotifier {
4191
4273
  /** Called by the stream relay for every parsed frame it forwards. */
4192
4274
  observe(sessionId2, frame) {
4193
4275
  if (!this.canNotify) return;
4276
+ if (this.windowRaise === "never") return;
4194
4277
  if (frame.type === "snapshot") {
4195
4278
  this.epochs.set(sessionId2, frame.payload.frontend.epoch);
4196
4279
  const gate = frame.payload.frontend.snapshot.pending_gate;
@@ -4414,9 +4497,7 @@ class DesktopNotifier {
4414
4497
  notification.on("click", () => {
4415
4498
  const target2 = this.window();
4416
4499
  if (!target2) return;
4417
- if (target2.isMinimized()) target2.restore();
4418
- target2.show();
4419
- target2.focus();
4500
+ raiseWindow(target2, this.windowRaise);
4420
4501
  target2.webContents.send("desktop-open-conversation", { sessionId: sessionId2 });
4421
4502
  });
4422
4503
  notification.show();
@@ -4472,6 +4553,111 @@ function installedBundleSealBlock(appBundlePath, detail, version2) {
4472
4553
  detail: `${appBundlePath}: ${detail}`
4473
4554
  };
4474
4555
  }
4556
+ const VIOLATION_LINE = /^file (added|modified|missing):\s*(.+)$/;
4557
+ function parseSealViolations(output) {
4558
+ const violations = [];
4559
+ for (const rawLine of output.split("\n")) {
4560
+ const line = rawLine.trim();
4561
+ if (line.length === 0) continue;
4562
+ const match = VIOLATION_LINE.exec(line);
4563
+ if (match) {
4564
+ violations.push({
4565
+ kind: match[1],
4566
+ path: match[2].trim()
4567
+ });
4568
+ continue;
4569
+ }
4570
+ violations.push({ kind: "other", path: line });
4571
+ }
4572
+ return violations;
4573
+ }
4574
+ function canonicalPath(path$1) {
4575
+ try {
4576
+ return fs.realpathSync(path$1);
4577
+ } catch {
4578
+ try {
4579
+ return path.join(fs.realpathSync(path.dirname(path$1)), path.basename(path$1));
4580
+ } catch {
4581
+ return path$1;
4582
+ }
4583
+ }
4584
+ }
4585
+ const BUNDLED_PYTHON_DIRS = ["python", "python_aarch64"];
4586
+ function isPythonBytecodePath(bundlePath, candidate) {
4587
+ const bundle = canonicalPath(bundlePath);
4588
+ const path$1 = canonicalPath(candidate);
4589
+ const dirs = BUNDLED_PYTHON_DIRS.map(
4590
+ (name) => path.join(bundle, "Contents", "Resources", name)
4591
+ );
4592
+ const separator = "/";
4593
+ if (!path$1.endsWith(".pyc")) return false;
4594
+ const segments = path$1.split(separator);
4595
+ if (!segments.includes("__pycache__")) return false;
4596
+ return dirs.some((dir) => path$1.startsWith(`${dir}${separator}`));
4597
+ }
4598
+ function planPythonBytecodeHeal(bundlePath, violations) {
4599
+ const added = violations.filter((violation) => violation.kind === "added");
4600
+ const elsewhere = violations.filter(
4601
+ (violation) => violation.kind !== "added"
4602
+ );
4603
+ if (added.length === 0) {
4604
+ return {
4605
+ healable: false,
4606
+ reason: `no added-resource violation to heal (${violations.length} violation(s))`,
4607
+ paths: []
4608
+ };
4609
+ }
4610
+ if (elsewhere.length > 0) {
4611
+ const kinds = [...new Set(elsewhere.map((violation) => violation.kind))];
4612
+ return {
4613
+ healable: false,
4614
+ reason: `unhealable violation class(es) present: ${kinds.join(", ")}`,
4615
+ paths: []
4616
+ };
4617
+ }
4618
+ const outside = added.filter(
4619
+ (violation) => !isPythonBytecodePath(bundlePath, violation.path)
4620
+ );
4621
+ if (outside.length > 0) {
4622
+ return {
4623
+ healable: false,
4624
+ reason: `added resource outside the bundled python trees: ${outside[0].path}`,
4625
+ paths: []
4626
+ };
4627
+ }
4628
+ return {
4629
+ healable: true,
4630
+ reason: `${added.length} bytecode file(s) added after signing`,
4631
+ paths: [...new Set(added.map((violation) => violation.path))]
4632
+ };
4633
+ }
4634
+ function healPythonBytecode(bundlePath, violations, remove = (path2) => fs.rmSync(path2, { force: true })) {
4635
+ const plan = planPythonBytecodeHeal(bundlePath, violations);
4636
+ if (!plan.healable) return { ...plan, removed: [] };
4637
+ const removed = [];
4638
+ for (const path2 of plan.paths) {
4639
+ if (!isPythonBytecodePath(bundlePath, path2)) {
4640
+ return {
4641
+ healable: false,
4642
+ reason: `refused to remove ${path2}: not bytecode under the bundled python trees`,
4643
+ paths: plan.paths,
4644
+ removed
4645
+ };
4646
+ }
4647
+ try {
4648
+ remove(path2);
4649
+ removed.push(path2);
4650
+ } catch (error) {
4651
+ return {
4652
+ healable: false,
4653
+ reason: `could not remove ${path2}: ${error instanceof Error ? error.message : String(error)}`,
4654
+ paths: plan.paths,
4655
+ removed
4656
+ };
4657
+ }
4658
+ }
4659
+ return { ...plan, removed };
4660
+ }
4475
4661
  function metadataBasenames(filePath, updatePath) {
4476
4662
  const names = [path.basename(filePath)];
4477
4663
  if (updatePath) names.push(path.basename(updatePath));
@@ -5318,7 +5504,11 @@ function runCommand(command, args, options = {}) {
5318
5504
  node_child_process.execFile(
5319
5505
  command,
5320
5506
  args,
5321
- { timeout: options.timeoutMs ?? 2e4, maxBuffer: 4 * 1024 * 1024 },
5507
+ {
5508
+ timeout: options.timeoutMs ?? 2e4,
5509
+ maxBuffer: 4 * 1024 * 1024,
5510
+ ...options.env ? { env: options.env } : {}
5511
+ },
5322
5512
  (error, stdout, stderr) => {
5323
5513
  const code = error?.code;
5324
5514
  const killed = error?.killed === true;
@@ -5457,6 +5647,20 @@ class UpdateService {
5457
5647
  markerDir() {
5458
5648
  return electron.app.getPath("userData");
5459
5649
  }
5650
+ /**
5651
+ * The environment every python this service spawns has to run under.
5652
+ *
5653
+ * The bundled venv's `python` is the interpreter we ship, so its stdlib - and
5654
+ * therefore any `__pycache__` CPython writes for it - is inside the code-sealed
5655
+ * `.app`. `pip show` and the pip upgrade both compile stdlib modules, so both
5656
+ * would unseal the bundle the pre-flight is about to ask ShipIt to replace.
5657
+ *
5658
+ * userData is the location for the same reason the pending marker uses it: it
5659
+ * is never inside the bundle being swapped.
5660
+ */
5661
+ pythonSpawnEnv() {
5662
+ return withPythonBytecodeCache(process.env, this.markerDir());
5663
+ }
5460
5664
  sendToRenderer(channel, payload) {
5461
5665
  const webContents = this.mainWindow?.webContents;
5462
5666
  if (!webContents || webContents.isDestroyed()) return false;
@@ -5721,6 +5925,13 @@ class UpdateService {
5721
5925
  * - A probe that could not run is retried once and then *allowed to proceed*.
5722
5926
  * "We could not ask" is not "the bundle is bad", and treating it as one is
5723
5927
  * how a transient hiccup became a permanent reinstall message (R5).
5928
+ *
5929
+ * A bundle that fails the check is given one chance to heal itself, because
5930
+ * the failure every 0.17.x/0.18.0 install has is ours rather than the user's:
5931
+ * a CPython stdlib bytecode cache written into the sealed bundle by our own
5932
+ * backend. Those writes are `file added:` violations and deleting exactly
5933
+ * those files restores the seal (measured; see `healPythonBytecode`), where a
5934
+ * bundle broken any other way still gets the reinstall refusal below.
5724
5935
  */
5725
5936
  async probeInstalledBundleSeal(version2) {
5726
5937
  if (process.platform !== "darwin" || !electron.app.isPackaged) return null;
@@ -5737,13 +5948,15 @@ class UpdateService {
5737
5948
  ["--verify", "--deep", "--verbose=2", bundlePath],
5738
5949
  { timeoutMs: SEAL_PROBE_TIMEOUT_MS }
5739
5950
  );
5740
- let seal = evaluateBundleSeal(await probeBundle());
5951
+ let probe = await probeBundle();
5952
+ let seal = evaluateBundleSeal(probe);
5741
5953
  if (seal.kind === "unavailable") {
5742
5954
  logger.warn(
5743
5955
  `Seal check did not complete, retrying once: ${seal.detail}`,
5744
5956
  LogFileType.UPDATE_SERVICE
5745
5957
  );
5746
- seal = evaluateBundleSeal(await probeBundle());
5958
+ probe = await probeBundle();
5959
+ seal = evaluateBundleSeal(probe);
5747
5960
  }
5748
5961
  if (seal.kind === "unavailable") {
5749
5962
  logger.warn(
@@ -5763,7 +5976,33 @@ class UpdateService {
5763
5976
  `Installed bundle failed its seal check: ${seal.detail}`,
5764
5977
  LogFileType.UPDATE_SERVICE
5765
5978
  );
5766
- return installedBundleSealBlock(bundlePath, seal.detail, version2);
5979
+ const violations = parseSealViolations(probe.stdout);
5980
+ const heal = healPythonBytecode(bundlePath, violations);
5981
+ if (!heal.healable) {
5982
+ logger.warn(
5983
+ `Installed bundle is not healable in place: ${heal.reason}`,
5984
+ LogFileType.UPDATE_SERVICE
5985
+ );
5986
+ return installedBundleSealBlock(bundlePath, seal.detail, version2);
5987
+ }
5988
+ logger.info(
5989
+ `Removed ${heal.removed.length} bytecode file(s) our own interpreter had written into the sealed bundle: ${heal.reason}. Paths:
5990
+ ${heal.removed.join("\n")}`,
5991
+ LogFileType.UPDATE_SERVICE
5992
+ );
5993
+ const healed = evaluateBundleSeal(await probeBundle());
5994
+ if (healed.kind === "sealed") {
5995
+ logger.info(
5996
+ `Installed bundle sealed again after removing ${heal.removed.length} added bytecode file(s); continuing with the install.`,
5997
+ LogFileType.UPDATE_SERVICE
5998
+ );
5999
+ return null;
6000
+ }
6001
+ logger.error(
6002
+ `Installed bundle still fails its seal check after healing: ${healed.detail}`,
6003
+ LogFileType.UPDATE_SERVICE
6004
+ );
6005
+ return installedBundleSealBlock(bundlePath, healed.detail, version2);
5767
6006
  }
5768
6007
  /** File names the updater's metadata knows this update by. */
5769
6008
  stagedArtifactCandidates(info) {
@@ -6866,7 +7105,7 @@ class UpdateService {
6866
7105
  const probe = await runCommand(
6867
7106
  pythonPath,
6868
7107
  ["-m", "pip", "show", "local-operator"],
6869
- { timeoutMs: 12e4 }
7108
+ { timeoutMs: 12e4, env: this.pythonSpawnEnv() }
6870
7109
  );
6871
7110
  if (probe.exitCode !== 0) {
6872
7111
  logger.warn(
@@ -7030,7 +7269,8 @@ class UpdateService {
7030
7269
  LogFileType.UPDATE_SERVICE
7031
7270
  );
7032
7271
  const pipRun = await runCommand(pip.command, pip.args, {
7033
- timeoutMs: 15 * 60 * 1e3
7272
+ timeoutMs: 15 * 60 * 1e3,
7273
+ env: this.pythonSpawnEnv()
7034
7274
  });
7035
7275
  logger.info(
7036
7276
  `pip stdout:
@@ -7298,6 +7538,105 @@ ${pipRun.stderr.trim()}`,
7298
7538
  }
7299
7539
  }
7300
7540
  }
7541
+ const WINDOW_MODES = ["normal", "headless", "inactive"];
7542
+ const WINDOW_MODE_ENV = "LOCAL_OPERATOR_UI_WINDOW_MODE";
7543
+ const WINDOW_SIZE_ENV = "LOCAL_OPERATOR_UI_WINDOW_SIZE";
7544
+ const WINDOW_MODE_FLAG = "--window-mode";
7545
+ const WINDOW_SIZE_FLAG = "--window-size";
7546
+ const WINDOW_MIN_WIDTH = 800;
7547
+ const WINDOW_MIN_HEIGHT = 600;
7548
+ const WINDOW_MAX_EDGE = 16384;
7549
+ const DEFAULT_WINDOW_WIDTH = 1380;
7550
+ const DEFAULT_WINDOW_HEIGHT = 900;
7551
+ const WINDOW_BEHAVIOUR = {
7552
+ normal: { show: "focus", focusable: true, backgroundThrottling: true },
7553
+ inactive: { show: "inactive", focusable: true, backgroundThrottling: false },
7554
+ headless: { show: "never", focusable: false, backgroundThrottling: false }
7555
+ };
7556
+ function readFlag(argv, name) {
7557
+ for (let index = 0; index < argv.length; index += 1) {
7558
+ const argument = argv[index];
7559
+ if (argument === name) {
7560
+ const next = argv[index + 1];
7561
+ return { found: true, value: next?.startsWith("--") ? void 0 : next };
7562
+ }
7563
+ if (argument.startsWith(`${name}=`)) {
7564
+ return { found: true, value: argument.slice(name.length + 1) };
7565
+ }
7566
+ }
7567
+ return { found: false, value: void 0 };
7568
+ }
7569
+ function parseWindowMode(value) {
7570
+ if (value === void 0) return null;
7571
+ const normalized = value.trim().toLowerCase();
7572
+ return WINDOW_MODES.includes(normalized) ? normalized : null;
7573
+ }
7574
+ const WINDOW_SIZE_PATTERN = /^(\d+)\s*[x×]\s*(\d+)$/i;
7575
+ function parseWindowSize(value) {
7576
+ if (value === void 0) return null;
7577
+ const match = value.trim().match(WINDOW_SIZE_PATTERN);
7578
+ if (!match) return null;
7579
+ const width = Number(match[1]);
7580
+ const height = Number(match[2]);
7581
+ if (width <= 0 || height <= 0) return null;
7582
+ return { width, height };
7583
+ }
7584
+ function resolveWindowLaunchPlan(input = {}) {
7585
+ const env = input.env ?? {};
7586
+ const argv = input.argv ?? [];
7587
+ const problems = [];
7588
+ const modeFlag = readFlag(argv, WINDOW_MODE_FLAG);
7589
+ const modeRaw = modeFlag.found ? modeFlag.value : env[WINDOW_MODE_ENV];
7590
+ const parsedMode = parseWindowMode(modeRaw);
7591
+ if (modeFlag.found && modeFlag.value === void 0) {
7592
+ problems.push(
7593
+ `${WINDOW_MODE_FLAG} needs a value: ${WINDOW_MODES.join("|")}`
7594
+ );
7595
+ } else if (parsedMode === null && modeRaw !== void 0) {
7596
+ problems.push(
7597
+ `${modeFlag.found ? WINDOW_MODE_FLAG : WINDOW_MODE_ENV}="${modeRaw}" is not one of ${WINDOW_MODES.join("|")}; using normal`
7598
+ );
7599
+ }
7600
+ const mode = parsedMode ?? "normal";
7601
+ const sizeFlag = readFlag(argv, WINDOW_SIZE_FLAG);
7602
+ const sizeRaw = sizeFlag.found ? sizeFlag.value : env[WINDOW_SIZE_ENV];
7603
+ const parsedSize = parseWindowSize(sizeRaw);
7604
+ if (sizeFlag.found && sizeFlag.value === void 0) {
7605
+ problems.push(`${WINDOW_SIZE_FLAG} needs a value: <width>x<height>`);
7606
+ } else if (parsedSize === null && sizeRaw !== void 0) {
7607
+ problems.push(
7608
+ `${sizeFlag.found ? WINDOW_SIZE_FLAG : WINDOW_SIZE_ENV}="${sizeRaw}" is not <width>x<height>; using ${DEFAULT_WINDOW_WIDTH}x${DEFAULT_WINDOW_HEIGHT}`
7609
+ );
7610
+ }
7611
+ const requested = parsedSize ?? {
7612
+ width: DEFAULT_WINDOW_WIDTH,
7613
+ height: DEFAULT_WINDOW_HEIGHT
7614
+ };
7615
+ const width = Math.min(
7616
+ Math.max(requested.width, WINDOW_MIN_WIDTH),
7617
+ WINDOW_MAX_EDGE
7618
+ );
7619
+ const height = Math.min(
7620
+ Math.max(requested.height, WINDOW_MIN_HEIGHT),
7621
+ WINDOW_MAX_EDGE
7622
+ );
7623
+ if (width !== requested.width || height !== requested.height) {
7624
+ problems.push(
7625
+ `window size ${requested.width}x${requested.height} clamped to ${width}x${height} (floor ${WINDOW_MIN_WIDTH}x${WINDOW_MIN_HEIGHT}, ceiling ${WINDOW_MAX_EDGE})`
7626
+ );
7627
+ }
7628
+ return {
7629
+ mode,
7630
+ ...WINDOW_BEHAVIOUR[mode],
7631
+ width,
7632
+ height,
7633
+ problems
7634
+ };
7635
+ }
7636
+ function describeWindowLaunch(plan) {
7637
+ const behaviour = plan.show === "never" ? "window created and never shown" : plan.show === "inactive" ? "window shown without activating the app" : "window shown and focused";
7638
+ return `window mode ${plan.mode}: ${plan.width}x${plan.height}, ${behaviour}, page throttling ${plan.backgroundThrottling ? "on" : "off"}`;
7639
+ }
7301
7640
  const BASE64_FILE_EXTENSIONS = ["csv", "tsv", "xls", "xlsx", "ods"];
7302
7641
  electron.app.setName("Local Operator");
7303
7642
  const image = electron.nativeImage.createFromPath(icon);
@@ -7435,16 +7774,28 @@ function createApplicationMenu() {
7435
7774
  }
7436
7775
  function createWindow() {
7437
7776
  const mainWindow2 = new electron.BrowserWindow({
7438
- width: 1380,
7439
- height: 900,
7777
+ width: windowLaunch.width,
7778
+ height: windowLaunch.height,
7440
7779
  // The layout is verified down to 800x600 and not below: the app rail,
7441
7780
  // the per-route list pane and the canvas all have their own minimums,
7442
7781
  // and past this point they start taking room from each other rather
7443
7782
  // than from the window. The auth popup below sets its own floor the
7444
- // same way.
7445
- minWidth: 800,
7446
- minHeight: 600,
7783
+ // same way. The floor lives in `window-mode.ts` because that is what
7784
+ // clamps a requested `--window-size` to it.
7785
+ minWidth: WINDOW_MIN_WIDTH,
7786
+ minHeight: WINDOW_MIN_HEIGHT,
7447
7787
  show: false,
7788
+ /*
7789
+ * `headless` is never shown, so no key window can be made out of it; this
7790
+ * keeps that true of the window object itself rather than of the code
7791
+ * path. It is NOT a safety net against a stray `show()`: on macOS
7792
+ * `NativeWindowMac::Show()` activates the app for any non-panel window
7793
+ * whatever `focusable` says (measured — a non-focusable window, shown,
7794
+ * made the app frontmost while `isFocused()` still read false). The guard
7795
+ * against a window being raised is that `window-raise.ts` is the only
7796
+ * module that raises one, and this flag is the second line of defence.
7797
+ */
7798
+ focusable: windowLaunch.focusable,
7448
7799
  autoHideMenuBar: true,
7449
7800
  title: "Local Operator",
7450
7801
  icon,
@@ -7458,7 +7809,17 @@ function createWindow() {
7458
7809
  nodeIntegration: false,
7459
7810
  contextIsolation: true,
7460
7811
  webSecurity: true,
7461
- allowRunningInsecureContent: false
7812
+ allowRunningInsecureContent: false,
7813
+ // Chromium throttles timers and animation frames in a window it
7814
+ // believes nobody is looking at, which would make a headless test
7815
+ // run measure a different app than the one users get. Measured on
7816
+ // Electron 35 / macOS: a `show: false` window does keep
7817
+ // `visibilityState` at "visible" and keeps rAF ticking, but that
7818
+ // comes from Electron's MacWebContentsOcclusion default rather
7819
+ // than from anything this app controls, so hold the page at full
7820
+ // rate explicitly instead of trusting the platform. `normal`
7821
+ // keeps Electron's own default.
7822
+ backgroundThrottling: windowLaunch.backgroundThrottling
7462
7823
  }
7463
7824
  });
7464
7825
  mainWindow2.webContents.on("console-message", (_event, level, message) => {
@@ -7467,7 +7828,16 @@ function createWindow() {
7467
7828
  write.call(logger, `[renderer] ${message}`, LogFileType.BACKEND);
7468
7829
  });
7469
7830
  mainWindow2.on("ready-to-show", () => {
7470
- mainWindow2.show();
7831
+ presentWindow(mainWindow2, windowLaunch.show);
7832
+ if (windowLaunch.mode === "normal") return;
7833
+ setTimeout(() => {
7834
+ if (!mainWindow2 || mainWindow2.isDestroyed()) return;
7835
+ const [width, height] = mainWindow2.getSize();
7836
+ const [contentWidth, contentHeight] = mainWindow2.getContentSize();
7837
+ console.log(
7838
+ `[window-mode] state: visible=${mainWindow2.isVisible()} focused=${mainWindow2.isFocused()} focusable=${mainWindow2.isFocusable()} size=${width}x${height} content=${contentWidth}x${contentHeight}`
7839
+ );
7840
+ }, 1500);
7471
7841
  });
7472
7842
  mainWindow2.webContents.setWindowOpenHandler((details) => {
7473
7843
  const url = new URL(details.url);
@@ -7538,6 +7908,17 @@ function createWindow() {
7538
7908
  }
7539
7909
  const backendService = new BackendServiceManager();
7540
7910
  const backendInstaller = new BackendInstaller();
7911
+ const windowLaunch = resolveWindowLaunchPlan({
7912
+ env: process.env,
7913
+ argv: process.argv
7914
+ });
7915
+ for (const problem of windowLaunch.problems) {
7916
+ logger.warn(`[window-mode] ${problem}`, LogFileType.BACKEND);
7917
+ console.log(`[window-mode] ${problem}`);
7918
+ }
7919
+ if (windowLaunch.mode !== "normal") {
7920
+ console.log(`[window-mode] ${describeWindowLaunch(windowLaunch)}`);
7921
+ }
7541
7922
  let mainWindow = null;
7542
7923
  const zoomInFromEvent = () => {
7543
7924
  if (mainWindow) {
@@ -7565,8 +7946,7 @@ if (!gotTheLock) {
7565
7946
  } else {
7566
7947
  electron.app.on("second-instance", (_event, commandLine) => {
7567
7948
  if (mainWindow) {
7568
- if (mainWindow.isMinimized()) mainWindow.restore();
7569
- mainWindow.focus();
7949
+ raiseWindow(mainWindow, windowLaunch.show);
7570
7950
  }
7571
7951
  });
7572
7952
  }
@@ -7586,7 +7966,11 @@ electron.app.whenReady().then(async () => {
7586
7966
  () => mainWindow,
7587
7967
  (input) => backendService.requestDesktop(input)
7588
7968
  );
7589
- const desktopNotifier = new DesktopNotifier(() => mainWindow, sendDesktop);
7969
+ const desktopNotifier = new DesktopNotifier(
7970
+ () => mainWindow,
7971
+ sendDesktop,
7972
+ windowLaunch.show
7973
+ );
7590
7974
  backendService.onBackendReady(() => {
7591
7975
  void desktopNotifier.refreshNotificationContract();
7592
7976
  });