AGENTS.md@apps/cli/src/lib/local-server · git:20261016.c9fd392 · 2026-10-16 · sha256 e6454e0bcdb3c756
AGENTS.md@apps/cli/src/lib/local-server git:20261016.c9fd392A
Immutable. This exact content never changes and is served at /api/v1/blob/e6454e0bcdb3c756.
# Agent Instructions — `apps/cli/src/lib/local-server` How `zitadel start`, `stop`, `status`, `logs`, `reset` and `console` run and find a directory's local Zitadel server. Read this before changing any of them. ## The rule everything here serves Every directory runs its own server, on its own port, with its own data and its own admin credential, and the CLI only talks to the server that directory's own `.zitadel/local/runtime.json` names. This exists because a second directory once attached silently to the first one's server on port 8080 and signed in with a password that server had never imported (issue #1863). So: nothing falls back to a default port, a recorded URL is never trusted on its own, and a recorded pid is never trusted on its own. ## Where things live `server.ts` is the only public entry point: `LocalServer`. Commands call it and nothing under `internal/`. When a command needs something the class does not offer, add it to the class (and to a launcher if it is about running a server), not to the command. `internal/lifecycle.ts` decides what to do (reuse, restart, which port, when a start has failed) against interfaces, so it is tested with no processes, sockets or real time. `internal/binary-launcher.ts` and `internal/docker-launcher.ts` know how to run, find and stop a server. `internal/probes.ts` and `internal/store.ts` are the real port, health and record implementations. `internal/failures.ts` turns the lifecycle's typed errors into the messages commands report. `internal/start-lock.ts` is the one-start-at-a-time lock, also used to claim ports. `runtime.ts` is the record on disk and where it is looked for; it does not decide whether a server is running. ## Rules that each came from a real failure - A pid is not an identity. Pids are reused and every directory's server runs the same program, so a record's server is "running" only if a process in its group runs the server program and serves that record's data directory. Reading the process list needs `ps axww` (without `ww`, `COLUMNS` cuts a long path short and a running server looks foreign). A server that `launch` has just returned is not identity-checked: on Linux `ps` shows it under its parent's name until it has exec'd. - Decide whether a port is free by connecting to it and binding it, never by listing sockets with `lsof`: a failed or timed-out listing reads as "free". `lsof` is only for naming who holds a port in an error message. - Claim a port (`PortProbe.reserve`) before launching on it and hold the claim until the server answers, or two starts at once pick the same port and the loser reports itself ready on the winner's server. - Never start the server through `@zitadel/server`'s wrapper script. The recorded pid must be the process that holds the port, and `@zitadel/server` owns the one list of platforms and where each binary is. - A port that was asked for is taken as asked or refused, never moved, and it is checked before anything is torn down: a refused move leaves the working server running. - Read a failed start's log before cleaning it up. Cleaning up a container removes its log, which is the only explanation of why it failed. - Handle the spawn `error` event. A binary that cannot be run is reported by an event, not thrown, and unhandled it crashes the CLI with a stack trace. ## Tests No test waits in real time. Code that waits takes a `Clock` (`internal/clock.ts`) and tests pass `ManualClock` from `tests/helpers/clock.ts`; code that needs a deterministic outcome gets it by construction (see the `orphaned` server shape in `tests/helpers/local-server.ts`), not by sleeping. Ports in tests come from `tests/helpers/ports.ts`, never from a bind-and-close probe. Unit tests mirror the file they cover; command behaviour belongs in that command's spec, arranged through `ScaffoldedApp`. The layout rules are in [`tests/AGENTS.md`](../../../tests/AGENTS.md).