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()returnslinux,darwinorwin32, and throwsElowen does not run on <platform>for anything else.hostArch()returnsx64orarm64, and throwsElowen does not run on <arch> processorsotherwise.
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.taskkillhas 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.mjskeeps its ownprocess.platformchecks (EXLOCK open on Windows, the bundledkernel-lockhelper on macOS,flockon 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.tsfor the same macOS helper, andsrc/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.platformand driveslocalTargetPathinpackages/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.