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

Intent

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.

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) 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. A PID file gives us certainty and enables usm docs stop.

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.

Flows

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

Contracts

port-check-clear-error

When the port is in use and --auto-port is not set, the user gets a clear, actionable error message.

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 --port N, --auto-port, or --restart as remedies
  • [ ] Exit code is non-zero

auto-port-selection

When --auto-port is set, the next available port is selected automatically.

Acceptance criteria:

  • [ ] Probes ports starting from the requested port, incrementing until one is free
  • [ ] Logs the selected port clearly (e.g. 'Port 5173 in use, using port 5174 instead')
  • [ ] Works for both --audience developer and --audience help
  • [ ] Does not probe more than 100 ports (safety limit)

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)

Tests

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_with_auto_port: 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_with_auto_port: 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:5173 (PID X)'
  • 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

Implementation

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

See Also

  • usm/cli-docs