@comput/pi-servicehost
v0.3.0
Published
Linux-first standalone project-local service runtime and interactive terminal panel
Maintainers
Readme
ServiceHost
A standalone, project-local service runner with a detached HTTP runtime and an interactive terminal panel. Linux-first, not production supervision. Start services explicitly, reconnect from another terminal, and leave them running when the panel closes. No Pi SDK, extension, or Herdr dependency.
Linux-first standalone npm CLI, not a pi install extension. Version 0.3.0 adds the compact, keyboard-driven service panel described below. Version 0.2.0 passed Linux CI and npm Trusted Publishing with provenance. Repository: mbundgaard/pi-servicehost. See GitHub releases for publication status and the release checklist for the publishing process.
Install and use
Install the published CLI:
npm install -g @comput/pi-servicehost
pi-servicehost --helpRequires Node.js 22.19+ and npm. Linux is the initial release target, with readable procfs in the same PID/network namespace and cooperative foreground services. macOS and other platforms are unsupported. Windows code is included for experimentation only: native validation and lifecycle work are deferred, not Linux release gates. Windows discovery requires Windows PowerShell and Get-NetTCPConnection; no Windows tree-stop guarantee is provided.
Use version 0.3.0 or newer for the interactive panel described below, or install a locally built tarball (see Development below). A project-local registry installation also works: npm install --save-dev @comput/pi-servicehost, then npx --no-install pi-servicehost --help. No install scripts or runtime dependencies are needed.
Put ServiceHost.json in your project:
{
"apiPort": 9500,
"logDirectory": "./logs",
"services": [{
"name": "api",
"command": "node",
"args": ["server.js"],
"workingDirectory": "./api",
"port": 5000,
"environment": { "PORT": "5000" }
}]
}pi-servicehost up
pi-servicehost panel
# or use independent commands:
pi-servicehost start api
pi-servicehost status
pi-servicehost logs api stderr
pi-servicehost stop api
pi-servicehost shutdownInteractive panel (0.3.0)
panel starts/reconnects the runtime, then opens the compact legacy-style alternate-screen TUI: service rows such as > api running :3000, a full-width separator, selected-service stdout/stderr, then a full-width separator and shortcuts/package version. No title, column headings or persistent connection banner. No services auto-start. Names stay selected across polls; updates never move focus. F2 cycles services; arrows also select. F1 shows PID, readiness, ownership and last exit; these observations are not control authority. Colors follow the terminal palette (NO_COLOR disables styling); selection also has a > marker. Pending/disconnected warnings remain visible while relevant; action results/errors and clear confirmations appear temporarily in log-pane space (eight seconds), with the last message retained in F1.
- ↑/↓ / F2: select previous/next service (F2 wraps).
- F1 / Esc: open/close info and full key help (↑/↓ scroll info).
- F3: start/stop selected service; F4: restart via the existing authenticated API. Runtime ownership/lease checks still apply; observe-only and known lost-ownership services have no controls. One action at a time; ambiguous results are never retried automatically. Inspect status before deciding to retry.
- F6: hide/show service list. Ctrl+L: clear selected log view only, never truncate/delete log files.
- q / Ctrl+Q / Ctrl+C: quit UI only. Terminal modes/cursor/screen are restored on normal exit and handled termination signals. Services and runtime remain running; quitting does not undo an action already sent. F5 batch operations are deliberately omitted.
The panel keeps at most 500 log rows per cached service (4096 characters per row, at most eight service caches; revisiting an evicted service reattaches to its tail), follows both streams with separate per-launch byte cursors, and reads at most 8 KiB per stream per poll. Initial attachment shows a bounded tail, not all history. Rows are clipped to terminal width; logs/follow remain available for inspection. Stream labels distinguish output; there is no reliable cross-stream chronological order. Complete ANSI sequences are stripped and residual controls escaped: child output cannot drive the terminal. Ordinary Unicode is preserved with grapheme-aware, approximate wide-character layout (terminal emoji/ambiguous-width conventions and split UTF-8 byte boundaries can differ). Logs may contain secrets.
Reads and controls share one serial lane; reads refresh one second after each cycle, requests are bounded to 20 seconds. On disconnect, cached content is marked stale, controls are disabled and only reads reconnect automatically. Ctrl+L discards any in-flight old chunk while advancing its cursor; clearing before the first read discards that first tail. Under 32×8 the panel requests a resize. No mouse, scrollback browser, adoption or destructive maintenance is added.
panel --prompt retains the command prompt; panel --live retains the line-oriented compact ledger. watch attaches without launching a runtime; follow api [stdout|stderr] follows one stream. These older views remain line-oriented and JSON-escape logs; q + Enter, EOF or Ctrl-C ends the view. Unlike the panel, their request errors exit. Non-TTY watch/follow return one JSON snapshot and exit; all panel modes require a TTY. Machine-readable commands/API are unchanged. lastExitCode persists across launches within this runtime (? unknown, no-code a nonnumeric exit); exitCode remains current-launch only.
You can launch the panel manually in a Herdr terminal. Nothing configures or manipulates Herdr. See panel design and evidence; visual approval and native Windows terminal testing are not claimed.
Every command accepts --config /path/to/ServiceHost.json. Config directories and executable paths containing separators resolve relative to the config file; bare executable names use PATH. Native executable arguments are passed without shell expansion and are interpreted by the child relative to its working directory. Windows batch launches use the restricted grammar below. port is optional and is TCP discovery, not a readiness or health assertion. Services never auto-start.
Configuration is read at runtime startup. Stop services and shut down the runtime before editing configuration, especially apiPort. There is no hot reload. Duplicate names/ports and sharing the management port are rejected.
Optional startup readiness (0.2.0)
Add to a service, explicitly:
"readiness": { "port": 5000, "path": "/ready", "expectedStatus": 200, "startupTimeoutMs": 10000 }Only HTTP GET to literal 127.0.0.1 is supported. All four fields are required: port 1–65535 (not the management port), status 200–599, startup deadline 100–120000 ms, path up to 1024 characters starting with a single / and containing only ASCII letters/digits or _ . / ~ -. No hosts, credentials, query strings, fragments, redirects, TLS, DNS or custom headers. Choose a side-effect-free local endpoint; do not place secrets in its path. The probe port can differ from discovery port, and its response is not proof that the managed process owns that listener.
Status separates process state/ownership from readiness: not-configured, inactive, pending, ready, or timed-out. A launch starts probes asynchronously; start acknowledges process launch, not readiness. Probes run sequentially with up to 1 second per request and 250 ms between attempts, bounded by the startup deadline. Only headers (8 KiB limit) are inspected; bodies are not collected. Success is an expected status observed before the deadline, not ongoing health monitoring. Exit, explicit stop request (even if stop fails), and anchor closure cancel the launch's observation. Restart uses a fresh observation; stale callbacks cannot mark the new launch ready. No response text, endpoint, errors or credentials are returned in status. Timeout never signals, restarts, releases ownership, or clears a lease. Adopted/stale services are not probed.
Credentials and logs
For ${credential:apiKey} in service args/environment, create a separate ServiceHost.credentials.json alongside the config:
{ "apiKey": "your-local-value" }Placeholders resolve only when launching. Prefer environment variables over arguments (arguments may be visible to OS process inspection). There is no shell expansion, credential editor, vault, or automatic redaction. Do not put secrets in commands or directories. Missing/invalid credentials block launch.
Child output may contain secrets, including expanded credentials. The authenticated logs endpoint deliberately returns raw child output in JSON. Terminal rendering JSON-escapes control characters; it does not remove secrets. API status/discovery never returns arguments, environment, credentials, or executable paths. Launch errors are sanitized.
Add these patterns to your project's .gitignore:
.servicehost/
logs/
ServiceHost.credentials.jsonAlso ignore any custom log directory. Protect your project and credentials with owner-only permissions/Windows ACLs. Created state/log files request POSIX modes 0700/0600; Node's mode flags do not establish Windows ACLs. The runtime inherits its launcher's environment and services inherit it too.
Each launch opens two unique files, NAME-ID.stdout.log and .stderr.log, with exclusive creation and passes their file descriptors directly to the child. No existing log is ever truncated, including reconnect/adoption. Logs are not rotated or pruned. The API tails up to 64 KiB from the current runtime's launch; old files remain on disk but are not automatically reattached.
New in 0.2.0: log-inventory [LIMIT] is an opt-in dry-run only inventory, not a cleanup command. It scans at most LIMIT directory entries (default 100, range 1–1000), without recursion, and reports sizes/basenames only for known service names with exact unique-launch filenames. Symlinks, directories, hard-linked files, unknown services and other filenames are excluded. truncated means the scan was incomplete; order is filesystem-dependent, with no pagination or whole-directory size claim. Every reported file is retained-inactivity-unproven, including stopped/historical launches. Group drain, file age and missing leases cannot prove escaped processes released inherited descriptors. Deletion/retention enforcement is intentionally deferred until positive inactivity can be established; this command never deletes or truncates anything. Do not treat the inventory as permission to remove files.
Ownership and recovery
- Start refuses an existing configured TCP listener, including an uninspectable listener. A port probe cannot eliminate a bind race; a started child is not necessarily ready.
discover apireturns PID plus opaque process identity.adopt api PID IDENTITYrechecks both listener and process identity. Adoption is observe-only: output unavailable, stop/restart forbidden. No automatic adoption or destructive action against discovered PIDs.- Linux: each launch has a private Node anchor in a new session/process group. The anchor retains membership after the service parent exits; stop asks this live anchor to send SIGTERM to its own group, never a saved numeric PGID. It ignores TERM itself, waits up to five seconds for group drain, then exits. No forced escalation; timeout, anchor loss, inspection failure or a remaining configured listener retains the lease and blocks restart. Shutdown refuses unreleased ownership, including after natural parent exit.
- Windows: stop remains direct-child-only using the retained Node handle (not graceful POSIX SIGTERM); batch leases remain fenced. No Job Object helper/binary is shipped.
- Group scope is not whole-tree containment. Linux descendants that use
setsid/setpgidcan escape. Procfs drain verification uses two empty non-zombie observations, not an atomic kernel tree-empty operation; hostile fork/exit churn is not supported. Prefer cooperative foreground services. Zombies cannot run or write logs and are excluded from drain; reaping orphan zombies belongs to the system init/subreaper. Unique logs protect files still inherited by escaped descendants. - A persisted per-service launch lease blocks re-launch after runtime loss. There is no PID-based reconnect or automatic stale-lease clearing. Natural service-parent exit also retains the lease until an explicit
stopconfirms the platform ownership outcome and configured port is free. Windows batch-wrapper leases require external reconciliation even afterstop(see below). - After a crash, externally inspect the saved lease, OS processes and descendants, and logs. Only after independently confirming termination and ensuring no other runtime for that config is active, remove the particular
.servicehost/NAME.lease.json. This is manual recovery, not a verified recovery command. Never delete leases to bypass uncertainty. shutdownrefuses while any managed child is live or Linux group ownership is unreleased. Runtime death disconnects the Linux anchor, which exits without signaling services; surviving processes continue direct-file logging. No new runtime regains signal authority from a lease. There is no crash cleanup guarantee, reboot recovery, autostart, watchdog, Job Object, pidfd, or system service installation.
Windows launching (experimental; validation deferred)
Native .exe/.com images use Node's normal argument quoting, never shell:true or verbatim arguments. Prefer node.exe with the installed package's CLI .js file in args, especially for credentials or complex arguments. Individual native programs can implement nonstandard argument parsing.
Bare names resolve on the child's PATH, trying .exe, .com, .cmd, then .bat per directory; no implicit current-directory search, relative PATH entries, or custom PATHEXT handlers. Supply ./script.cmd for a config-relative script. Explicit extensions must be one of those four. Files are resolved before spawning; trusted local files/PATH are required, not protected against malicious file replacement.
Experimental, restricted .cmd/.bat launching (including npm/pnpm/yarn shims when found on PATH) invokes the system cmd.exe /d /s /v:off /c with a deliberately closed grammar:
- Batch path: local drive-absolute ASCII letters/digits, spaces, underscore, dot, dash and path separators; no UNC/device paths, shell syntax, or components ending in space/dot.
- Each argument: nonempty ASCII letters/digits or
_ . / : + -(the spaces in this list are separators, not allowed characters). No whitespace, quotes, backslashes, comma, equals, semicolon,%,!, shell operators, controls or Unicode. For exampleargs: ["run", "dev", "--", "--port", "3000"];"--port=3000"is rejected because batch parameter parsing treats equals as a separator. - Expanded credentials are checked against the same grammar; errors do not echo rejected values. Complex input must use a native executable/JS entrypoint or environment variables. Batch script contents remain trusted executable code, responsible for their own environment expansion and forwarding.
- Command text is capped at 8000 characters. There is no unsafe quoting fallback or ComSpec override.
Batch lifecycle is wrapper-only. Stop can terminate only the owned cmd.exe, not its launched program. Its lease always remains: stop reports that external descendant reconciliation is required; restart is blocked even if no port is configured/listening. After independent termination verification, shut down the runtime and follow manual lease recovery above. This intentionally limits batch convenience until reviewed Job Object ownership exists. Explicitly configuring your own shell/wrapper does not confer tree guarantees.
Planner tests run on Linux; actual Windows argv/batch/PATH/log/lease tests are supplied but were not run locally. This is not a claim of complete Windows support. See native ownership design/blockers.
Local control API
The runtime binds only 127.0.0.1:apiPort. All requests use POST /v1, exact Content-Type: application/json, Authorization: Bearer TOKEN from .servicehost/token, and X-ServiceHost-Project: ID (SHA-256 of the canonical config path, lowercased on Windows). Browser Origin requests are rejected, no CORS is enabled. Clients must check ping's project and protocol. A conflicting port fails; there is no alternate-port fallback.
Bodies: { "action": "status" }, or { "action": "start|stop|restart|discover", "name": "api" }; logs accepts stream and bytes; adopt takes expected: { pid, identity }; ping and shutdown have no service. 0.2.0 additions: log-inventory takes optional limit and no service; log-follow takes name, stream, bytes (1–65536, default 16384), and an optional per-stream cursor: { launchId, offset }. The first read tails at most bytes; subsequent reads advance by actual bytes read. A new launch or size smaller than the cursor resets to a bounded tail and returns reset: true. Follow does not drain historical launches after restart; fast restarts/backlogs can lose output. UTF-8 characters split across byte reads may render replacement characters. No historical file path can be selected through this API. This is an initial protocol, not a stable public API. Authentication is not a security boundary against other code running as the same OS user. Protect the project directory from untrusted writers/symlinks. Containers/network namespaces and socket activation are not supported discovery environments.
Development (source checkout only)
npm ci
npm run typecheck
npm test
npm run check:package
npm pack
# install the generated comput-pi-servicehost-0.3.0.tgz locally if desired
npm install -g ./comput-pi-servicehost-0.3.0.tgzprepack builds standalone ES modules into dist/. When disabling scripts, run npm run build before npm pack --ignore-scripts; the release workflow does this explicitly. The tarball contains built JS, selected docs, example config and MIT license, not source, tests, legacy references, credentials or runtime state. Tests install the actual tarball with scripts disabled into a disposable prefix and run its CLI outside the checkout.
The checked-in CI workflow targets Linux on Node 22.19 and 24; local validation is not evidence of remote CI success. Windows native validation is deferred; Linux planner tests do not prove Windows execution. Fixtures use disposable projects/processes, never real configured services.
See design and next steps, validation record, changelog and release/setup checklist.
License
MIT — see LICENSE.
