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.
- add → Port probe using net.createServer().listen() before spawning VitePress
- add → If port is free, proceed as normal (no extra output)
- add → If port is in use and --auto-port flag is set, find next free port and use it
- 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.'
- add → Detect the process using the port (via lsof on macOS/Linux) for the error message — best-effort, graceful degradation if lsof unavailable
- 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.
- add → Write a PID file to .usm-workspace/.vitepress.pid on server start
- add → On serve, check if PID file exists and the process is still alive
- add → If already serving on the target port, print: 'Docs already served at http://localhost:X (PID Y). Use --restart to restart.'
- add → Add --restart flag to kill the existing server and start fresh
- add → Clean up PID file on graceful shutdown (SIGINT/SIGTERM)
- 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.
- add → --watch flag on usm docs serve
- add → Use fs.watch on .usm/ directory recursively
- add → On .usm file change, run usm generate --only docs (in-process, not a subprocess)
- add → Debounce: wait 500ms after last change before regenerating
- add → Log: 'Regenerated docs (3 files changed)' after each watch-triggered generation
- add → VitePress HMR picks up the changed markdown files automatically
Quality-of-life improvements (quality-of-life)
Graceful shutdown, browser-open, and status command.
- add → --open flag to auto-open browser at the served URL
- add → Graceful shutdown: SIGINT/SIGTERM handler that kills the VitePress child and removes the PID file
- add → usm docs status command: checks PID file, prints 'Served at http://localhost:X (PID Y)' or 'Not running'
- 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:
- assertion: Prints 'Served at http://localhost:5173 (PID X)'
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