Navigation
ELOWEN · Documentation

Last updated: October 20, 2026

Browse documentation · Host platform seam
Developer reference

Host platform seam

packages/plugin-shared/hostPlatform.mjs is the one place the Elowen CLI client, the updater, the installer, the client build and the shared helpers learn which operating system and CPU the process runs on. Every Windows, Linux and macOS difference in that code branches on its answer; nothing there reads process.platform or process.arch itself, apart from the two server-side exceptions in Limits.

  • hostPlatform() returns linux, darwin or win32, and throws Elowen does not run on <platform> for anything else.
  • hostArch() returns x64 or arm64, and throws Elowen does not run on <arch> processors otherwise.

The names are Node's, because the client release contract (clientPlatformSchema, clientArchSchema in src/licence/contract.ts), the archive names elowen-client-<version>-<platform>-<arch>.tgz and the ticket metadata already carry them. Both read process on every call, so a test that sets process.platform sees the platform it set.

Using it

Branch on the seam where the difference lives:

import { hostPlatform } from '../../packages/plugin-shared/hostPlatform.mjs';

if (hostPlatform() === 'win32') process.chdir(homedir());

A function that takes a platform so its tests can pass one (clientShell, tarProgram, pathHint, ServiceHost.platform, the process watchdog's platform option) gets its production value from the seam: defaultServiceHost builds the host from hostPlatform(), and the watchdog's option defaults to it.

What goes through it

Area Where Difference
Install and service src/cli/clientService.ts::defaultServiceHost, src/cli/clientUpdate.ts Run key and PATH entry, systemd user unit or LaunchAgent; release URL and package launcher
Connect src/cli/clientConnect.ts::runConnect Windows moves to the home directory and writes its own log
Executor and shell src/cli/localExecution.ts, packages/plugin-shared/localProcessWatchdog.mjs, packages/plugin-shared/localExecutor.mjs, packages/plugin-shared/localPtyWatchdog.mjs, src/cli/chat/localShell.ts PowerShell and taskkill on Windows, /bin/sh and process groups elsewhere; a python3 PTY helper on Linux and ConPTY on Windows; the reported platform, arch and shell
Bundled tools src/cli/localExecution.ts::bundledRipgrep vendor/ripgrep/rg.exe on Windows, vendor/ripgrep/rg elsewhere
Updater src/update/core.ts junction swap, rename retry and trash sweep on Windows
Durable writes packages/plugin-shared/atomicJson.mjs::syncDirectory no directory fsync on Windows
Process identity src/cli/chat/processTermination.ts, packages/plugin-shared/processTokens.mjs /proc only on Linux
Server installer src/cli/install/preflight.ts, src/cli/install/hostConvergence.ts, src/cli/privileged.ts Linux only
Client build scripts/build-client.mjs macOS archives build only on macOS

Windows argv processes in runLocalProcessWatchdog use cross-spawn for executable and npm .cmd resolution and command-line quoting. Arguments remain an array at this seam. The child inherits the watchdog's stdin, stdout and stderr descriptors, so MCP and LSP streams preserve protocol bytes rather than crossing a PowerShell text pipeline. PowerShell remains the shell-command path. Cancellation and owner loss still terminate the same process tree.

Windows uninstall stops connect in one PowerShell invocation. It opens and retains the recorded process's handle, verifies that the same process object names the install's runtime\node.exe, and runs taskkill /T /F before disposing the handle in finally. Windows cannot reuse the PID while that handle is held. A held lock alone cannot prove the PID file is current. A mismatch aborts uninstall without killing that process. An already exited process is harmless; the stop completes only once the lock is free. The uninstall Path read also removes ElowenConnect through the PowerShell registry provider: absent keys and values are harmless, while access and deletion errors abort uninstall.

PowerShell commands keep leading using statements before the prelude, including semicolon-separated statements on one line. A boundary tokenizer treats strings, here-strings, nested comments, escaped characters and balanced groups as opaque, so their separators remain inside the original statement. Script-level param, attributes and named blocks are dot-sourced. Each executable body captures its final $? in finally before dot-sourcing resets it, including an early return. A handled error followed by success returns 0; a final failing cmdlet returns 1, independently of $Error contents. A native $LASTEXITCODE takes precedence. The clean block does not change the captured execution status. The shared shell launcher is used by local execution and the chat's local shell.

Interactive terminals

A Project terminal on a paired computer runs under its own watchdog, and the platform decides how. packages/plugin-shared/localPtyWatchdog.mjs starts /bin/bash -l through packages/plugin-shared/ptyHelper.py under /usr/bin/python3 -I -S on Linux, and loads node-pty with useConpty and the locally found PowerShell plus -NoLogo on Windows. Everything else answers LOCAL_TARGET_UNSUPPORTED: macOS, a Windows build below 18309, or a Linux machine whose /usr/bin/python3 is missing or not executable. packages/plugin-shared/localExecutor.mjs puts the transport split on the seam too: pty-input and pty-resize travel as IPC messages on Windows and as framed bytes on the helper's stdin on Linux. scripts/build-client.mjs copies ptyHelper.py beside the client bundle for Linux and only the target architecture's conpty.node and conpty_console_list.node prebuilds for Windows, keeps node-pty out of the bundle and core's production dependencies, and packages/plugin-shared/pty.mjs holds the frame codec and names /usr/bin/python3 as the interpreter every POSIX target runs the helper with. The protocol itself belongs to the client and paired computers.

Limits

  • Windows cancellation uses taskkill /T /F /PID, then kills the shell directly if it still lives. taskkill has a five-second timeout and the direct kill settles after that bound even if its streams never close. A process detached from the parent tree survives cancellation; inherited output handles can keep the command streams open until that detached process exits.

  • scripts/kernelLock.mjs keeps its own process.platform checks (EXLOCK open on Windows, the bundled kernel-lock helper on macOS, flock on Linux). The privileged worker installs that file alone as /usr/local/libexec/kernelLock.mjs, so it cannot import anything. Server-side code that takes no test platform reads it directly as well: packages/db/src/config/kernelLock.ts for the same macOS helper, and src/cli/install/databasePolicy.ts, which reads the cgroup CPU quota only on Linux.

  • The seam answers for the process it runs in. The platform of a paired computer as the server sees it is a different fact: it arrives in the ticket metadata as LocalExecutionTarget.platform and drives localTargetPath in packages/plugin-shared/localExecution.mjs.

  • The server outside the installer runs on Linux only and does not branch per platform.

Registered PDF tools

Poppler is installed by the computer's package manager. The shared command elowen tools install pdf [--bin <directory>] registers that installation through src/cli/pdfTools.ts: it finds pdfinfo, pdftotext and pdftoppm on PATH or in the given directory, validates each with -v and atomically saves absolute paths to pdf-tools.json in clientPrefix. Windows searches for .exe files and reads PATH without regard to its spelling. A missing tool or failed probe leaves the previous registration intact.

The CLI passes pdfTools: () => readPdfTools(env) to createLocalExecutor. Each Poppler argv execution reads the current registration before starting a watchdog, so an already running background connection sees a new registration without restarting. A missing registration runs the tool from the client's PATH. Invalid stored data or a registered executable that no longer runs fails the operation; it does not substitute another executable. Shell commands keep their ordinary PATH.

Install Poppler with winget install --id oschwartz10612.Poppler -e on Windows, brew install poppler on macOS, or apt install poppler-utils / dnf install poppler-utils on Linux. Then register it from a new terminal. Use --bin when its binary directory is outside PATH. Files' PDF Read reports the command for the target OS at any missing converter stage. PDF extraction and bounded page rendering run where the file lives; Poppler is not bundled in the client archive.

Bundled tools

The client archive ships ripgrep, because the files plugin's Grep and content Search run rg on the paired computer and Windows has none. scripts/build-client.mjs takes --ripgrep with the official BurntSushi/ripgrep release archive for the target, refuses any other file name or a SHA-256 that differs from RIPGREP_RELEASE (version 15.2.0, one pinned hash per target, Linux the static musl build), checks that the binary's ELF, Mach-O or PE header matches the target, and writes vendor/ripgrep/rg or rg.exe with the release's COPYING, LICENSE-MIT and UNLICENSE. At runtime bundledRipgrep(packageRoot) names that program, and createLocalExecutor runs it for every rg argv command; a client run from a checkout has no vendor/ and runs the rg on PATH. When no rg runs at all, the tool answers with one text that names the install command for macOS, Debian and Ubuntu, and Windows.