Three separate questions, always answered separately:
- Runtime compatibility: can Vexor launch and supervise the app?
- Behavioural compatibility: does Vexor reproduce the PM2 behaviour the app relies on?
- Security support: does the runtime still get upstream fixes? (Vexor warns on EOL Node and never forces an upgrade.)
Status values: planned (spec only), tested (green in CI), qualified (works with stated limits), unsupported.
Every row below is planned until CI says otherwise.
CI (legacy-node job, every push): static linux-x64 musl binary on node:<v>-alpine for Node 8, 10, 12, 14, 16, 18, 20, 22 and 24, six checks each: ready via process.send('ready'), natural exit with the IPC channel open, graceful SIGTERM, crash restart with crash-loop limit, args/env/cwd/instance variable, readiness timeout. The same checks pass on linux-arm64 (run locally). The full test suite runs in CI on Linux x64, Linux arm64 and macOS arm64 (with the runner's current Node).
Not covered yet: Node 4, 6 and 0.12 (tier 4), cluster mode (Phase 4), and the npm wrapper's own Node range (Phase 3).
| Target | Build | Tier | Notes |
|---|---|---|---|
| linux-x64 (musl, static) | cross | 1 | Runs on any glibc/musl distro. pidfd needs kernel ≥ 5.3; older kernels fall back to start-time re-check. |
| linux-arm64 (musl, static) | cross | 1 | Graviton / ARM VPS |
| darwin-arm64 | native | 2 | Dev machines; launchd for startup |
| darwin-x64 | cross | 3 | Only if CI runner available |
| win32-x64 | — | not in v0.x | Needs named pipes, Job Objects, different signals. Not advertised until tested. |
Startup integration: systemd (Linux), launchd (macOS). OpenRC/upstart/SysV: unsupported in v0.x (PM2 supports them; reported clearly by migrate).
Docker node:<ver> images (pinned by digest) are used for every version listed.
| Node | Tier | CI cadence | Notes |
|---|---|---|---|
| 24, 22, 20 | 1 | every PR | current/maintenance LTS |
| 18, 16, 14 | 2 | every PR (Linux x64 only) | EOL; warning shown |
| 12, 10, 8 | 3 | nightly | EOL; ESM configs unsupported below 12 |
| 6, 4, 0.12 | 4 | weekly, best effort | qualified: fork mode, signals, logs, NODE_CHANNEL_FD readiness; cluster shim to be confirmed |
The per-version test list follows prompt §6.4: HTTP server, worker, exe path selection, args/env/cwd, logs, non-zero exit, crash + backoff, SIGTERM/SIGINT, kill timeout, multi-instance, daemon restart recovery, readiness, missing runtime.
PM2 baseline: PM2 7.x (7.1.0 latest, 7.0.4 installed locally) declares engines.node >=18.0.0. Earlier majors: v2 ≥0.12, v3 ≥6, v4 ≥8.10, v5 ≥12, v6 ≥16. Vexor's behavioural baseline is PM2 7.x semantics, while its runtime baseline reaches as far back as the PM2 v2-era Node versions. Vexor's supervisor has no Node dependency at all.
| Item | Promise |
|---|---|
| Node | ≥ 10: tested in CI on 10, 12, 14, 16, 18, 20, 22, 24 (Linux x64) and 24 (macOS arm64) |
| npm | ≥ 6: tested with npm 6, 8, 10, 11 (global install; optional platform package resolution) |
| Package managers | npm, pnpm, yarn classic; yarn berry PnP = qualified |
| Below the floor | direct binary install: release tarball + SHA-256 + install.sh |
The wrapper is a zero-dependency ES5 script with no postinstall and no network access.
| PM2 | Vexor | Notes |
|---|---|---|
| start / stop / restart / delete | same | accepts name, id, all, config file |
| reload | same | Implemented. Cluster mode: start new → wait healthy → retire old gracefully; a failed replacement keeps the old process and reload exits 1 naming the instances. Fork mode: restart by default (as PM2); reload_strategy: start_first for apps that can run twice (no fixed port, or reusePort) |
| list / ls / status | same | --json stable schema |
| logs | same | Implemented: --lines, --follow, --raw, --err/--out; all apps when no name, merged into one timeline by time: HH:MM:SS.mmm app │ line (stderr marked; JSON logs shown as level, message, fields). --raw prints files as written |
| describe / show | same | Implemented: env values hidden unless --show-env; --json |
| save / resurrect | same | Implemented. Vexor already restores apps from its own state whenever the daemon starts; save writes an explicit snapshot (dump.json, 0600) and resurrect restores from it (adds missing apps, starts those saved running, leaves differently configured apps alone) |
| startup / unstartup | same | Implemented for systemd (system unit, needs root: prints the sudo command otherwise) and launchd (per-user LaunchAgent). Stopping the service stops only the daemon, never the apps. OpenRC/upstart/SysV: not supported |
| scale | same | cluster mode |
| flush / reloadLogs | same | |
| monit | vexor top |
simple TUI later |
| — | migrate --from-pm2 | Dry run, backup, apply, rollback and status implemented (docs/migration.md) |
| deploy, install, link, plus, ecosystem gen | — | unsupported, explicit error |
Install: npm i -g vexord (provides the vexor command).
Implemented so far (Phase 2.5): start (script, existing name, all, or --spec), stop, restart, delete/del, list/ls/status (--json). Differences from PM2 so far:
startpasses only an environment allowlist (PATH, HOME, USER, LOGNAME, SHELL, LANG, LC_*, TZ, TMPDIR, NODE_ENV) plus--env KEY=VALUE;--inherit-envpasses the whole shell environment like PM2.startof a name that exists with a different configuration is an error instead of silently keeping the old one..js/.cjs/.mjsrun with thenodefound on the caller's PATH, resolved to an absolute path at start time.
Exit codes: 0 ok, 1 command failed, 2 usage error, 3 daemon unreachable, 4 partial success (some targets failed).
Implemented in crates/vexor-pm2 (4.1). vexor check ecosystem.config.js prints every finding without starting anything; vexor start refuses to start while any fatal finding remains, unless --allow-unsupported. Current classification (supersedes the table below where they differ):
- fatal (until implemented):
exec_mode: clusteron a non-Node script,shutdown_with_messageon a non-Node app,uid/gid/usernamewhen Vexor doesn't run as root,.ts/.coffeewithoutinterpreter, function/RegExp values,wait_readyon a non-Node app, malformed values. - partial:
merge_logs(instances always share log files),log_file(no combined log),listen_timeoutwithoutwait_ready. - ignored (reported):
watch_options(chokidar-specific), PM2 Plus/monitoring fields,namespace,pid_file,kill_retry_time,force,log_type,time,log_date_format,filter_env,windowsHide, unknown fields. instanceswithoutexec_moderuns N fork-mode processes (they can't share a port).args/node_argsstrings are split shell-style (quotes, backslashes) with no expansion. Environment: caller allowlist (or--inherit-env), thenenv, thenenv_<NAME>. Relativecwdresolves against the ecosystem file's directory andscriptagainstcwd(to confirm against PM2 in the conformance suite).
| Class | Fields |
|---|---|
| supported | name, script, cwd, args, interpreter/exec_interpreter, node_args/interpreter_args, env, env_*, instances, exec_mode, autorestart, max_restarts, min_uptime, restart_delay, exp_backoff_restart_delay, kill_timeout, wait_ready, listen_timeout, shutdown_with_message, stop_exit_codes, out_file, error_file, log_file, merge_logs, log_date_format, time, disable_logs, namespace, instance_var, increment_var, treekill, max_memory_restart, pid_file, autostart, append_env_to_name, kill_retry_time, filter_env, name_prefix |
| partial | watch, ignore_watch, watch_delay, watch_options (no chokidar-specific options); log_type (json supported, format may differ); windowsHide (Windows only); cron_restart (phase 6) |
unsupported-fatal (unless --allow-unsupported) |
uid, gid, username (privilege drop: phase 6, needs a security review), cron_restart (until implemented), force |
| ignored-safe (warned) | vizion, pmx, automation, trace, disable_trace, v8, event_loop_inspector, deep_monitoring, source_map_support, disable_source_map_support, install_url, post_update, execute_command, port, write, io |
"Ignored-safe" means ignoring the field doesn't change process lifecycle. It is still reported every time.
- Cluster mode runs through Vexor's cluster primary on the app's own Node (not inside a Node daemon).
NODE_APP_INSTANCE(orinstance_var) is set per worker;pm_idis not set. Workers share the primary's process group, sotreekillcleans up a worker's own child processes only when the whole app stops, not when one worker restarts. If the primary dies, all workers are restarted. pm2programmatic API (require('pm2')) is not provided.- With
treekill(default true), processes left in the app's process group after its main process crashes get SIGTERM, then SIGKILL afterkill_timeout, before the restart. PM2 only kills the tree on stop/restart, so leftovers from a crash can keep ports or queue messages.treekill: falseleaves them alone, as PM2 does. - On stop, Vexor waits for the whole group, not just the main process, up to
kill_timeout, then SIGKILLs what is left. - Linux only: descendants that escape the group (
setsid, double fork) are adopted by the shim (child subreaper), reaped when they exit, reported asorphanevents, and stopped when the shim shuts down. macOS: such processes re-parent to launchd and Vexor can't track them. - Automatic restarts are limited to one per second per instance (spawn to spawn). PM2 restarts immediately with
restart_delay: 0. listen_timeoutexpiry without a ready signal is a failed start (stopped, counted as unstable, restarted with backoff). PM2 marks the app online anyway.max_restarts: Nmeans at most N runs in a row that end beforemin_uptime, then errored, as in PM2 7 (verified by the conformance harness).max_restarts: 0in an ecosystem file means PM2's default, 16.- Graceful stop sends SIGINT by default, as PM2 does (verified); set
kill_signal(e.g."SIGTERM") to change it. Leftover processes after a crash still get SIGTERM, then SIGKILL. max_memory_restart(bytes, or K/M/G as in PM2): memory is checked every 5 s (PM2: every 30 s), so a leaking app is restarted sooner. As in PM2, the resident memory (RSS) of the instance's own process is measured (one worker in cluster mode), not its children, and one reading above the limit triggers a graceful restart. Each is recorded as amemory_restartevent with the measured size.vexor start --max-memory-restart 200Mfor scripts.cron_restart: same patterns as PM2 7 (itscronerlibrary, via the Rust port: 5 fields, or 6 with seconds first; local time). As in PM2 (verified), every instance is restarted on schedule, and an app that exited (e.g. a job withautorestart: false) is run again. Difference: PM2 also starts an app the operator stopped (pm2 stop) at its next scheduled time (verified with PM2 7.1.0); in Vexor,vexor stopholds untilvexor start. Each run is acron_restartevent.vexor start --cron-restart "0 3 * * *"for scripts.watch(true= the app's working directory, or paths relative to it),ignore_watch,watch_delay, as PM2 7 (lib/Watcher.js): dot-files andnode_modulesare ignored unlessignore_watchreplaces that list; on the first change Vexor waitswatch_delay(at least 100 ms, so one save is one restart), then restarts every instance. Differences:ignore_watchentries are glob patterns, matched against the path relative to the watched directory, each directory above it, and each path component (PM2 passes them to chokidar, which also takes regular expressions; regex-looking entries are reported); the app's own log files are always ignored; one watcher serves all instances (PM2 runs one per instance, so an app with N instances restarts N times per change); symbolic links are not followed. Linux uses inotify; macOS compares the tree every second (up to 100,000 entries).vexor start --watchfor scripts.health_check(Vexor extension; PM2 has none outside PM2 Plus):{ http: "http://127.0.0.1:3000/health" | tcp: "127.0.0.1:3000", interval: "10s", timeout: "2s", unhealthy_after: 3, restart_after: 6 }. HTTP passes on 2xx/3xx, TCP on an accepted connection; only loopback targets (no TLS). Fork mode: each instance is checked ({instance}in the address is replaced by the instance number); withoutwait_ready, an instance is ready when its first check passes (withinlisten_timeout, default 30 s); afterunhealthy_afterfailures in a row it isunhealthy, afterrestart_afterit is restarted. Cluster mode: workers share the port, so the app is checked as a whole; afterrestart_afterfailures it is reloaded (new workers first), counting only once the app has passed a check, so a check that never passes doesn't cause endless reloads. Changes between passing and failing arehealthevents.vexor start --health-check URL|ADDR.routes(Vexor extension; PM2 has per-route metrics only in PM2 Plus, through@pm2/ioin the app): per-route requests, errors and latency read from the app's JSON request logs, on by default.routes: falseturns it off;{ route, method, status, duration, duration_unit, max_routes }maps fields. See routes.md.- Log timestamps: Vexor starts every plain-text log line with the local time it was received (
2026-10-23T15:17:05.344+05:30), sovexor logscan merge apps into one timeline; JSON lines (starting with{) are left as written. PM2 writes timestamps only withtime: trueorlog_date_format; in Vexortime: falseturns them off, andlog_date_formatis noted (Vexor's format is fixed). @pm2/iometrics/actions are not collected.- Log rotation is built in and on by default: 10 MiB per file, 5 rotated files (PM2 needs
pm2-logrotate, whose defaults are 10 MB and 30 files). - When an app writes output faster than the disk can take it, Vexor discards the excess by default and says so in the log and in a
log_droppedevent, so the app never waits on logging.logs.overflow: blockmakes the app wait instead (nothing lost). describeredacts env values.