Skip to content

usm/docs-serve-port-check [built] ​

Add port availability checking, already-serving detection, watch mode for auto-regeneration, graceful shutdown, and browser-open to usm docs serve — making the dev loop seamless for both developer and help audience docs.

Status: built

Why this exists ​

usm docs serve currently has several gaps in the dev loop: (1) no port availability check — VitePress fails with a raw EADDRINUSE if the port is taken; (2) no detection of an already-running server — running serve twice silently collides; (3) no watch mode — after usm generate, the user must manually restart or rely on VitePress's file watcher alone; (4) no graceful shutdown — Ctrl+C may leave the VitePress child dangling; (5) no browser-open convenience. This feature addresses all five, making the serve experience production-quality.

Design decisions ​

no-dependency [accepted] ​

Decision: Implement port probing with Node.js built-in net module — no third-party dependency like detect-port or chokidar.

Rationale: Keeps the dependency footprint zero. net.createServer().listen() for port probing and fs.watch for file watching are both built-in and sufficient.

auto-port-opt-in [accepted] ​

Decision: Auto-port selection is opt-in via --auto-port flag, not the default.

Rationale: Users expect --port 5173 to mean port 5173. Silently switching ports would break bookmarks, CORS configs, and automation scripts.

lsof-best-effort [accepted] ​

Decision: Process detection via lsof is best-effort only — graceful degradation if lsof is unavailable.

Rationale: lsof may not be installed in minimal Docker images or on Windows. The error message should still be clear without it, just omitting the process name.

pid-file-approach [accepted] ​

Decision: Use a PID file (.usm-workspace/.vitepress.pid) plus a port file (.vitepress.port) for already-serving detection, not port-sniffing alone.

Rationale: Port-sniffing can't distinguish between a VitePress server and another process on the same port. The PID file gives certainty and enables usm docs stop; the port file records the actually-bound port so status/serve messages never echo a requested-but-unbound port (wrong-URL bug, 2026-09-25).

watch-in-process [accepted] ​

Decision: Watch-mode regeneration runs usm generate --only docs in-process (calling the generator functions directly), not as a subprocess.

Rationale: Avoids the overhead and complexity of spawning a child process. The generator functions are already imported and callable.

debounce-500ms [accepted] ​

Decision: Watch-mode debounce is 500ms — a pragmatic balance between responsiveness and avoiding thrash on bulk writes.

Rationale: git checkout, usm scan, and editor auto-save can touch many files in rapid succession. 500ms is fast enough to feel instant but long enough to batch.

auto-port-default [accepted] ​

Decision: Auto-port is the DEFAULT when --port is omitted; an explicit --port N is strict and fails loudly if N is taken.

Rationale: Supersedes auto-port-opt-in (2026-09-25). Real usage: many USM projects run docs servers concurrently on one machine; port clashes were the actual pain (servers drifting to unexpected ports, agents verifying the wrong server). Nobody bookmarks hot-reloading dev docs. Omitting --port now means 'give me a free port and tell me which one'. Explicit --port still means exactly that port — but with --strictPort so announced==bound, always.

How it works ​

Pre-flight port availability check (port-check) ​

Before spawning VitePress, probe the target port. If in use, either fail with a clear message or auto-select the next free port.

  1. Add — Port probe using net.createServer().listen() before spawning VitePress
  2. Add — If port is free, proceed as normal (no extra output)
  3. Add — If port is in use and --auto-port flag is set, find next free port and use it
  4. Add — If port is in use and --auto-port is not set, fail with clear message: 'Port X is in use by process Y. Use --port N or --auto-port.'
  5. Add — Detect the process using the port (via lsof on macOS/Linux) for the error message — best-effort, graceful degradation if lsof unavailable
  6. Apply — Works for both --audience developer and --audience help

Detect already-running server (already-serving) ​

Before starting, check if a VitePress server is already running on the target port. If it is, offer to reuse it or kill and restart.

  1. Add — Write a PID file to .usm-workspace/.vitepress.pid on server start
  2. Add — On serve, check if PID file exists and the process is still alive
  3. Add — If already serving on the target port, print: 'Docs already served at http://localhost:X (PID Y). Use --restart to restart.'
  4. Add — Add --restart flag to kill the existing server and start fresh
  5. Add — Clean up PID file on graceful shutdown (SIGINT/SIGTERM)
  6. Add — Add usm docs stop command to kill a running server by PID file

Watch mode for auto-regeneration (watch-mode) ​

Add --watch flag that watches .usm/ files and re-runs usm generate before VitePress picks up the changes, keeping docs always in sync.

  1. Add — --watch flag on usm docs serve
  2. Add — Use fs.watch on .usm/ directory recursively
  3. Add — On .usm file change, run usm generate --only docs (in-process, not a subprocess)
  4. Add — Debounce: wait 500ms after last change before regenerating
  5. Add — Log: 'Regenerated docs (3 files changed)' after each watch-triggered generation
  6. Add — VitePress HMR picks up the changed markdown files automatically

Quality-of-life improvements (quality-of-life) ​

Graceful shutdown, browser-open, and status command.

  1. Add — --open flag to auto-open browser at the served URL
  2. Add — Graceful shutdown: SIGINT/SIGTERM handler that kills the VitePress child and removes the PID file
  3. Add — usm docs status command: checks PID file, prints 'Served at http://localhost:X (PID Y)' or 'Not running'
  4. Add — usm docs stop command: reads PID file, kills the process, removes PID file

Guarantees ​

port-check-clear-error ​

When an EXPLICITLY requested --port is in use, the user gets a clear, actionable error message — explicit requests are strict.

Acceptance criteria:

  • [ ] Error message includes the port number
  • [ ] Error message includes the process name/PID using the port (when detectable via lsof)
  • [ ] Error message suggests --auto-port (omit --port) or --restart as remedies
  • [ ] Exit code is non-zero

auto-port-selection ​

When no --port is given, the next available port is selected automatically — concurrent instances never clash.

Acceptance criteria:

  • [ ] Default port base is 5173; if taken, probe incrementing until free (max 100 probes)
  • [ ] Logs the selected port clearly (e.g. 'Port 5173 in use, using port 5174 instead')
  • [ ] Works for both --audience developer and --audience help
  • [ ] The announced URL is always the bound URL (USM probes and passes --strictPort to VitePress; bind failure fails loudly, never silently drifts)

already-serving-detection ​

Running serve when a server is already up is handled gracefully.

Acceptance criteria:

  • [ ] PID file written to .usm-workspace/.vitepress.pid on start
  • [ ] On serve, detects existing server and prints its URL + PID
  • [ ] Does not start a second server unless --restart is passed
  • [ ] --restart kills the old server before starting a new one
  • [ ] PID file cleaned up on shutdown (SIGINT, SIGTERM, normal exit)

watch-mode-regeneration ​

--watch keeps docs in sync with .usm changes automatically.

Acceptance criteria:

  • [ ] Watches .usm/ directory recursively for .usm file changes
  • [ ] Debounces regeneration (500ms after last change)
  • [ ] Runs usm generate --only docs in-process (not a subprocess)
  • [ ] Logs regeneration summary (file count)
  • [ ] VitePress HMR picks up changed markdown files

qol-commands ​

usm docs status, usm docs stop, --open, and graceful shutdown all work.

Acceptance criteria:

  • [ ] usm docs status prints server URL + PID or 'Not running'
  • [ ] usm docs stop kills the server and removes PID file
  • [ ] --open flag opens browser at the served URL
  • [ ] SIGINT/SIGTERM kills VitePress child and removes PID file
  • [ ] usm docs build is unaffected by all changes

no-breakage ​

Existing behavior is unchanged when the port is free and no server is running.

Acceptance criteria:

  • [ ] When port is free, behavior is identical to current (no extra output)
  • [ ] usm docs serve --port 5173 works exactly as before when 5173 is free
  • [ ] usm docs build is unaffected
  • [ ] No new required dependencies (net, fs, child_process are all built-in)

Test specifications ​

port-free-passes-through ​

Given:

  • port_5173_is_free: true
  • run_docs_serve: true

Then:

  • assertion: VitePress starts on port 5173
  • assertion: No port-related warnings or errors in output

port-in-use-clear-error ​

Given:

  • port_5173_is_in_use: true
  • run_docs_serve_no_auto_port: true

Then:

  • assertion: Command fails with non-zero exit code
  • assertion: Error message mentions port 5173
  • assertion: Error message suggests --port N, --auto-port, or --restart

auto-port-selects-next ​

Given:

  • port_5173_is_in_use: true
  • port_5174_is_free: true
  • run_docs_serve_no_port_flag: true

Then:

  • assertion: VitePress starts on port 5174
  • assertion: Log message: 'Port 5173 in use, using port 5174 instead'

auto-port-safety-limit ​

Given:

  • ports_5173_to_5272_all_in_use: true
  • run_docs_serve_no_port_flag: true

Then:

  • assertion: Command fails after probing 100 ports
  • assertion: Error message: 'No free port found between 5173 and 5272'

already-serving-detected ​

Given:

  • server_already_running_on_5173: true
  • run_docs_serve: true

Then:

  • assertion: Prints 'Docs already served at http://localhost:<bound-port> (PID X)' using the port file, not the requested port
  • assertion: Does not start a second VitePress process

restart-kills-and-restarts ​

Given:

  • server_already_running_on_5173: true
  • run_docs_serve_with_restart: true

Then:

  • assertion: Old server process is killed
  • assertion: New VitePress starts on port 5173
  • assertion: PID file updated with new PID

watch-regenerates-on-change ​

Given:

  • server_running_with_watch: true
  • modify_usm_file: true

Then:

  • assertion: Log message: 'Regenerated docs (1 file changed)'
  • assertion: VitePress HMR picks up the change

docs-status-shows-running ​

Given:

  • server_running_on_5173: true
  • run_docs_status: true

Then:

docs-status-shows-not-running ​

Given:

  • no_server_running: true
  • run_docs_status: true

Then:

  • assertion: Prints 'Not running'

docs-stop-kills-server ​

Given:

  • server_running_on_5173: true
  • run_docs_stop: true

Then:

  • assertion: Server process is killed
  • assertion: PID file is removed
  • assertion: Prints 'Stopped docs server (was PID X)'

graceful-shutdown-cleans-up ​

Given:

  • server_running: true
  • send_sigint: true

Then:

  • assertion: VitePress child process is killed
  • assertion: PID file is removed
  • assertion: Exit code is 0

help-audience-works ​

Given:

  • run_docs_serve_help_audience: true

Then:

  • assertion: Port check, PID file, and all features work for --audience help

strict-port-bind-failure ​

Given:

  • explicit_port_requested: true
  • port_free_at_preflight_check: true
  • port_taken_by_the_time_vitepress_binds: true

Then:

  • assertion: Command fails with non-zero exit code, never silently moves to another port
  • assertion: Error output mentions the requested port and remedies (--auto-port by omitting --port, --restart)

Implementation ​

  • Primary: src/cli/docs.ts; src/cli/index.ts
  • Test code status: none

See Also ​

  • usm/cli-docs