mcproc 0.0.0-stage → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/hosts.md ADDED
@@ -0,0 +1,84 @@
1
+ # Hosts
2
+
3
+ Apps run on the local machine by default. A host declared under `hosts:` is reached over ssh, `docker exec`, or both; the manifest is read where mcproc runs, and each app is driven where it is bound.
4
+
5
+ ## Apps on several hosts
6
+
7
+ When part of a dev stack runs elsewhere, say watch builds on your machine and the dev server inside a
8
+ dev container, bind those apps to their host with `host:`:
9
+
10
+ ```yaml
11
+ apps:
12
+ libs-watch:
13
+ description: watch build of the shared libraries
14
+ kind: build
15
+ run: npm run watch:libs
16
+ host: local
17
+ web:
18
+ description: dev server inside the dev container
19
+ run: npm run dev
20
+ host: devbox
21
+ hosts:
22
+ devbox:
23
+ ssh: dev@devbox.local
24
+ ssh_args: ["-p", "2222"]
25
+ cwd: /home/dev/project
26
+ ```
27
+
28
+ A bound app always runs on its host. An app without `host:` runs wherever the command points:
29
+ `--host`, or local when there is none.
30
+
31
+ - Without `--host`, every command reaches each app at its own host. `ls` prints one table with a HOST
32
+ column (one probe per host, in parallel). `start`/`stop`/`restart`/`delete` of a profile or `all` go to
33
+ each app's host in dependency order, and a dependency on another host starts first, on that host.
34
+ `logs -f` streams them all, and `monit` shows them all on one screen.
35
+ - With `--host X`, a command reaches only the apps that run on X: the apps bound to X and the unbound
36
+ ones. Naming an app bound elsewhere is refused before anything runs.
37
+ - A host that does not answer never holds the others back. `ls` shows its apps as `unreachable` rows
38
+ with the reason. `monit` keeps their last known rows and names the host in the footer. `logs -f` prints
39
+ one note per app and picks the stream up where it was once the host is back, and a host that was down
40
+ at the start gets connected later. A change that fails on one host keeps the rows of what already
41
+ happened (`start` skips what comes after the failure). A readonly host refuses the whole change before
42
+ any host is touched, and the error names the apps that run there, dependencies included.
43
+ - ssh gives up connecting after 10 s (`-o ConnectTimeout=10`). A `ConnectTimeout` in `ssh_args` wins.
44
+
45
+ ## What a host needs
46
+
47
+ `ps`, `ss` (or `/proc`) and `tar`. Following logs live uses only POSIX tools there (`sh`, `stat` or `wc`,
48
+ `tail`, `head`, `od`). pm2 comes from the host's login-shell PATH (Node 18 or newer)
49
+ or from `unpack`. mcproc itself is only installed where the agent runs; locally it ships its own
50
+ pm2. `doctor` tells you which pm2 a host would use and why, or why none is usable.
51
+ `curl` or `wget` for URL checks on the host (optional: without them a url is checked from this machine under `url_check: auto`, and reads `unchecked` under `url_check: host`), and `getent` to report where a url's name resolves there.
52
+
53
+ ## How ports are found
54
+
55
+ `ls` runs one command on the host: `pm2 jlist`, `ps -eo pid,ppid` and `ss -ltnp` / `ss -lunp`
56
+ (falling back to `/proc/net/tcp` and `/proc/*/fd` when `ss` is missing or fails). pm2's pid plus its
57
+ descendants are matched to listening sockets, then compared with the declared ports:
58
+ `listening`, `declared-not-listening` (naming the process that holds the port, if any), `undeclared`.
59
+ Ports of processes owned by other users show without a pid, as the kernel reports them.
60
+ Ports published by Docker containers show the container name instead of a pid.
61
+
62
+ ## Hosts without pm2: `unpack`
63
+
64
+ `unpack` makes a host self-sufficient without installing anything system-wide. mcproc does not
65
+ contain pm2: it ships pm2's lockfile and assembles the tree once on
66
+ your machine with `npm ci` (needs `npm`, `tar` and access to the npm registry the first time; cached
67
+ in `~/.cache/mcproc/pm2` afterwards), then copies it into `<cwd>/.mcproc/runtime` on that host (or
68
+ `--dir`, which must be a directory of its own: a `bin/` mcproc did not write is refused). When the
69
+ host has no Node 18 or newer it also brings an official Node build: downloaded once on your machine
70
+ from nodejs.org, checked against the release's `SHASUMS256.txt`, cached in `~/.cache/mcproc/node`,
71
+ then streamed to the host, which needs no internet access. It copies the manifest there too, but never
72
+ over a hand-written one unless `--force`. Running it again changes nothing while the versions match
73
+ (an unpacked Node stays at its version until you pass `--node-version` or `--force`); a newer pm2 or
74
+ Node lands next to the old one, and for a daemon that is already running mcproc prints the
75
+ `bin/pm2 update` command instead of running it.
76
+
77
+ ```bash
78
+ mcproc unpack --host stand # pm2 only, when the host has Node >= 18
79
+ mcproc unpack --host box --with-node always # bare images such as alpine
80
+ mcproc unpack --host stand --dir /opt/mcproc/runtime # then add hosts.stand.runtime: /opt/mcproc/runtime
81
+ ```
82
+ Hosts without bash (alpine, busybox) need `shell: /bin/sh -c` in their `hosts:` entry. The official
83
+ Node builds are linked against the C++ runtime library, so a bare alpine needs `apk add libstdc++`
84
+ first (glibc distributions already have it); `unpack` checks this before downloading anything.
@@ -0,0 +1,39 @@
1
+ # New errors at a glance: `incidents`
2
+
3
+ `incidents` (MCP tool, and `mcproc incidents` on the CLI) returns only the errors that are new since they were last
4
+ taken, each with lines of context before and after, a stack trace folded in, plus crashes and restarts pm2 saw. Nothing
5
+ new is one short line, so an agent can poll it during long work with whatever its client offers (a loop, a cron, a
6
+ monitor), and read `logs` only for an incident worth a closer look. `mcproc incidents -f` keeps watching in a terminal.
7
+
8
+ A call takes what it returns: the pick-up point, kept in `.mcproc/incidents.json`, moves past those incidents for every
9
+ session, agent and CLI call on this machine, so nobody gets the same incident twice. `peek` (`--peek`) looks without
10
+ taking. An agent takes once it has dealt with what it was shown: the incidents screen in `mcproc monit` then shows those
11
+ incidents muted ([monit](monit.md)). A first call looks back 15 minutes (`since`). One poll reads at most 1 MB of new log
12
+ per file (the most recent part) and says how much it skipped.
13
+
14
+ An answer carries about 20,000 characters of incidents, newest first. The older ones past that are counted in `more`
15
+ and, on a take, taken with the rest; `logs` shows them. An incident's own line keeps up to 2000 characters, its context
16
+ lines and stack frames up to 300.
17
+
18
+ Clearing logs starts it over: `flush`, or `c` twice in `mcproc monit` (the logs of the running apps in view), empties
19
+ the log files and records when; nothing logged before the clear comes up again, and monit shows `cleared HH:MM` in the
20
+ log pane title.
21
+
22
+ What counts as an error: `level=error|fatal`, JSON `"level":"error"` or 50/60, `ERROR`/`FATAL` as a word, `SomeError:` /
23
+ `…Exception`, `Traceback`, `panic:`, `Unhandled…`, and Node's `E…` codes such as `EADDRINUSE`. A lowercase "error" in
24
+ a sentence does not count, and neither does a line merely being on stderr. Per app:
25
+
26
+ ```yaml
27
+ apps:
28
+ api:
29
+ errors:
30
+ match: ['Request failed with status 5\d\d'] # more patterns (JavaScript regular expressions)
31
+ ignore: ['ECONNRESET.*healthcheck'] # never an incident
32
+ context: [3, 5] # lines before and after
33
+ stderr: true # every stderr line counts (default false)
34
+ ```
35
+
36
+ `errors: false` turns it off for an app. Patterns are matched against the first 2000 characters of each line; a pattern
37
+ that repeats a repeat, like `(a+)+` or `(\w+\s*)*`, is refused when the manifest loads, because on a line that almost
38
+ matches it could run for minutes. Patterns that still take longer than 2 seconds on an app's log are stopped: that app
39
+ reports an error and keeps its place until they are fixed, the other apps are not held up.
package/docs/init.md ADDED
@@ -0,0 +1,30 @@
1
+ # `init`: drafting the manifest
2
+
3
+ `mcproc init` writes `.mcproc/` with a manifest draft read from `package.json` scripts and workspaces, nx `project.json`, `turbo.json`, `ecosystem.json` and a `Procfile`. Every candidate comes with a `# source:` trailer saying where it was found. On a plain repository every candidate is commented out; a turbo repository with a persistent `dev` task gets its dev stack live.
4
+
5
+ ## Turbo monorepos
6
+
7
+ On a turbo repo whose `turbo.json` has a persistent `dev` task, `init` drafts the dev stack live: each workspace package
8
+ with a `dev` script becomes an uncommented app named after the package, and together they form the `dev` profile, so
9
+ `start dev` runs them. Their placeholder description, `TODO: what this app does`, lets the manifest load as is until you
10
+ replace it. The root `dev` script, other persistent tasks (under a commented profile) and every other script stay commented.
11
+
12
+ - Libraries consumed from `dist/` (a `build` script plus `main`, `module`, `types` or an `exports` target under `dist/`)
13
+ share two apps instead of a `<pkg>-build` each: `libs`, one `turbo run build` over them (`kind: task`), and `libs-watch`,
14
+ `turbo watch build` once `libs` has completed (turbo 1 has no `turbo watch`, so there `libs` comes alone). A live app
15
+ that depends on such a library, directly or through other packages, gets
16
+ `depends_on: [{ app: libs, condition: completed, timeout: 600 }]`: the longest wait the manifest allows, since a cold
17
+ build of many libraries can outlast the default 60 s.
18
+ - A library live through its own `dev` watch is drafted as a `build` app and left out of `libs`, so its first build is not
19
+ awaited: the draft leaves a note on each app that uses it saying how to wait for it.
20
+ - Ports are also guessed for Next (3000), Vite (`server.port` in `vite.config.*`, or `preview.port` for `vite preview`,
21
+ as a literal or an env read's fallback, `process.env` or loadEnv's `env`, directly or through a constant of the config;
22
+ 5173 or 4173 when the section sets no port, no guess when it sets one `init` cannot read) and, in `src/`, `PORT = N`,
23
+ `DEFAULT_PORT = N`, `PORT ?? N`, `PORT || N` or `.listen(N)`. The guess shows as `port?` in each candidate's
24
+ `# source:` trailer; a live app also declares it as `port:` (and `url:`), so check it with `ls` (a wrong guess shows
25
+ ✗) and correct it by hand: `--adopt-ports` adds the real port but leaves the guess in place.
26
+ - `init --adopt-ports` (MCP: `adopt_ports`) drafts nothing and ignores `--force`: it needs an existing manifest and writes
27
+ the ports `ls` reports as undeclared for the running apps into it, comments and layout kept, then prints the diff. Ports
28
+ from 32768 up (the ephemeral range, almost always transient) and ports another app declares are reported, not adopted.
29
+ It refuses a manifest that marks the local host readonly, and a file that changed while the hosts were probed
30
+ (`ALREADY_EXISTS`: run it again).
package/docs/logs.md ADDED
@@ -0,0 +1,50 @@
1
+ # Logs
2
+
3
+ `logs` reads pm2's log files: one app's tail, several apps merged by time, or a live follow. pm2 stamps every line to the millisecond (`2026-10-12T21:00:00.123: …`).
4
+
5
+ ## Since a point in time
6
+
7
+ After a restart, "what happened since" is one call over a profile instead of a tail per app. The MCP `logs` tool takes
8
+ `targets` (apps, profiles or `all`) and `since`: a span (`90s`, `2m`, `2h`, at most 24 h), a time
9
+ (`2026-10-14T07:30[:00[.000]]`), or `last`, everything since your previous `logs` call over those apps in this MCP
10
+ session (a first call gives the last 100 lines of each log file). A time without a zone is the host's local time; with
11
+ `Z` or `+02:00` it is converted with each host's own zone. Lines are picked by pm2's stamps, which are in each host's
12
+ local time, and ordered by the moment they were written: each host's UTC offset, read along with its clock, puts hosts
13
+ in different zones on one timeline (a host whose zone cannot be read is placed by its raw stamps, and a note says so).
14
+ Each host's clock may still drift: a host whose clock runs ahead or behind has its lines placed that much later or
15
+ earlier. Each file is read at most 1 MB back: when the window reaches further, `skipped` names the bytes not read.
16
+ `lines` caps the merged answer (the most recent kept) and `truncated` counts what was dropped; `targets` without
17
+ `since` gives the latest `lines` (default 100) across the apps.
18
+ On the command line, `mcproc logs base --since 2m` prints the merged lines and exits, and `-f --since 2m` then follows
19
+ from exactly where that read ended; an app that was not running yet is shown from the same cutoff once it starts. `last`
20
+ needs the session that keeps the marks, so the CLI refuses it.
21
+
22
+ ## Live logs
23
+
24
+ `monit` and `logs -f` show a line moments after the app writes it. On this machine mcproc reads the
25
+ pm2 log files itself and the system tells it when they change, so no command runs per read. On a host
26
+ reached over ssh or docker, one connection stays open while you watch: a small `sh` loop there checks
27
+ the followed files four times a second and sends only what was added. If that connection drops, mcproc
28
+ reconnects and meanwhile reads on its own clock (monit every 2 s, `logs -f` every second).
29
+
30
+ The flat view of `monit` orders the lines of one host by their stamps, then by arrival; with apps on several
31
+ hosts, whose clocks may differ, by arrival.
32
+
33
+ ## Colors
34
+
35
+ `mcproc logs` and `monit` show the colors your apps write, as on their own terminal. `logs` prints them only
36
+ to a terminal: `--color` (or `FORCE_COLOR`) forces them into a pipe, `--no-color` or `NO_COLOR` turns them
37
+ off. `--json`, the MCP `logs` tool and `monit --once` always give plain text. Other escapes an app writes
38
+ (cursor moves, window titles) are dropped.
39
+
40
+ Many tools stop coloring when their output is not a terminal, and under pm2 it never is. Give such an app
41
+ `FORCE_COLOR: 1` in its `env`. mcproc never sets it on its own, because it puts color codes into that app's
42
+ pm2 log files:
43
+
44
+ ```yaml
45
+ apps:
46
+ web:
47
+ description: Next.js dev server
48
+ run: npm run dev
49
+ env: { FORCE_COLOR: 1 }
50
+ ```
@@ -0,0 +1,83 @@
1
+ # The manifest
2
+
3
+ `.mcproc/mcproc.yaml` describes every launch of a project: what each app is, how it starts, where it runs and what it listens on. `init` drafts it ([init](init.md)); everything else reads it.
4
+
5
+ ```yaml
6
+ version: 1
7
+ project: demo # pm2 namespace and process-name prefix; default: directory name
8
+ vars: # per-checkout values, see [variables](variables.md)
9
+ slot: 0
10
+ apps:
11
+ libs:
12
+ description: one-shot build of the shared libraries into dist/
13
+ kind: task # runs once; ls shows done / failed (exit N)
14
+ run: npm run build:libs
15
+ api-build:
16
+ description: tsc watch build of the API into dist/
17
+ kind: build # build | server | worker | task (default server)
18
+ run: npx tsc -w -p apps/api
19
+ api:
20
+ description: HTTP API serving /health
21
+ run: node dist/apps/api/main.js
22
+ cwd: . # relative to the project root
23
+ env: { PORT: 3310 }
24
+ port: 3310 # declared listening port (or ports: [3310, 9230])
25
+ url: http://localhost:3310/ # optional: the address to open; ls/describe show it with the HTTP answer, monit shows it
26
+ memory: 600M # expected memory, see [memory](memory.md)
27
+ containers: [demo-db] # docker containers the app owns
28
+ watch: [dist/apps/api] # pm2 restarts the server when the build output changes
29
+ depends_on:
30
+ - { app: libs, condition: completed } # wait for the build to exit 0
31
+ - api-build # plain name = started (start order only)
32
+ profiles:
33
+ base: [libs, api-build, api] # "all" is built in
34
+ hosts: # "local" is implicit
35
+ stand:
36
+ ssh: stand # alias from ~/.ssh/config; no secrets live here
37
+ cwd: /srv/demo # project root on that host
38
+ prod:
39
+ ssh: prod
40
+ docker: { container: demo-app, user: node }
41
+ cwd: /home/node/demo
42
+ readonly: true # every mutating tool is refused here
43
+ ```
44
+ `version` is the format of the file. `mcproc` names the mcproc that wrote it: `init` records its own version there
45
+ and nothing rewrites it later. `describe` shows it next to the running version, and a manifest from a newer mcproc
46
+ that fails to load says so in the error's hint.
47
+
48
+ Other app fields: `host` ([hosts](hosts.md)), `url_check` ([urls](urls.md)), `watch_delay`, `ignore_watch`, `autorestart`, `pm2:` (raw pm2 fields merged last).
49
+ Other host fields: `ssh_args`, `shell` (default `/bin/bash -lc`), `pm2` (binary or command), `pm2_home`,
50
+ `runtime`, `env` (merged into every app on that host). Names match `^[a-z0-9][a-z0-9._-]*$`.
51
+
52
+ The folder is created with a `.gitignore` containing `*`: it never enters your repository, so each
53
+ checkout keeps its own. `.mcproc/README.md` holds free-form notes that `describe` returns.
54
+
55
+ ## Kinds
56
+
57
+ | kind | what it is | autorestart | `ls` shows after it exits (autorestart on: pm2's words; off: the words below) |
58
+ |---|---|---|---|
59
+ | `server` | a process that listens (default) | on | `stopped`, `errored` |
60
+ | `worker` | a process without a port | on | `stopped`, `errored` |
61
+ | `build` | a long-running watch build | on | `stopped`, `errored` |
62
+ | `task` | runs once: a build, a migration, a seed | off | `done`, `failed (exit N)`, `stopped` |
63
+
64
+ Autorestart defaults to on for every kind except `task`, which defaults it off. `autorestart: false` on any kind
65
+ gives the same behaviour as a task, and `autorestart: true` on a task is refused. For an app with autorestart off,
66
+ the exit code is pm2's `exit_code`: 0 is `done`; no code, 130 or 143 (a stop) is `stopped`; any other code is
67
+ `failed (exit N)`. So a task that itself exits 130 or 143 reads `stopped`. A manual stop reads `stopped` because
68
+ mcproc puts `trap 'exit 130' INT; trap 'exit 143' TERM;` in front of the command of an app with autorestart off,
69
+ since pm2 itself records exit code 0 for a process killed by a signal. Two edges: a `run` that starts with `exec …`
70
+ replaces the shell and loses the trap, so a manual stop of it reads `done`; a task whose process tree takes longer
71
+ than pm2's kill timeout (1.6 s by default) to exit after SIGINT, whether it ignores the signal or only cleans up
72
+ slowly, is killed with SIGKILL and also reads `done`. The host shell must be POSIX (`bash` or `sh`): the default shell and the
73
+ documented `/bin/sh -c` both are. pm2 keeps no exit time, so UPTIME stays `-`. `start` on a done or failed task runs
74
+ it again. `init` drafts `build`, `migrate`, `seed` and `once` scripts as tasks and watch-style commands as `build`.
75
+
76
+ ## Process names
77
+
78
+ In pm2 the processes are named `<project>-<app>` (`demo-api`, `demo-api-build`) and grouped in the
79
+ `<project>` namespace; mcproc itself always takes and shows the short app name. The prefix is not
80
+ cosmetic: pm2 matches processes by name only, ignoring namespaces, so with bare names a second
81
+ project's `api` would restart the first project's `api` with its own settings. For the same reason
82
+ `start` and `restart` refuse with `NAME_COLLISION` when the name already belongs to another namespace
83
+ or to another checkout that declares the same `project`; give that checkout its own `project:`.
package/docs/memory.md ADDED
@@ -0,0 +1,57 @@
1
+ # Memory: budgets and the lease queue
2
+
3
+ Several stands on one machine can starve it, so a `start` asks for memory first. The manifest declares what an app needs:
4
+
5
+ ```yaml
6
+ resources:
7
+ reserve: 2G # what the host keeps for everything else (default 2G; also a percentage, 15%)
8
+ margin: 10% # added to every estimate (default 10%)
9
+ settle: 20 # seconds a fresh start still counts as ramping up (default 20)
10
+ apps:
11
+ api:
12
+ memory: 1.5G # what the app is expected to take
13
+ containers: [demo-db] # docker containers it owns: removed with it, counted in its memory
14
+ ```
15
+
16
+ - **The estimate**, per app: the larger of the declared `memory:` and what a running sibling slot of the same app
17
+ measures (its process tree plus its declared containers; the largest among the siblings). The lease's need is
18
+ `ceil(estimate × (1 + margin))`.
19
+ - **Admission.** The start runs at once when `MemAvailable − reserve − ramping` covers the need and nothing is pending
20
+ ahead of it. `ramping` is the need of running leases admitted within `settle` seconds, or whose starter is still alive.
21
+ Otherwise the answer is `queued #N`, and the queue is strict FIFO: a small start never jumps a large one. A stand that
22
+ could not fit even on an idle host is refused with `NEVER_FITS`.
23
+ - **The watcher** is a pm2 process, `mcproc-watcher` in the `mcproc` namespace. It judges the head every 2 s and at once
24
+ when the queue changes, exits after a minute idle, and the next queued start launches it again. While the head waits,
25
+ each tick reads the memory alone; its siblings are measured again when that reading lets it fit, when the queue
26
+ changes, and every 30 s. Each full judgement budgets the head again (apps that came up meanwhile drop out of its need),
27
+ and a queued start keeps the `--host` it was given. It ends leases whose owner is gone (`checkout gone`, `requester gone`) and an admitted exec
28
+ that has not run within 60 s (`the exec did not take its turn`). `stop` and `delete` cancel the starts they overlap
29
+ before stopping, and free the running leases afterwards.
30
+ - `docker stats` costs about 2 s per call, so the containers' memory is asked only by `ls` (its MEM column includes it for
31
+ apps that declare `containers:`), `describe` and the sibling measurement; never by monit's loop or by `wait`.
32
+ - **Looking and cancelling.** `mcproc leases` (tool `leases`) shows the host's queue: who runs, who waits and why, the
33
+ memory reading and the watcher. `leases cancel <id>` cancels a pending lease or a starting exec; a starting start is
34
+ refused: stop its target instead. `ls` shows a queued app as `queued #N`, `wait` rides through the queue, `doctor`
35
+ reports the state directory and the watcher. `start --note <text>` keeps a reason on the lease.
36
+ - **State** lives in `~/.local/state/mcproc` (or `$XDG_STATE_HOME/mcproc`, or `MCPROC_STATE_DIR`): `leases/`, `slots/`,
37
+ `watcher.json` and `watcher.ecosystem.json`. A dev checkout runs the watcher from the bundle `MCPROC_CLI_ENTRY` names.
38
+
39
+ ## Exec: one-off work under a lease
40
+
41
+ ```bash
42
+ mcproc exec --memory 4G --note "e2e run" -- npx vitest run # --timeout <s> bounds the wait in the queue (default 600, max 3600)
43
+ ```
44
+
45
+ `exec` runs a command from the project root with the manifest's `exec.env` (templates allowed, so it can point at this
46
+ checkout's ports) and `MCPROC_PROJECT` set. With `--memory` it is a lease like a start: it runs at once when the host has
47
+ room, else waits its turn, and frees the lease when the command exits. Without it, it runs at once. The MCP tool returns
48
+ the exit code and the last 200 lines of output, and is marked destructive: it runs any local argv. It reads that output
49
+ through pipes: a process the command leaves behind on them (a server sent to the background) loses them one second after
50
+ the command exits (its next write gets EPIPE), so such a process should detach its own stdio.
51
+
52
+ ## Limits
53
+
54
+ - Leases are local: `exec` and the worktree commands run on this machine only (a `host` other than `local` is refused), and a `start` of an app bound to a remote host that
55
+ declares `memory:` is refused (`LEASE_UNSUPPORTED_HOST`). That makes monit's `R` key (`start all --reload`) refuse such
56
+ a manifest.
57
+ - Without `/proc/meminfo` (not Linux) there is no memory reading, so nothing is checked.
package/docs/monit.md ADDED
@@ -0,0 +1,40 @@
1
+ # `mcproc monit`
2
+
3
+ A terminal dashboard on the same data as `ls` and `logs`: a table of apps with real memory and CPU (summed over each
4
+ app's process tree, which is what `bun run dev` or `next dev` hide behind their launcher), the ports with ✓/✗, the
5
+ runtime command, a live log tail with history, and the selected app's details. `mcproc monit --once` prints one frame
6
+ as text, which is what an agent or a script reads.
7
+
8
+ ## Keys
9
+
10
+ | key | what it does |
11
+ |---|---|
12
+ | `j` / `k`, Tab | select an app (off in the incidents view) |
13
+ | `↑` / `↓`, PageUp / PageDown, the mouse wheel | scroll the log pane; End returns to the tail |
14
+ | `o` / `e` / `b` | show stdout, stderr or both |
15
+ | `r` / `s` / `x` | restart, start or stop the selected app (dashboard only) |
16
+ | `c` twice | clear the logs of the running apps in view; the pane title then shows `cleared HH:MM`, and `incidents` starts over there |
17
+ | `R` twice | reload every app in view the way `mcproc start all --reload` does: in dependency order with their `depends_on` waits, running apps restart, stopped ones start, `kind: task` apps run again. A view with an app on a readonly host is refused whole; narrow it with `h` |
18
+ | `f` | the flat view: every app's logs merged into one labeled stream, `app \| line` and `app ! line` for stderr, one color per app, like `mcproc logs -f`; `f` again returns |
19
+ | `i` | the incidents of the apps in view: errors, crashes and restarts with their context, as the `incidents` tool shows them, from the last 15 minutes or the last clear, then new ones as they happen; `i` again returns |
20
+ | `t` | on the incidents screen, hide or show the incidents an agent has taken |
21
+ | `h` | cycle hosts, from `all` to each host, narrowing the screen the way `--host` narrows a command |
22
+ | `q` | quit |
23
+
24
+ The incidents screen is for you: it reads from its own position and never takes anything, so the agent's next
25
+ `incidents` call returns exactly what it would have ([incidents](incidents.md)). What an agent has taken is shown in
26
+ muted colors and marked `taken`, and the title counts it. An incident counts as taken when its time stamp is at or
27
+ before the last line a take read in that file.
28
+
29
+ ## Several hosts
30
+
31
+ With host-bound apps the screen opens on every app at its own host, with a HOST column, and reads each app's logs
32
+ where it runs. A host that does not answer keeps its last known rows, and the footer names it ([hosts](hosts.md)).
33
+
34
+ ## What the screen shows
35
+
36
+ Memory is the sum of each process's resident set, so shared pages count once per process; pm2's own root-pid figure
37
+ is shown next to it in the details. CPU is per core over the refresh interval. Long log lines wrap with indented
38
+ continuations. The selected app's detail line counts its in-process reloads since its pm2 start
39
+ ([reloads](reloads.md)). The screen shows an app's URL but never requests it, so your dev servers' request logs
40
+ stay yours ([urls](urls.md)). How lines reach the screen: [live logs](logs.md#live-logs).
@@ -0,0 +1,53 @@
1
+ # Readiness: `wait` and `depends_on` conditions
2
+
3
+ `wait` (MCP tool, `mcproc wait <app> --until <cond> [-t 60]` on the CLI) blocks until an app is usable or finished,
4
+ instead of sleeping and polling:
5
+
6
+ | `until` | met when |
7
+ |---|---|
8
+ | `listening` | every declared port is bound (needs `port`/`ports`) |
9
+ | `url-ok` | the `url` answers 2xx/3xx, checked as `ls` checks it (on the host, and from this machine when the host gets no answer; see [urls](urls.md)) |
10
+ | `exited` | the process left `online`; with autorestart off the answer carries `exit_code` and `done` / `failed (exit N)` / `stopped`, otherwise pm2's `stopped` or `errored` |
11
+ | `log:<regex>` | a new line of its out or err log matches (ANSI stripped, matched against the line without pm2's time stamp; the answer keeps the stamp; history is not searched) |
12
+ | `idle:<seconds>` | no log output for that long: a watcher settled |
13
+ | `reloaded` | the app's next in-process reload (see [reloads](reloads.md)) has been quiet for 2 s; the answer carries its time, its line count and its first restart or rebuild line, else its first line |
14
+
15
+ Without `curl` or `wget` on the host, `url-ok` under `url_check: auto` asks from this machine; under `url_check: host` it runs to the timeout and reports `unchecked`.
16
+ `log:`, `idle:` and `reloaded` see only output written after the call begins, and notice a dead process only every 5 s; for
17
+ "is it up" prefer `listening` or `url-ok`. `wait --until exited` succeeds (exit code 0) once the process has exited,
18
+ whatever its exit code; scripts read `exit_code` in `--json` or the `(exit N)` in the line. A wait on an app that
19
+ was never started is a `NOT_STARTED` error, not a result. `listening` needs a declared port, `url-ok` a declared
20
+ url and `reloaded` an app without `reloads: false`, else `BAD_ARGUMENT`. `exited` on an app that has already stopped answers at once.
21
+
22
+ After a `restart`, and for an app that pm2 itself restarts on a change (`watch:`), wait until `listening`. An app that
23
+ reloads in place (a watcher: tsx watch, nodemon, `node --watch`, Vite, `next dev`) keeps its process and its port, so
24
+ `listening` answers at once while the old code may still run: after an edit, follow it with `wait until: reloaded`.
25
+
26
+ The answer is one line: `web: listening after 1.3 s (:3000 ✓)`, `libs: done after 12.4 s (exit 0)`. On timeout
27
+ (default 60 s, max 600) it says what was last seen (`web: listening not reached after 60.0 s (status online, :3000 ✗
28
+ nothing listens)`), and `ok` is false. If the process dies meanwhile, the wait ends at once with its reason and
29
+ status and no observation, for example `web: exited (stopped) after 3.2 s while waiting for listening` (`observed`
30
+ is in `--json`). Either way it is a result, not an error, and the CLI exits 1. Local apps are checked twice a second, remote ones once; `log:`/`idle:`/`reloaded` read the log as it grows. A process that pm2 is
31
+ restarting (a watch restart, a restart delay) is not dead: the wait goes on, bounded by the timeout. `listening` and
32
+ `url-ok` count only while the process is online or launching; a restart in progress is waited out, and a port or url that
33
+ still answers after the process died is not ready. Claude
34
+ Code does not cut a long MCP call, but moves one into the background after two minutes. A cancelled MCP `wait` (or
35
+ `start`) call stops the polling at once.
36
+
37
+ `depends_on` entries can name what they wait for:
38
+
39
+ ```yaml
40
+ depends_on:
41
+ - logger # a name: start order only
42
+ - { app: libs, condition: completed } # the task exited with code 0
43
+ - { app: api, condition: ready, timeout: 90 } # ports listening, or the url answers; default 60 s, max 600
44
+ ```
45
+
46
+ `completed` needs a `kind: task` dependency; `ready` one with a port or a url (ports are checked when declared, the
47
+ url otherwise). `start` launches dependencies first and waits at each such edge, on the dependency's
48
+ host, also when the dependent runs elsewhere. A done task is run again, a running one is waited on. When a dependency
49
+ fails, times out or dies, the dependent is not started: its row reads `skipped` with the reason, and so do the apps
50
+ that depend on it; unrelated apps still start. The answer adds `waited 4.2 s for libs (completed)` for each wait that was met (a failed
51
+ wait shows in the rows), `start`'s JSON carries the waits as `waits[]`, and the CLI exits 1 when anything was skipped. With `--host X`, dependencies that run elsewhere are neither started nor waited for.
52
+ `restart` does not order apps by their dependencies or wait for conditions; `start` does, and `start --reload`
53
+ restarts the apps that are already online the same way.
@@ -0,0 +1,67 @@
1
+ # Reloads
2
+
3
+ Dev tools in watch mode reload after an edit inside their own process. pm2 sees none of it: RESTARTS stays 0, and a
4
+ port that never closed says nothing about which code answers. mcproc reads these reloads from the app's log. A
5
+ reload is one of these lines, matched without pm2's stamp and with ANSI stripped:
6
+
7
+ - a restart: tsx's `[tsx] … Restarting...`, nodemon's `restarting due to changes`, `node --watch`'s `Restarting '…'`;
8
+ - a rebuild: Vite's `[vite] page reload` or `hmr update`, Next's `✓ Compiled in …` (with a page path, `✓ Compiled /page
9
+ in …`, it is that page's first compile and does not count) and `Reload env:`, tsc's `File change detected. Starting
10
+ incremental compilation`;
11
+ - a repeated `listening on` or `listening at` line, for watchers that print nothing else (`bun --watch`).
12
+
13
+ Marker lines less than 2 s apart, stdout and stderr together, are one reload: a library rebuild that prints a line per
14
+ changed file counts once, and so does a restart followed by the server's new `listening` line. The first burst after
15
+ pm2 starts the process, when it holds only `listening` lines, is the boot, not a reload. A restart less than 2 s after
16
+ the boot's `listening` line joins the boot's burst: it counts once, and is shown by the restart line. Counting starts at
17
+ pm2's start of the process: lines an earlier run left in the same log file never count.
18
+
19
+ Per app:
20
+
21
+ ```yaml
22
+ apps:
23
+ api:
24
+ description: API under a home-grown file watcher
25
+ run: node scripts/dev.js
26
+ reloads:
27
+ match: ['^\[devwatch\] reloaded'] # more markers (JavaScript regular expressions)
28
+ ignore: ['listening on queue'] # never a reload
29
+ defaults: true # false: only your own patterns, no built-in markers
30
+ ```
31
+
32
+ `reloads: false` turns it off for an app. Own patterns follow the rules of `errors` patterns ([incidents](incidents.md)): matched against the first
33
+ 2000 characters of each line, a repeat of a repeat refused when the manifest loads, and stopped when they take longer
34
+ than 2 seconds on an app's log, which leaves that app's reloads uncounted with an error while the other apps go on.
35
+
36
+ Four places show them:
37
+
38
+ - `wait until: reloaded` (`mcproc wait web --until reloaded`) answers once the app's next reload has been quiet for 2 s,
39
+ with its time, its line count and its first restart or rebuild line, else its first line:
40
+ `web: reloaded after 3.0 s (3 lines from 2026-10-14T12:00:01.000): [vite] (client) page reload /src/a.ts`. A server
41
+ whose reload restarts it may print its new `listening` line more than 2 s later, after the reload was answered: wait
42
+ until `listening` next. A wait that times out or is aborted while a reload is still under way says so, with `ok`
43
+ false: `reload in progress since 2026-10-14T12:00:59.000 (2 lines): [tsx] change in src/a.ts Restarting...`.
44
+ - `incidents` with `include_reloads: true` (`mcproc incidents --reloads`) adds one `reloaded` incident per app with
45
+ the reloads since the last call: how many, the first and the last one's time, and the last one's first restart or
46
+ rebuild line, else its first line.
47
+ - `ls` with `reloads: true` (`mcproc ls --reloads`) adds each running app's reloads since its pm2 start to RESTARTS,
48
+ `0 (+7)`, and to each JSON row as `reloads: { count, last, partial }`. It reads each host's logs once more, so a plain
49
+ `ls` does not count them.
50
+ - `monit` shows them in the selected app's detail line while it runs (online or launching), `restarts 0 (+7 reloads,
51
+ last 12:01:05)`, counted for the apps whose logs it follows: the selected one on the dashboard, every running one in
52
+ the flat view. `monit --once` counts from the history it reads.
53
+
54
+ Limits. A count marked `≥` (`0 (+≥7)`, `+≥7 reloads`) is a lower bound: the log was cleared since the process started,
55
+ or the app wrote more than the last 1 MB that is read. monit and `monit --once` count from the history they read, the
56
+ last `--lines` lines of each log (200 by default), so an app that has run for a while often shows `+≥7` there where
57
+ `ls --reloads` shows an exact `+7`. An err log whose first line came more than a minute after the start also marks
58
+ monit's count `≥`: monit cannot tell where in the file its history began. A reload whose lines are split between two
59
+ `incidents` calls is reported in both. With `include_reloads`, an app whose own `reloads` patterns run away also has
60
+ its error incidents held back, and its point stays where it was, until the patterns are fixed. The process start is
61
+ pm2's start time read in the host's current time zone, so a daylight saving change since the start, or a pm2 daemon
62
+ started under a different `TZ`, can shift it, and with it which lines count.
63
+
64
+ A process that runs several servers (a turbo or concurrently dev task, `docker compose up`) is one app, and only its
65
+ first burst of `listening` lines is the boot: a later server whose first `listening` line comes more than 2 s after the
66
+ one before it counts as a reload. Put that server's line in the app's `reloads.ignore`, for example
67
+ `ignore: ['ws listening on']`.
package/docs/urls.md ADDED
@@ -0,0 +1,56 @@
1
+ # URLs
2
+
3
+ An app may declare `url`, the address you open in a browser; `init` drafts `http://localhost:<port>`
4
+ for every server it finds a port for. `ls`, `describe`, `monit --once`, `wait until: url-ok` and a `ready`
5
+ dependency on a url all check it the same way and report `ok` (2xx/3xx), `error` (4xx/5xx: up but refusing),
6
+ `down` (nothing answered) or `unchecked` (not requested, or `url_check: host` on a host with neither curl nor
7
+ wget). It is a liveness check, not a trust check: self-signed development certificates are accepted, and a
8
+ redirect is an answer (it is not followed, except by `wget` on a host without curl).
9
+ The interactive `monit` shows the URL but never requests it, so your dev servers' request logs stay yours.
10
+
11
+ Where it is requested from is set per app with `url_check`:
12
+
13
+ - `auto` (the default): on the app's host first (`curl`, or `wget` when there is no curl). An HTTP answer
14
+ there is the result. When the host gets none, mcproc requests the url from this machine, where mcproc
15
+ and usually your browser run, so a dev container whose name for the site points at itself, or whose
16
+ `localhost` is not where the port is published, still reads `✓ 200 (here)`. When neither answers it is `down`.
17
+ - `host`: only on the app's host.
18
+ - `here`: only from this machine. `ls`, `wait` and `ready` never request it on the host; `doctor` may, to show
19
+ where it answers.
20
+
21
+ On the `local` host this machine is the host: the url is requested once, whatever `url_check` says. A url the
22
+ host answers costs one request, on the host; the second request is made only for a url the host could not reach.
23
+ On the host each url's name lookup is bounded at 2 s, its request at 2 s (3 s with `wget`), and the host's
24
+ environment lookups at about 4 s (where it has `timeout`), so a host whose resolver drops packets makes `ls` and
25
+ each poll of `wait until: url-ok` that much slower. The probe's time budget grows with the number of urls it checks
26
+ to match (30 s, plus 4 s and 5 s per url), up to 120 s.
27
+
28
+ One catch of `auto`: a `localhost` (or loopback-address) url of an app on a remote host can read `✓ 200 (here)`
29
+ when this machine happens to serve the same port, even while that app is down. `wait` and a `ready` dependency
30
+ still require the app's process to be online; `url_check: host` rules the catch out.
31
+
32
+ `ls` marks an answer from this machine with `(here)`, and a `down` url with the short reason of the last place
33
+ tried: `refused`, `not resolved`, `timeout`, `tls`, `reset` or `no answer`. The JSON rows carry `url_from` (`host`,
34
+ `here`, or null when unchecked) and `url_reason` (why it is down, with both sides when neither answered, as in
35
+ `on box: refused; here: refused`, or why it was taken from this machine), which `describe` prints too.
36
+
37
+ When the url's name resolves on the host to the host itself (for instance through a hostname line in its
38
+ `/etc/hosts`), that is the reason only when the url answered from this machine:
39
+ `on box site.test resolves to 127.0.1.1, the host itself`, the dev container whose name for the site means another
40
+ machine. Otherwise the host's own failure stays the reason and the address follows as a note, so a box whose own
41
+ name resolves to itself and whose app is down reads `✗ down (refused)`, with the `url_reason`
42
+ `on box: refused (devbox resolves to 127.0.1.1, the host itself)`.
43
+
44
+ `doctor` shows each host's environment (container or not, hostname) and where each of its apps' urls answers, and
45
+ suggests `url_check: auto` for an app with `url_check: host` whose url answers only from here.
46
+
47
+ ```yaml
48
+ apps:
49
+ web:
50
+ description: dev server in the container
51
+ run: yarn dev
52
+ port: 3000
53
+ url: https://site.test/
54
+ url_check: here # checked only from this machine: ls, wait and ready never request it in the container
55
+ host: box
56
+ ```
@@ -0,0 +1,34 @@
1
+ # Variables and the per-checkout override
2
+
3
+ Ports, container names and paths that differ per checkout are variables, declared once under `vars:` with a default:
4
+
5
+ ```yaml
6
+ vars:
7
+ slot: 0 # the main checkout; a worktree gets its own (see [worktrees](worktrees.md))
8
+ web_port: '{{ 5181 + slot * 100 }}' # a default may use the vars before it
9
+ apps:
10
+ web:
11
+ description: dev server
12
+ run: npm run dev -- --port {{ web_port }}
13
+ port: '{{ web_port }}' # port and ports are templates only as quoted strings
14
+ db:
15
+ description: postgres for the stand
16
+ run: docker run --rm --name {{ project }}-db -p {{ 5432 + slot }}:5432 postgres
17
+ containers: [ '{{ project }}-db' ]
18
+ ```
19
+
20
+ - `{{ expr }}` takes names, integers, `+ - * / %` and parentheses; division truncates. `\{{` writes a literal `{{`,
21
+ which a docker `--format '{{.Names}}'` in a `run` line needs. Any other `{{` in a string under `apps`, `hosts`, `worktree` or `exec` is
22
+ refused when the manifest loads (`BAD_TEMPLATE`, naming the field: `apps.ps.run: unexpected "." in ".Names"`). The syntax is `{{ }}` because bash, Caddy and compose own
23
+ the other ones.
24
+ - The built-ins are `project` (the pm2 namespace of this checkout) and `root` (its project root).
25
+ - Templates work in every string field under `apps`, `hosts`, `worktree` and `exec.env`. They do not work in `project`, in
26
+ the names of apps, profiles and hosts, or in the `vars` names.
27
+ - `.mcproc/mcproc.local.yaml` is the checkout's own override (it lives in the gitignored folder). It holds `project` (the
28
+ slug this checkout runs under), `repo` (the main checkout, for a worktree) and `vars` (values that win over the
29
+ defaults). A var the manifest does not declare is refused. In a main checkout it may also hold `worktree` (`copy`,
30
+ `link`): machine-local extras for every new worktree (see [worktrees](worktrees.md)).
31
+ - `describe` without an app prints the resolved vars, where each value came from, and the override's path.
32
+
33
+ The slot idiom: the manifest says `slot: 0` and writes ports as `'{{ 5181 + slot * 100 }}'` and containers as
34
+ `{{ project }}-db`. The main checkout keeps 5181 and `demo-db`; a worktree at slot 2 gets 5381 and `demo-w2-db`.