Persistent localhost servers (macOS launchd)
One script does everything. Resolve scripts/persistent-localhost.sh relative to this SKILL.md, never from the current directory.
Workflow
- Check what already exists:
scripts/persistent-localhost.sh list
- Start the server.
--nameis the handle.--portenables readiness checks and duplicate handling.--dirdefaults to the current directory.
scripts/persistent-localhost.sh start --name myproj --port 5111 --cmd "./bin/serve"
The command returns only after the port is listening, or fails with the last 20 log lines. Report the printed URL= to the user.
- Inspect, restart, read logs, stop:
scripts/persistent-localhost.sh status myproj
scripts/persistent-localhost.sh restart myproj # after editing code that has no reloader
scripts/persistent-localhost.sh logs myproj 100
scripts/persistent-localhost.sh stop myproj
Hot reload
Prefer the framework's own reloader in --cmd (flask run --debug, vite, uvicorn --reload, node --watch). If the server has none, add --watch. It wraps the command in watchexec -r on the working directory. Never use both. Requires brew install watchexec.
Rules the script enforces
- Runs as
com.persistent-localhost.<name>in the userguidomain vialaunchctl bootstrap. Restart useskickstart -kp, stop usesbootout. No deprecatedload,start,stop, no broadpkill. KeepAliveonly on crash. A clean exit stays down. Throttle 5s so a syntax error cannot spin.- Port already taken by the same app (our own label, or a stray process with the same working directory) is replaced. A different app is left alone and the next free port is used. Read the printed
PORT=. - The caller's
PATHand aPORTvariable are passed into the job. Add more with--env KEY=VAL. - Logs:
~/Library/Logs/persistent-localhost/<name>.log. State:~/Library/Application Support/persistent-localhost/<name>.state. Plist:~/Library/LaunchAgents/.
Fallback
If launchctl bootstrap fails, say so and stop. Do not fall back to nohup or &.