Sign in

    pm3

    A language-agnostic process manager (PM2-like) with built-in log rotation

    process-manager
    pm2
    supervisor
    daemon
    logrotate
    log-rotation
    cli
    Download zip
    Author
    Version
    0.1.7
    License
    Apache-2.0
    Last updated
    7 days ago
    Downloads
    27

    #pm3

    pm3 is a small, language-agnostic process manager with built-in log rotation, written in MoonBit. It is inspired by PM2 and by the pm2-logrotate module, but it manages any background service — a shell script, a Python worker, a Go daemon, a Node app — not just JavaScript runtimes.

    pm3 start ./worker.sh --name worker pm3 start ./worker.py --interpreter python3 --name py-worker --cwd /srv/app pm3 list pm3 logs worker pm3 stop worker

    #Highlights

    • Any executable. pm3 supervises an arbitrary command line, and resolves interpreters the way PM2 does — .js through node, .py through python, binaries directly — with --interpreter and --interpreter-args to pin either. See Interpreters.
    • Crash recovery. Automatic restarts with min_uptime / unstable-restart tracking, restart budgets, restart delays, stop_exit_codes and max_memory_restart.
    • PM2's look. list, describe and logs use PM2's colors, table borders, log prefixes and --help layout, so the two tools read the same — see Terminal colors.
    • Built-in, optimized log rotation. No external module, no logrotate cron job. Rotation is an O(1) rename + reopen performed by the daemon that owns the file descriptors — see Log rotation.
    • Background gzip. Rotated files are compressed asynchronously, so a multi-gigabyte log never blocks a worker.
    • A real daemon. The CLI talks to a detached supervisor over a loopback socket; the daemon owns child stdout/stderr pipes and keeps running after the terminal closes.
    • Single static binary. One executable provides both the client and the daemon (pm3 daemon is the internal entry point).

    #Install

    # npm, pnpm, yarn or bun. The launcher downloads the native binary for this # platform on first run; nothing is downloaded at install time. npm install -g @tomasko/pm3 # From the MoonBit package registry, if the MoonBit toolchain is already # installed. Builds from source and installs `pm3` to ~/.moon/bin. moon install kupas/pm3/cmd/pm3 # Install script, no Node.js required. curl -LsSf https://raw.githubusercontent.com/tomasky/pm3/master/install.sh | bash # Or build from source, see below.

    The Linux builds are linked statically against musl, so one binary runs on any distribution regardless of its glibc version.

    PlatformRelease asset
    Linux x86-64pm3-linux-x86_64 (static)
    Linux arm64pm3-linux-aarch64 (static)
    macOS Apple siliconpm3-darwin-aarch64

    The npm package ships a launcher rather than the binary, and deliberately declares no postinstall. Since npm 12 dependency install scripts are blocked unless the user opts in, and pnpm 12 fails a project install outright with ERR_PNPM_IGNORED_BUILDS when a dependency declares one:

    $ pnpm add @tomasko/pm3 Error: ERR_PNPM_IGNORED_BUILDS × adding a new package ╰─▶ Ignored build scripts: @tomasko/pm3@0.1.1

    Downloading lazily from the launcher works under npm, pnpm, yarn, bun, npx and --ignore-scripts, because it is an ordinary command rather than an install hook. See npmjs/README.md for the environment variables that redirect the download (PM3_DOWNLOAD_BASE, PM3_BINARY, …).

    The install script takes --version, --install-dir and --no-verify, and verifies the published SHA-256 before installing.

    #Build

    moon build --target native --release # the binary is at _build/native/release/build/cmd/pm3/pm3.exe

    Copy it somewhere on $PATH as pm3. During development the debug build works too:

    moon build --target native ./_build/native/debug/build/cmd/pm3/pm3.exe list

    #Static Linux build

    scripts/build-linux-static.sh produces a fully static binary with musl, which is what the release workflow publishes. A glibc build records the build machine's symbol versions, so a binary built on a new runner refuses to start on an older distribution (GLIBC_2.38 not found); linking against musl statically removes that entire class of problem.

    sudo apt-get install musl-tools ./scripts/build-linux-static.sh dist/pm3-linux-x86_64

    Run the unit tests with:

    moon test --target native

    #Releases

    .github/workflows/release.yml builds one binary per target on its own runner (MoonBit's native backend compiles through the system C compiler, so it is not cross-compiled), smoke-tests it, and attaches the binary plus a .sha256 to a GitHub Release — two assets per target, no archive, because both installers fetch the bare binary and set the mode themselves. It then publishes @tomasko/pm3 to npm, after the release exists, so the asset is there for the first user to pull. Pushing a tag is the whole trigger:

    git tag v0.1.1 && git push origin v0.1.1

    The npm job is skipped when NPM_TOKEN is not configured, so a fork can still cut releases.

    #Quick start

    # Start a service. The name defaults to the script's basename ("worker"). pm3 start ./worker.sh # Run a non-executable script through an interpreter. pm3 start ./jobs/digest.py --interpreter python3 --name digest # The extension already picks the interpreter (.js -> node); arguments for # the interpreter itself go before the script, as PM2 places them. pm3 start ./app.js --interpreter-args "--trace-warnings --max-old-space-size=512" # Pass arguments to the managed process after `--`. pm3 start ./server --name api -- --port 8080 # Environment variables. pm3 start ./server --name api -e PORT=8080 -e LOG_LEVEL=info # Inspect it. The columns are PM2's — id, name, namespace, version, mode, pid, # uptime, restarts (↺), status, cpu, mem, user, watching — and the footer # totals the running processes. pm3 list # ┌────┬────────┬───────────┬─────────┬──────┬──────┬────────┬───┬────────┬───────┬────────┬──────┬──────────┐ # │ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │ # ├────┼────────┼───────────┼─────────┼──────┼──────┼────────┼───┼────────┼───────┼────────┼──────┼──────────┤ # │ 0 │ digest │ default │ N/A │ fork │ 4242 │ 6s │ 0 │ online │ 86.9% │ 11.3mb │ root │ disabled │ # │ 1 │ worker │ default │ 2.5.1 │ fork │ 4243 │ 6s │ 0 │ online │ 0% │ 2.0mb │ root │ disabled │ # └────┴────────┴───────────┴─────────┴──────┴──────┴────────┴───┴────────┴───────┴────────┴──────┴──────────┘ # # 2/2 online total cpu 86.9% total mem 13.3mb pm3 describe api # Describing process with id 0 - name digest # ┌────────────────────┬───────────────────────────────────┐ # │ pm_id │ 0 │ # │ name │ digest │ # │ status │ online │ # │ pid │ 21557 │ # │ command │ python3 ./digest.py │ # │ cwd │ <inherited> │ # │ out log │ /root/.pm3/logs/digest-out.log │ # │ memory │ 11.3mb │ # │ cpu │ 86.9% │ # └────────────────────┴───────────────────────────────────┘ pm3 logs digest # Stop it, remove it. pm3 stop api pm3 delete api

    Or declare the services in a pm3.toml and bring them all up at once — see Application config.

    pm3 start # reads ./pm3.toml pm3 config apps # show what that would start

    #Configuration

    pm3 reads TOML and JSON only. Anything else — YAML, INI, a .js file — is refused with an error rather than ignored.

    There are two things to configure, and they live in the same file format:

    What it describesWhere pm3 looks
    ApplicationsWhich processes to run, and how$PM3_CONFIG, then ./pm3.toml, ./pm3.json, then the global file
    Log rotationThe rotation policy, shared by every processthe global file only

    The global file is $PM3_HOME/config.toml (or config.json) — by default ~/.pm3/config.toml. It is global because the daemon is: one supervisor owns every process on the machine, so one rotation policy applies to all of them. A legacy ~/.pm3/logrotate.json is still read.

    pm3 config prints the global file and its contents, pm3 config init toml (or json) creates one seeded with the current policy, and pm3 config apps prints the app list pm3 would start.

    #Application config

    This is the pm3 equivalent of PM2's ecosystem.config.js: a project checks in pm3.toml, and pm3 start brings the whole thing up.

    # pm3.toml [defaults] # optional, applied to every app below max_restarts = 16 max_memory_restart = "200M" [[app]] name = "worker" script = "./worker.py" interpreter = "python3" args = ["--verbose"] env = { PORT = "8080", LOG_LEVEL = "info" } [[app]] name = "api" script = "./bin/server" max_memory_restart = "1G" # overrides the default for this app only stop_exit_codes = [0, 2]

    The same document in JSON:

    { "defaults": { "max_restarts": 16 }, "app": [ { "name": "worker", "script": "./worker.py", "interpreter": "python3" } ] }

    Every start option has a config key of the same name:

    KeyType
    namestring (default: the script's basename without its extension)
    scriptstring, required
    namespacestring (default default); pm3 stop\|restart\|delete <namespace> acts on all of its apps
    argsarray of strings
    interpreterstring (exec_interpreter is accepted as PM2's alias)
    interpreter_argsstring (split on whitespace) or array; PM2's node_args and interpreterArgs are accepted too
    cwdstring (default: the config file's directory)
    envtable of scalars
    autorestartbool
    max_restarts, restart_delay, min_uptime, kill_timeoutinteger
    max_memory_restartsize ("200M", "1G", or bytes)
    kill_signalstring
    stop_exit_codesarray of integers
    merge_logsbool
    out_log, err_logstring
    log_date_formatstring

    An unknown key is an error, not a shrug — a misspelled max_memory_restart fails immediately instead of silently leaving the limit off.

    Paths are resolved against the config file, not the working directory. A checked-in pm3.toml therefore behaves the same whether pm3 is run from the repository root, a subdirectory, or a cron job. cwd defaults to the config file's directory for the same reason.

    A script with a slash in it, or one that names a file next to the config, is a path. A bare name that is not a file — sh, python3, go — is left alone and looked up on PATH, exactly as pm3 start sh would.

    pm3 start # ./pm3.toml, or the global file pm3 start --config prod.toml pm3 start pm3.json # a positional .toml/.json is a config file pm3 config apps # show what that would start, and validate the file

    Apps that are already online are left running, so re-running pm3 start after adding one app to the file is not disruptive; everything else is started, and a failure in one app does not abort the rest of the file.

    pm3 start <script> ... is unchanged: a single script is still started directly, and options after the script are passed to the program, so pm3 start sh -c 'echo hi' works. Use --config when the ambiguity matters.

    A single script is resolved against the directory the command was run in, the way PM2 resolves it in the CLI — the daemon's working directory is never consulted. pm3 start app.js therefore records /here/app.js and runs in /here whether or not the daemon happens to live there, and the saved record keeps working after pm3 update restarts the daemon from somewhere else. A bare name that is no file at all is still left for the daemon: it may be a managed process (pm3 start worker) or a command looked up on PATH.

    #Interpreters

    PM2 never runs a script naked unless the file is a real executable, and pm3 makes the same decisions:

    1. An explicit --interpreter <cmd> (or interpreter = "..." in a config file) wins.
    2. Otherwise the extension picks the interpreter, from PM2's map: .js → node, .py → python, .sh → bash, .rb → ruby, .php → php, .pl → perl, .coffee → coffee, .ls → lsc, .ts / .tsx → bun.
    3. Otherwise a binary runs directly (shown as <none> in pm3 describe), and any other file — a text file with no extension — is handed to node, the way PM2 falls back to its own runtime.

    A missing interpreter fails the way PM2 reports it, rather than crashing:

    $ pm3 start ./worker.rb Interpreter ruby is NOT AVAILABLE in PATH. (type 'which ruby' to double check.)

    Arguments meant for the interpreter itself are passed with --interpreter-args (--node-args is PM2's alias; both are accepted in config files, under interpreter_args, node_args or interpreterArgs):

    pm3 start ./app.js --interpreter-args "--trace-warnings --max-old-space-size=512"

    interpreter_args = "--trace-warnings --max-old-space-size=512" # split like a shell interpreter_args = ["--trace-warnings", "--max-old-space-size=512"]

    The string form is split on whitespace, with single and double quotes grouping a run of spaces into one argument; the array form is taken as is. The arguments are placed before the script — node --trace-warnings app.js — so they reach process.execArgv. pm3 describe shows them in the command it prints.

    Python interpreters additionally get PYTHONUNBUFFERED=1, so their output appears in the logs as it is written instead of at exit, and a machine whose python does not exist falls back to python3. PM2's exec_interpreter is accepted as an alias of interpreter.

    pm3 start <name> for a process that is already known brings it back up from its saved launch specification, the way pm2 start <name> does: the interpreter and arguments it was created with are reused, rather than name being taken as a script path. pm3 start all does the same for every process in the list, the way pm2 start all does.

    #Commands

    CommandDescription
    pm3 start <script> [options] [-- args...]Start and supervise a process
    pm3 start [--config <file>]Start every app a config file declares
    pm3 list (aliases ls, ps, status)Table of managed processes with PM2's columns, plus running/total counts and total CPU and memory
    pm3 describe <name\|id> (aliases desc, info, show)Full configuration and runtime state
    pm3 stop [name\|id\|namespace\|all]Stop one process, a whole namespace, or all when all (or no name) is given
    pm3 restart [name\|id\|namespace\|all]Restart one process, a whole namespace, or all (resets the restart counter)
    pm3 delete [name\|id\|namespace\|all] (alias del)Stop and remove processes; a namespace name removes all of its apps
    pm3 logs [name\|id\|namespace] [--lines N] [--nostream] [--timestamp [fmt]] [--out] [--err] [--raw]Tail the logs, then stream new lines (PM2's behaviour); --nostream keeps the tail alone
    pm3 flush [name]Truncate the live log(s)
    pm3 reloadLogsRotate every managed log now
    pm3 logrotateShow the rotation policy
    pm3 set logrotate:<key> <value>Change the rotation policy
    pm3 configShow the active config file and its contents
    pm3 config init [toml\|json] [--force]Create a config file
    pm3 config apps [--config <file>] [--json]Show what pm3 start would start, without starting it
    pm3 savePersist the process list to dump.json
    pm3 resurrectStart the processes that were desired online
    pm3 updateRestart the daemon with this build, then resurrect what was online
    pm3 killStop the daemon (and its processes)
    pm3 versionPrint the version

    #start options

    OptionMeaning
    --config <file>Read the app list from a pm3.toml / pm3.json instead
    -n, --name <name>Process name (default: script basename)
    --namespace <ns>Start the app within the given namespace (default default)
    -i, --interpreter <cmd>Run <script> as an argument to <cmd>
    --interpreter-args <arguments>Arguments for the interpreter, before the script (--node-args is PM2's alias)
    --cwd <dir>Working directory
    --merge-logsSend stderr into the stdout log
    --out-file <path>stdout log path
    --err-file <path>stderr log path
    --no-autorestartDo not restart the process when it exits
    --max-restarts <n>Restart budget (0 = unlimited)
    --restart-delay <ms>Delay before each restart
    --min-uptime <ms>Runs shorter than this count as "unstable"
    --max-memory-restart <size>Restart when RSS exceeds <size>
    --kill-timeout <ms>Grace period before SIGKILL
    --kill-signal <signal>Graceful signal (default SIGINT)
    --stop-exit-codes <a,b>Exit codes that should not trigger a restart
    --timePrefix every log line with PM2's default YYYY-MM-DDTHH:mm:ss timestamp
    --log-date-format <fmt>Custom moment-style timestamp prefix for log lines (wins over --time)
    -e, --env KEY=VALUESet an environment variable (repeatable)

    #Terminal colors

    pm3 list, pm3 describe and pm3 logs use PM2's palette: grey table borders, bold-cyan headers and ids, green online, blue launching, yellow restarting, red for anything failed, and the blue PM3 | prefix on daemon log lines. pm3 help is laid out the way commander prints PM2's help, and pm3 logs prefixes every line with the same id|name | column PM2 uses.

    The totals under pm3 list follow PM2's footer spec: a bold-cyan label, bold metric names, and a CPU total that turns green, bold yellow and bold red as it crosses PM2's 40%/70% thresholds. pm3 logs streams new lines the way pm2 logs does — --nostream keeps the tail alone — and its --timestamp [fmt] adds PM2's dim-grey current-time prefix to streamed lines.

    Colors are emitted only when standard output is a terminal, so piping pm3list into a file or a log collector gives plain text. FORCE_COLOR=1 forces them on (a non-empty FORCE_COLOR=0 forces them off), NO_COLOR turns them off, and pm3 --no-color <command> does the same for one invocation.

    #Exit status

    A command that could not do what it was asked to exits with status 1 — a pm3 start whose script or interpreter is missing, pm3 describe, pm3 stop and friends for an unknown process, an unknown option or command, or a config file whose apps failed to start. Like PM2, pm3 start of a config file still launches the apps that can start and then exits non-zero, and running pm3 with no command prints the usage block and exits 1. Successful commands — and --help / --version — exit 0.

    #How it works

    pm3 <command> pm3 daemon ┌────────────────────┐ ┌──────────────────────────┐ │ client package │ loopback │ daemon package │ │ parse CLI │ ─────────► │ accept + dispatch │ │ connect to daemon │ newline- │ supervisor (per app) │ │ print results │ delimited │ log sinks + rotation │ └────────────────────┘ JSON │ gzip + prune workers │ │ resource monitor │ └────────────┬─────────────┘ │ pipes ┌─────────┴──────────┐ │ managed children │ └────────────────────┘

    • The CLI reads the daemon's ephemeral port from ~/.pm3/daemon.port. If the daemon is not running it starts one detached (spawn_orphan + setsid) and waits for the port file.
    • Each managed app gets one supervisor coroutine that runs the spawn → wait → maybe restart loop.
    • Child stdout/stderr are pipes owned by the daemon, pumped into LogSinks. This is what makes safe rotation possible.
    • State lives in ~/.pm3:

    ~/.pm3/ ├── dump.json # serialized process table ├── config.toml # configuration (or config.json) ├── daemon.port # ephemeral TCP port (while running) ├── daemon.pid # daemon pid (while running) ├── pm3.log # daemon's own stdout/stderr └── logs/ ├── <name>-out.log ├── <name>-err.log └── <name>-out__<stamp>.log[.gz]

    Set PM3_HOME to relocate everything (useful for tests).

    pm3.log carries the same shape as PM2's daemon log: every line is prefixed with a local timestamp and a PM3 log: tag, and the lifecycle is logged with PM2's wording — App [web:0] starting in -fork mode-, App [web:0] online, Stopping app:web id:0, App [web:0] exited with code [0] via signal[SIGINT], pid=1234 msg=process tree killed (1 pids), PM3 successfullystopped. PM3_LOG_DATE_FORMAT (or PM2_LOG_DATE_FORMAT) replaces the timestamp format; an empty value disables the prefix.

    The daemon renames its command line the way PM2's God daemon does, so ps shows PM3 v0.1.6: God Daemon (/home/me/.pm3) instead of the binary path; PM3_DAEMON_TITLE (or PM2's variable of the same name) overrides the title.

    #Log rotation

    pm3 integrates what pm2-logrotate does as a separate module, and fixes the three problems in its rotation strategy.

    #Why the rewrite

    pm2-logrotate rotates a file by streaming it to a dated copy and then truncating the original. That design has three well-known defects:

    1. Data loss. Anything a worker writes between the copy finishing and the truncate completing is silently discarded.
    2. O(size) work. A multi-gigabyte log has to be copied in full before it can be truncated; the rotation worker stalls, and on a busy service the window above is large.
    3. Sparse holes. After truncate, the writer's file offset is still at the old position, so the next write lands far into the file and leaves a NUL-filled gap.

    It also matches rotated files with a substring test (file.indexOf(base) >= 0), which happily matches unrelated files, and it assumes every log ends in .log.

    pm3 avoids all of this because the daemon owns every log descriptor:

    • Rotation is rename(live, live__<stamp>.log) followed by opening a fresh live file. That is O(1), atomic on POSIX, loses nothing (the writer immediately switches to the new descriptor), and never produces holes.
    • Rotated files are matched by an exact prefix/suffix test (<base>__<stamp>.log[.gz]), so api-out-backup.log or a file that merely contains the base name is never mistaken for a rotated log. (A file deliberately named <base>__<something>.log does match the pattern — the stamp is not required to be a date — so keep unrelated files out of the log directory.)
    • Gzip runs on a background worker with a configurable level, instead of blocking the rotation path.
    • Retention is computed from modification times, and can additionally cap files by age.
    • An optional min_size prevents rotating near-empty logs.
    • If two rotations happen within the same timestamp tick, the second one gets a -1, -2, … suffix instead of overwriting its sibling.
    • The daemon's own pm3.log rotates under the same policy, as pm2-logrotate rotates pm2.log. Matching PM2's split: the size rule applies to it, the cron trigger and reloadLogs apply to application logs only, and pm3 flush truncates it alongside them.

    #Policy

    Show the current policy:

    pm3 logrotate

    Change a key:

    pm3 set logrotate:max_size 100M pm3 set logrotate:compress true pm3 set logrotate:retain 14 pm3 set logrotate:rotate_interval "0 3 * * *"

    KeyDefaultMeaning
    max_size10MRotate at this size (0 disables size rotation)
    min_size0Never rotate below this size
    retain30Number of rotated files to keep per log
    max_age_days0Also delete rotated files older than this (0 = off)
    compressfalseGzip rotated files in the background
    compression_level9gzip level, 1–9 (validated and stored; the built-in encoder writes at a fixed level)
    date_formatYYYY-MM-DD_HH-mm-ssTimestamp tokens used in rotated names
    worker_interval30Seconds between size/prune passes (min 1)
    rotate_interval0 0 * * *Cron expression that forces a rotation
    tzsystemTimezone for file names (UTC+8, +05:30, or an IANA name)
    rotate_moduletrueReserved; kept for pm2-logrotate compatibility

    The policy is shared by every sink, so changes take effect immediately without restarting the daemon. Every key also accepts pm2-logrotate's camelCase spelling — dateFormat, workerInterval, rotateInterval, rotateModule and TZ — so a policy copied over from PM2 keeps working.

    #Configuration file

    The policy lives in the global file, $PM3_HOME/config.toml or $PM3_HOME/config.json — by default ~/.pm3/config.toml. It is global because the daemon is: one supervisor owns every process on the machine, so one policy applies to all of them. A project-local pm3.toml is read for its applications, never for rotation, and a [logrotate] table in one is rejected rather than quietly ignored.

    Only TOML and JSON are accepted — a config file that is silently ignored is worse than a loud error. logrotate.json is still read so that an existing install keeps working.

    # ~/.pm3/config.toml [logrotate] max_size = "100M" retain = 14 compress = true rotate_interval = "0 3 * * *" tz = "UTC+8"

    { "logrotate": { "max_size": "100M", "retain": 14, "compress": true } }

    Both are parsed into the same tree, so they behave identically. pm3 config prints the file in use and its contents; pm3 config init toml (or json) creates one seeded with the current policy; pm3 set logrotate:... rewrites whichever file is active, keeping its syntax.

    Sizes may be written as 100M, 1G, 512K or a plain byte count, and a value written back is always exact: min_size = 1000 stays 1000 rather than rounding to 0.98K. Unknown keys are an error rather than being ignored, so a typo fails loudly instead of quietly doing nothing.

    rotate_interval accepts the usual cron syntax (5 or 6 fields, names, ranges, steps, lists) plus @hourly/@daily/@weekly/@monthly/@yearly. When both day-of-month and day-of-week are restricted, the schedule fires if either matches, as Vixie cron does.

    #Process lifecycle

    • Stop sends kill_signal (default SIGINT, PM2's KILL_SIGNAL), waits up to kill_timeout milliseconds, then sends SIGKILL.
    • Autorestart restarts the process whenever it exits, unless --no-autorestart was given, the exit code is listed in --stop-exit-codes, or the restart budget (--max-restarts) is exhausted.
    • A run shorter than --min-uptime increments the unstable restarts counter.
    • --max-memory-restart is enforced by a monitor that samples /proc/<pid>/status every two seconds.
    • A process that is no longer desired online (stopped by the user, or out of restarts) is left alone by pm3 resurrect.
    • After a daemon restart nothing can reattach to a dead child's pipes, so the loaded table starts out stopped; run pm3 resurrect to bring back the processes that were desired online. Any orphan from a hard daemon death is terminated first, so a service is never started twice.
    • pm3 resurrect resolves each record the way pm3 start does before running it — an older pm3 could leave a record without an interpreter, which would otherwise be executed directly and fail without explanation — and reports a record it cannot resolve instead of dropping it.
    • A daemon left over from another build announces itself before the next command runs, with PM2's out-of-date warning; pm3 update is the fix it points at: save the list, kill the daemon, launch this build, resurrect.
    • Records written by an older pm3 (including a dump from before a field existed) are read with the missing values filled in, so upgrading never empties the process list over one missing key.

    #Environment variables

    VariableMeaning
    PM3_HOMEState directory (default $HOME/.pm3)
    PM3_LOG_DATE_FORMATDate format for pm3.log line prefixes (default YYYY-MM-DDTHH:mm:ss; empty disables the prefix)
    PM2_LOG_DATE_FORMATFallback for PM3_LOG_DATE_FORMAT, so a PM2 environment keeps its format
    PM2_KILL_SIGNALDefault graceful-stop signal (default SIGINT), PM2's variable
    PM3_DAEMON_TITLEps title for the daemon (default PM3 v<version>: God Daemon (<PM3_HOME>))
    PM2_DAEMON_TITLEFallback for PM3_DAEMON_TITLE, PM2's variable

    The launcher and installer add their own variables (PM3_BINARY, PM3_CACHE_DIR, PM3_VERSION, PM3_DOWNLOAD_BASE, PM3_REPO, …); they are documented in npmjs/README.md.

    #Limitations

    • Native (--target native) only: it needs POSIX process, signal and file APIs.
    • Linux/macOS: process memory and CPU are read from /proc, so resource reporting is Linux-specific. Supervision itself is POSIX.
    • kill_signal accepts SIGTERM, SIGINT, SIGHUP and SIGKILL.
    • compression_level is validated and stored but the built-in gzip encoder writes at a fixed level, so the level only documents intent.

    #License

    Apache-2.0.

    App

    pub(all) struct App {
    pm_id : Int
    name : String
    app_namespace : String
    script : String
    args : Array[String]
    interpreter : String
    interpreter_args : Array[String]
    cwd : String
    env : Map[String, String]
    out_log : String
    err_log : String
    merge_logs : Bool
    log_date_format : String
    autorestart : Bool
    max_restarts : Int
    restart_delay : Int
    min_uptime : Int
    max_memory_restart : Int64
    kill_timeout : Int
    kill_signal : String
    stop_exit_codes : Array[Int]
    status : String
    version : String
    username : String
    desired_state : String
    pid : Int
    restarts : Int
    unstable_restarts : Int
    created_at : Int64
    started_at : Int64
    stopped_at : Int64
    exit_code : Int
    out_log_size : Int64
    err_log_size : Int64
    memory : Int64
    cpu : Double
    last_start_ms : Int64
    } derive(Eq, ToJson,
    Debug
    ,
    FromJson
    )

    A process managed by pm3.

    App::command_display

    fn App::command_display(self : App) -> String

    Human-readable one-line command, for pm3 list and pm3 describe.

    App::command_line

    fn App::command_line(self : App) -> (String, Array[String])

    The argv pm3 will execute for this app: the interpreter (if any), its arguments, then the script and the script's own arguments.

    App::equal

    fn App::equal(App, App) -> Bool

    App::is_online

    fn App::is_online(self : App) -> Bool

    Whether the app currently has a live process.

    App::mark_exited

    fn App::mark_exited(self : App, code : Int) -> Unit

    Record that the process exited with code. The resource samples are dropped with it: PM2's monitor reports 0 for anything that is not running, and the last sample of a dead process is not memory it holds.

    App::mark_started

    fn App::mark_started(self : App, pid : Int) -> Unit

    Record that the process just started.

    App::new

    fn App::new(name : String, script : String, args : Array[String]) -> App

    Build a new application with conservative defaults.

    App::not_equal

    fn App::not_equal(x : App, y : App) -> Bool

    App::record_restart

    fn App::record_restart(self : App) -> Unit

    Record one restart, tracking whether the previous run was "unstable".

    App::should_restart

    fn App::should_restart(self : App, code : Int) -> Bool

    Whether the process should be restarted after exiting with code.

    App::to_json

    fn App::to_json(App) -> Json

    App::to_repr

    App::to_summary_json

    fn App::to_summary_json(self : App) -> Json

    A compact JSON description of an app, used for IPC payloads and --json.

    App::wants_stop

    fn App::wants_stop(self : App) -> Bool

    Whether a pm3 stop was requested.

    ConfigFormat

    pub(all) enum ConfigFormat {
    Toml
    Json
    } derive(Eq,
    Debug
    )

    The two configuration syntaxes pm3 understands.

    ConfigFormat::equal

    ConfigFormat::not_equal

    fn ConfigFormat::not_equal(x : ConfigFormat, y : ConfigFormat) -> Bool

    ConfigSource

    pub(all) struct ConfigSource {
    path : String
    format : ConfigFormat
    } derive(Eq,
    Debug
    )

    A configuration file pm3 found: where it is and how to read it.

    ConfigSource::equal

    ConfigSource::not_equal

    fn ConfigSource::not_equal(x : ConfigSource, y : ConfigSource) -> Bool

    CronSpec

    pub struct CronSpec {
    seconds : Array[Int]
    minutes : Array[Int]
    hours : Array[Int]
    days_of_month : Array[Int]
    months : Array[Int]
    days_of_week : Array[Int]
    seconds_restricted : Bool
    dom_restricted : Bool
    dow_restricted : Bool
    } derive(Eq,
    Debug
    )

    Parsed cron schedule. Fields are sorted ascending and de-duplicated.

    CronSpec::equal

    fn CronSpec::equal(CronSpec, CronSpec) -> Bool

    CronSpec::not_equal

    fn CronSpec::not_equal(x : CronSpec, y : CronSpec) -> Bool

    CronSpec::to_repr

    LogrotateConfig

    pub struct LogrotateConfig {
    max_size : Int64
    retain : Int
    compress : Bool
    compression_level : Int
    date_format : String
    worker_interval : Int
    rotate_interval : String
    tz : String
    rotate_module : Bool
    min_size : Int64
    max_age_days : Int
    } derive(Eq, ToJson,
    Debug
    ,
    FromJson
    )

    Rotation policy. Persisted as JSON so pm3 set logrotate:... survives daemon restarts.

    LogrotateConfig::equal

    LogrotateConfig::not_equal

    fn LogrotateConfig::not_equal(x : LogrotateConfig, y : LogrotateConfig) -> Bool

    LogrotateConfig::set_field

    fn LogrotateConfig::set_field(self : LogrotateConfig, key : String, value : String) -> Result[Unit, String]

    Update one field of the policy from a string, as used by pm3 set logrotate:<key> <value>.

    Keys use pm3's snake_case spelling; the camelCase names pm2-logrotate documents (dateFormat, workerInterval, rotateInterval, rotateModule, TZ) are accepted as aliases.

    LogrotateConfig::to_display

    fn LogrotateConfig::to_display(self : LogrotateConfig) -> String

    Render the policy as aligned key = value lines for pm3 logrotate.

    LogrotateConfig::to_json

    Request

    pub(all) struct Request {
    cmd : String
    args : Array[String]
    opts : Map[String, String]
    flags : Map[String, Bool]
    } derive(Eq, ToJson,
    Debug
    ,
    FromJson
    )

    A request from the CLI. Kept deliberately generic so new commands do not require protocol version bumps.

    Request::equal

    fn Request::equal(Request, Request) -> Bool

    Request::new

    fn Request::new(cmd : String) -> Request

    A request with a command name and no arguments.

    Request::not_equal

    fn Request::not_equal(x : Request, y : Request) -> Bool

    Request::to_json

    fn Request::to_json(Request) -> Json

    Request::to_repr

    Request::with_args

    fn Request::with_args(cmd : String, args : Array[String]) -> Request

    A request with positional arguments.

    Response

    pub(all) struct Response {
    ok : Bool
    msg : String
    data : Json
    } derive(ToJson,
    Debug
    ,
    FromJson
    )

    A reply from the daemon. data carries an optional JSON payload.

    Response::failure

    fn Response::failure(msg : String) -> Response

    A failed response.

    Response::success

    fn Response::success(msg : String) -> Response

    A successful response with no payload.

    Response::success_data

    fn Response::success_data(msg : String, data : Json) -> Response

    A successful response carrying a JSON payload.

    Response::to_json

    fn Response::to_json(Response) -> Json

    Response::to_repr

    RotatedFile

    pub(all) struct RotatedFile {
    name : String
    mtime : Int64
    } derive(Eq,
    Debug
    )

    A rotated log file: its name and modification time.

    The modification time is in nanoseconds since the epoch. Sub-second precision matters: a busy log can rotate several times inside one second, and retention has to tell those files apart to keep the true newest ones.

    RotatedFile::equal

    fn RotatedFile::equal(RotatedFile, RotatedFile) -> Bool

    RotatedFile::not_equal

    fn RotatedFile::not_equal(x : RotatedFile, y : RotatedFile) -> Bool

    active_config_source

    fn active_config_source() -> ConfigSource

    The file pm3 will read, or the file it would create.

    app_config_candidates

    fn app_config_candidates() -> Array[String]

    Configuration files pm3 looks for when pm3 start is given no script.

    A project-local pm3.toml comes first, so a checked-in file wins over the user's global defaults; PM3_CONFIG overrides both. This list is about applications only — the rotation policy is read from the global file alone, whatever else exists.

    app_field_names

    fn app_field_names() -> Array[String]

    Field names an [[app]] entry understands. Anything else is rejected, so a typo is reported rather than silently ignored.

    exec_interpreter, interpreterArgs and node_args are accepted because PM2's ecosystem documents them — a config carried over from PM2 should load as-is.

    app_records_from_json

    fn app_records_from_json(json : Json) -> Array[App]?

    Decode a JSON list of App records as one daemon reported them. None means the payload is not a list of apps this build can read, so a caller can report a real error instead of showing an empty process list.

    apps_from_json

    fn apps_from_json(json : Json, base_dir : String) -> Result[Array[App], String]

    Build the application list declared in a parsed config document.

    base_dir is the directory of the config file, used to resolve the paths it contains.

    bg_black_bright

    fn bg_black_bright(text : String) -> String

    Bright-black background, the style PM2 gives search highlights in logs.

    blue_bold

    fn blue_bold(text : String) -> String

    Bold blue, the style PM2 gives the launching status.

    bold_cyan

    fn bold_cyan(text : String) -> String

    Bold cyan, the style PM2 gives table headers and pm3 show keys.

    bold_gray

    fn bold_gray(text : String) -> String

    Bold grey, the style PM2 gives the [TAILING] … note.

    bold_inverse

    fn bold_inverse(text : String) -> String

    Bold inverse, the style PM2 gives the Describing process … banner.

    civil_from_days

    fn civil_from_days(days : Int) -> (Int, Int, Int)

    Civil date (year, month, day) for a number of days since 1970-01-01.

    cmd_delete

    let cmd_delete : String

    cmd_describe

    let cmd_describe : String

    cmd_flush

    let cmd_flush : String

    cmd_get_config

    let cmd_get_config : String

    cmd_get_logrotate

    let cmd_get_logrotate : String

    cmd_kill

    let cmd_kill : String

    cmd_list

    let cmd_list : String

    cmd_ping

    let cmd_ping : String

    Command names understood by the daemon.

    cmd_reload_logs

    let cmd_reload_logs : String

    cmd_reset

    let cmd_reset : String

    cmd_restart

    let cmd_restart : String

    cmd_resurrect

    let cmd_resurrect : String

    cmd_save

    let cmd_save : String

    cmd_set_logrotate

    let cmd_set_logrotate : String

    cmd_start

    let cmd_start : String

    cmd_start_apps

    let cmd_start_apps : String

    Start every app declared in a config file. args[0] is a JSON array of App records, already resolved by the client.

    cmd_stop

    let cmd_stop : String

    cmd_version

    let cmd_version : String

    Answered with the daemon's build; a client older than this command gets "unknown command" back, which also means the daemon is not its build.

    colorized_metric

    fn colorized_metric(value : Double, warn : Double, alert : Double, text : String) -> String

    PM2's UxHelpers.colorizedMetric: a zero stays plain, anything below warn is green, up to alert bold yellow, above it bold red — the way pm2 ls colours the host metrics footer.

    colors_enabled

    fn colors_enabled() -> Bool

    Whether output should be colorized.

    FORCE_COLOR wins over everything: an empty value, 1 and true force colours on, 0 and false force them off. Otherwise NO_COLOR (any non-empty value) disables them, and the fallback is whether stdout is a terminal.

    config_format_of

    fn config_format_of(path : String) -> ConfigFormat?

    Format implied by a file name, or None for an extension pm3 refuses to guess at.

    cron_matches

    fn cron_matches(spec : CronSpec, local_secs : Int64) -> Bool

    Does spec fire at local Unix second epoch_secs (already offset-shifted)?

    current_username

    fn current_username() -> String

    The login name of the user running pm3 — PM2's pm2_env.username, taken from Common.getCurrentUsername: the passwd entry of the effective user comes first, the usual environment variables are the fallback.

    daemon_process_title

    fn daemon_process_title() -> String

    The command line the daemon renames itself to, the way PM2 shows its God daemon as PM2 v7.0.4: God Daemon (/home/me/.pm2).

    PM3_DAEMON_TITLE wins, then PM2's own variable, so a drop-in environment keeps working; an empty value falls through, matching the falsy check in PM2's process.env.PM2_DAEMON_TITLE || ….

    days_from_civil

    fn days_from_civil(year : Int, month : Int, day : Int) -> Int

    Days since 1970-01-01 for a civil date (Howard Hinnant's algorithm).

    decode_request

    fn decode_request(line : String) -> Request?

    Decode one request line.

    decode_response

    fn decode_response(line : String) -> Response?

    Decode one response line.

    default_config_source

    fn default_config_source() -> ConfigSource

    The configuration file pm3 writes to when none exists yet.

    default_kill_signal

    fn default_kill_signal() -> String

    The signal pm3 stop sends before kill_timeout expires. PM2 v7's daemon uses its KILL_SIGNAL constant — PM2_KILL_SIGNAL when set, SIGINT otherwise — so pm3 starts from the same default while still letting each app override it.

    default_kill_timeout_ms

    let default_kill_timeout_ms : Int

    The default grace period before escalating to SIGKILL, matching PM2's KILL_TIMEOUT.

    default_log_path

    fn default_log_path(name : String, stream : String) -> String

    Default log path for a stream (out or err) of an application.

    default_logrotate_config

    fn default_logrotate_config() -> LogrotateConfig

    Default policy, matching the familiar pm2-logrotate defaults.

    derive_config_app_name

    fn derive_config_app_name(script : String) -> String

    Derive an app name from a script path: the basename without its extension.

    encode_request

    fn encode_request(request : Request) -> String

    Encode a request as a single newline-terminated line.

    encode_response

    fn encode_response(response : Response) -> String

    Encode a response as a single newline-terminated line.

    find_app_config

    fn find_app_config() -> String?

    The first application config file that exists, or None.

    find_config_source

    fn find_config_source() -> ConfigSource?

    The first configuration file that exists, or None.

    The order is TOML, JSON, then the legacy logrotate.json, so a user who migrates by writing config.toml does not have to delete the old file.

    find_package_version

    fn find_package_version(script : String, base : String) -> String

    The version of the nearest package.json at or above the app's script, the way PM2 fills pm2_env.version (Utility.findPackageVersion, which walks outwards with find-package-json). base resolves a relative script. The first package.json that exists and parses decides the answer: its version, or N/A when it has none. A file that exists but does not parse is skipped, and running out of directories is also N/A.

    format_cpu_percent

    fn format_cpu_percent(cpu : Double) -> String

    Render a CPU percentage the way PM2 does: rounded to one decimal, with a trailing .0 dropped — 0%, 85.4%, 100%.

    PM2 stores Math.round(stat.cpu * 10) / 10 and prints it with JavaScript's default number formatting, which is why a whole percentage has no decimal point. Matching that keeps the two tools' tables comparable line by line.

    format_duration

    fn format_duration(millis : Int64) -> String

    Render a millisecond duration as a short human string.

    format_fixed

    fn format_fixed(value : Double, precision : Int) -> String

    Format value with a fixed number of decimal places, without relying on locale-aware formatting.

    format_size

    fn format_size(bytes : Int64, precision? : Int) -> String

    Format a byte count with a binary unit and precision decimals.

    format_size_exact

    fn format_size_exact(bytes : Int64) -> String

    Render a byte count so that parse_size reads back exactly the same number.

    format_size rounds to two decimals, which is right for a status column but wrong for a config file: it would turn 1000 into 1003 on the next load. This only uses a unit when the value is an exact multiple, and otherwise falls back to a plain byte count.

    format_size_pm2

    fn format_size_pm2(bytes : Int64, precision? : Int) -> String

    Render a byte count the way PM2's bytesToSize does: one decimal place, a lowercase unit and no space — 2.0mb, 512.0kb, 1.2gb, 123b.

    The status tables use this so that pm3 list and pm2 ls agree on the number they show for the same process. PM2's own function appends a trailing space ('24.1mb '); that only matters where the value is concatenated into a sentence, and the tables pad the cell anyway.

    format_timestamp

    fn format_timestamp(epoch_secs : Int64, offset_secs : Int, fmt : String) -> String

    Format a Unix second using a subset of the moment.js-style tokens: YYYY, YY, MMM, MM, M, DD, D, ddd, HH, H, mm, m, ss, s, Z (±HH:mm) and ZZ (±HHmm). [literal] escapes text, as in dayjs.

    format_uptime_pm2

    fn format_uptime_pm2(millis : Int64) -> String

    Render the uptime column the way PM2's UxHelpers.timeSince does: whole seconds below a minute, then the largest unit that is used more than once — 119s stays 119s, 120s becomes 2m, 3600s is 60m, 7200s is 2h — with s, m, h, D, M and Y as the units.

    green_bold

    fn green_bold(text : String) -> String

    Bold green, the style PM2 gives the online status.

    has_apps

    fn has_apps(json : Json) -> Bool

    Whether a document declares any applications.

    interpreter_for_extension

    fn interpreter_for_extension(extension : String) -> String?

    PM2's lib/API/interpreter.json, verbatim.

    The returned name is a command, not a path: it is looked up on PATH when the app starts.

    is_app_config_file

    fn is_app_config_file(path : String) -> Bool

    Whether a path names a config file pm3 should read rather than a program to run. Only TOML and JSON are ever config files.

    is_binary_content

    fn is_binary_content(bytes : Bytes) -> Bool

    Whether a file's first bytes look like a compiled executable rather than text — PM2's isbinaryfile heuristic: a NUL byte, or more than 10% of the sample being non-text, means binary. UTF-8 multibyte sequences and a UTF-8 BOM are not binary. An empty sample is text.

    is_daemon_log_path

    fn is_daemon_log_path(path : String) -> Bool

    Whether path is the daemon's own log. It rotates under the same policy as application logs — pm2-logrotate rotates pm2.log the same way — but reloadLogs and the cron trigger deliberately leave it alone, again matching pm2-logrotate, and flush truncates it like PM2 does.

    is_python_interpreter

    fn is_python_interpreter(interpreter : String) -> Bool

    Whether a python interpreter needs its output unbuffered: PM2 exports PYTHONUNBUFFERED=1 whenever the interpreter contains python, so that logs appear in the file as the program writes them.

    load_apps_from_file

    fn load_apps_from_file(path : String) -> Result[Array[App], String]

    Read and parse an application config file, returning the apps it declares.

    load_logrotate_config

    fn load_logrotate_config() -> (LogrotateConfig, ConfigSource)

    Load the rotation policy, together with the file it came from.

    A missing file is not an error: pm3 falls back to the defaults and reports where the settings would be written.

    logrotate_from_json

    fn logrotate_from_json(json : Json) -> Result[LogrotateConfig, String]

    Extract the rotation policy from a parsed config document.

    A [logrotate] table is the documented shape. A document that is itself a policy (the legacy logrotate.json) is also accepted, so upgrading does not require rewriting the file by hand. Unknown keys are rejected: a typo in a config file should be loud, not silently ignored.

    logrotate_to_json

    fn logrotate_to_json(config : LogrotateConfig) -> Json

    The rotation policy as a Json object nested under logrotate.

    logrotate_to_toml

    fn logrotate_to_toml(config : LogrotateConfig) -> String

    Serialise the rotation policy as a config.toml document.

    The output is regenerated from scratch, so comments in a hand-edited file are lost when pm3 set rewrites it. pm3 config prints the file path first, which is the hint to edit it by hand instead.

    next_cron_fire

    fn next_cron_fire(spec : CronSpec, from_secs : Int64, offset_secs : Int) -> Int64?

    Compute the next fire time strictly after from_secs.

    offset_secs is the local UTC offset applied before evaluating the calendar fields. Returns None when no fire time is found within roughly four years (e.g. for an impossible date such as February 30th).

    pad_left_text

    fn pad_left_text(text : String, width : Int, pad : Char) -> String

    Left-pad text with pad until it is at least width characters wide.

    pad_right

    fn pad_right(text : String, width : Int, pad : Char) -> String

    Right-pad text with pad until it is at least width characters wide.

    pad_visible

    fn pad_visible(text : String, width : Int, pad : Char) -> String

    Pad text on the right with spaces until it is width terminal columns wide. Unlike pad_right, ANSI escape sequences in text are not counted, which is what lets render_box lay out pre-coloured cells.

    parse_bool_value

    fn parse_bool_value(text : StringView) -> Bool?

    Parse a true/false value, also accepting 1/0 and yes/no.

    parse_config_text

    fn parse_config_text(text : String, format : ConfigFormat) -> Result[Json, String]

    Parse configuration text in the given syntax.

    parse_cron

    fn parse_cron(expr : String) -> CronSpec?

    Parse a cron expression. Returns None for malformed input.

    parse_non_negative_int

    fn parse_non_negative_int(text : StringView) -> Int?

    Parse a base-10 non-negative integer.

    parse_rotated_stamp

    fn parse_rotated_stamp(log_path : String, file_name : String) -> String?

    If file_name is a rotated sibling of log_path, return the timestamp suffix that was embedded in its name.

    This replaces the original's indexOf(base) test, which also matched unrelated files such as api-out__backup.log or a differently named log that merely contained the base name.

    parse_size

    fn parse_size(text : StringView) -> Int64?

    Parse a size string such as 10M into bytes.

    parse_size_or

    fn parse_size_or(text : StringView, default : Int64) -> Int64

    Parse a size, falling back to default when the input is empty or invalid.

    parse_toml

    fn parse_toml(source : String) -> Result[Json, String]

    Parse a TOML document into the same Json shape that the JSON loader produces, so both config formats share one downstream path.

    The error string carries the offending line number.

    path_basename

    fn path_basename(path : String) -> String

    Return the final component of path.

    path_dirname

    fn path_dirname(path : String) -> String

    Return the directory part of path (everything before the last /). Returns "." when there is no separator.

    path_join

    fn path_join(left : String, right : String) -> String

    Join two path segments with a single /.

    pm3_config_candidates

    fn pm3_config_candidates() -> Array[String]

    Configuration files pm3 looks for, in order of precedence. Only TOML and JSON are supported; the last entry is the pre-config.* name.

    pm3_config_json_path

    fn pm3_config_json_path() -> String

    Path of the JSON configuration file.

    pm3_config_toml_path

    fn pm3_config_toml_path() -> String

    Path of the TOML configuration file.

    pm3_daemon_log_path

    fn pm3_daemon_log_path() -> String

    Path of the daemon's own stdout/stderr log.

    pm3_dump_path

    fn pm3_dump_path() -> String

    Path of the serialized process list.

    pm3_home

    fn pm3_home() -> String

    The pm3 home directory. Honours PM3_HOME, otherwise $HOME/.pm3.

    pm3_isatty

    fn pm3_isatty(fd : Int) -> Int

    isatty(3) from the C library.

    pm3_log_dir

    fn pm3_log_dir() -> String

    Directory holding application log files.

    pm3_logrotate_config_path

    fn pm3_logrotate_config_path() -> String

    Path of the persisted log-rotation configuration.

    logrotate.json is the historical name and is still read, but new installs are steered at config.toml / config.json, which can hold more than the rotation policy.

    pm3_pid_path

    fn pm3_pid_path() -> String

    Path of the file that publishes the daemon's process id.

    pm3_port_path

    fn pm3_port_path() -> String

    Path of the file that publishes the daemon's TCP port.

    pm3_state_dir

    fn pm3_state_dir() -> String

    Directory holding the daemon's own state (dump, port, config).

    pm3_version

    let pm3_version : String

    The version of this pm3 build.

    This is the single source for pm3 --version and for the version request the daemon answers with: the CLI compares the two to notice it is talking to a daemon left over from another build. Keep it in step with moon.mod and npmjs/package.json when a release is cut.

    read_config_file

    fn read_config_file(path : String) -> Result[Json, String]

    Read a configuration file and parse it according to its extension.

    Returns Err with a human-readable message for a missing file, an unsupported extension, or a syntax error.

    red_bold

    fn red_bold(text : String) -> String

    Bold red, the style PM2 gives errored and friends.

    render_box

    fn render_box(header : Array[String]?, rows : Array[Array[String]], style? : Bool) -> String

    Render a table the way PM2 renders its tables through cli-tableau: a box-drawing border around every cell, with exactly one space of padding on each side of the content.

    header is optional. pm3 describe passes None to get the key/value shape pm2 show prints; pm3 list passes column names.

    With style set the table is colored the way PM2 colors it: grey borders, a bold cyan header row, and — for the headerless key/value shape — a bold cyan first column. Cells may already contain ANSI codes; their widths are measured on the visible text.

    ┌────┬────────┐ │ id │ name │ ├────┼────────┤ │ 0 │ worker │ └────┴────────┘

    render_config

    fn render_config(config : LogrotateConfig, format : ConfigFormat) -> String

    Serialise the rotation policy in the syntax the source file uses.

    resolve_start_path

    fn resolve_start_path(script : String, cwd : String?, base : String) -> (String, String?)

    Resolve a pm3 start <script> argument against the directory the command was run in.

    PM2 does this in the CLI — cwd = path.resolve(process.cwd(), cwd) and pm_exec_path = path.resolve(cwd, app.script) — because the daemon's own working directory is an implementation detail: it is wherever the daemon happened to be launched from, which pm3 update or the first pm3 command of the day can change. A record that keeps the argument as typed would then stop resolving after the next daemon restart.

    Returns the script to record and the cwd to run it in. A cwd that was given is resolved like the script, even when the script does not name a file. An argument that names no file is left exactly as it was, so pm3start <name> can still revive a managed process and a bare command still reaches PATH (through the daemon's which).

    rotated_path

    fn rotated_path(log_path : String, stamp : String, compress : Bool) -> String

    Compute the path a rotated log should be renamed to.

    log_path is the live log (e.g. .../logs/api-out.log); the result is .../logs/api-out__<stamp>.log (plus .gz when compression is enabled).

    rotation_due

    fn rotation_due(size : Int64, config : LogrotateConfig) -> Bool

    Whether a log of size bytes should be rotated under config.

    save_logrotate_config

    fn save_logrotate_config(config : LogrotateConfig, source : ConfigSource) -> Result[Unit, String]

    Persist the rotation policy back to its file, creating parent directories as needed. The format follows the file name.

    script_extension

    fn script_extension(path : String) -> String

    The extension of path, including the dot, as path.extname defines it: the last dot of the last component, and nothing for a leading dot or a missing one.

    select_for_deletion

    fn select_for_deletion(files : Array[RotatedFile], retain : Int, max_age_days : Int, now : Int64) -> Array[String]

    Choose which rotated files to delete.

    Files are ordered newest-first by modification time (nanoseconds since the epoch); everything after the first retain is deleted. When max_age_days > 0, files older than that are deleted as well, regardless of position.

    set_colors_enabled

    fn set_colors_enabled(enabled : Bool) -> Unit

    Enable or disable styling for the rest of this process.

    split_shell_words

    fn split_shell_words(text : String) -> Array[String]

    Split text the way PM2 splits option values that are meant to be argument lists: on whitespace, with single and double quotes grouping a run of spaces into one word. The quotes themselves are not part of the words.

    --max-old-space-size=4096 --trace-warnings becomes two arguments, and so does --require "lib/setup file.js".

    split_timestamp

    fn split_timestamp(local_secs : Int64) -> (Int, Int, Int, Int, Int, Int, Int)

    Split a local Unix second into its calendar/clock fields.

    status_errored

    let status_errored : String

    status_launching

    let status_launching : String

    status_online

    let status_online : String

    status_stopped

    let status_stopped : String

    status_stopping

    let status_stopping : String

    strip_suffix_owned

    fn strip_suffix_owned(name : String, suffix : String) -> String

    Remove a trailing suffix from name, if present.

    style_blue

    fn style_blue(text : String) -> String

    Blue text — PM2's colour for the daemon's own log lines.

    style_bold

    fn style_bold(text : String) -> String

    Bold text.

    style_cyan

    fn style_cyan(text : String) -> String

    Cyan text.

    style_dim

    fn style_dim(text : String) -> String

    Faint text.

    style_gray

    fn style_gray(text : String) -> String

    Grey text — PM2 draws table borders and muted notes in grey.

    style_green

    fn style_green(text : String) -> String

    Green text — PM2's colour for "online" and for stdout log lines.

    style_inverse

    fn style_inverse(text : String) -> String

    Inverted text, for the banner PM2 draws around a title.

    style_red

    fn style_red(text : String) -> String

    Red text — PM2's colour for stopped/errored states and stderr log lines.

    style_yellow

    fn style_yellow(text : String) -> String

    Yellow text — PM2's colour for restarting/created states.

    truncate_text

    fn truncate_text(text : String, width : Int) -> String

    Shorten text to at most width characters, appending an ellipsis.

    visible_width

    fn visible_width(text : String) -> Int

    The length of text as it appears on a terminal: ANSI escape sequences contribute nothing.

    weekday_from_days

    fn weekday_from_days(days : Int) -> Int

    Day of week for a number of days since 1970-01-01 (0 = Sunday).

    with_app_defaults

    fn with_app_defaults(json : Json) -> Json

    Fill in the fields a record written by another pm3 build did not have.

    write_default_config

    fn write_default_config(format : ConfigFormat, force : Bool) -> Result[String, String]

    Create a configuration file seeded with the current effective policy.

    Refuses to overwrite an existing file unless force is set, so a stray invocation cannot silently discard a hand-written config. Returns the path that was written.

    yellow_bold

    fn yellow_bold(text : String) -> String

    Bold yellow, the style PM2 gives restarting and created.