Documentation
lewp CLI reference
One binary, two roles: the CLI client you type commands into, and the launchd-managed daemon that does the routing.
Most commands talk to the daemon over a local control socket at ~/Library/Application Support/lewp/control.sock.
Build and Install
curl -fsSL https://raw.githubusercontent.com/scottwater/lewp/main/install.sh | bash go install github.com/scottwater/lewp/cmd/lewp@latest bin/build bin/install bin/reinstall
install.sh downloads the latest GitHub release archive for the current Mac architecture and installs lewp into ~/.local/bin by default. Set LEWP_INSTALL_DIR to install elsewhere. go install builds from source. bin/install installs a local checkout build; bin/reinstall runs build then install.
Commands
lewp setup [--suffix S] [--allow-domain-mirror] [--log-requests errors|all] lewp system start|stop|status|restart lewp system uninstall [--yes] lewp lease [--root R] [--name N] [--host H] [--auto-suffix] [--reset] [--json|--shell] lewp alias add <host> [--json] lewp alias remove <host> lewp alias list [--json] lewp init [--root R] [--name N] [--host H] [--force] lewp info [--name N] [--host H] [--json|--shell|--port] lewp move --from <path> [--json] lewp port [--name N] [--json|--shell] lewp release [--route | --port [<name>]] [--forget] lewp list [--all] [--json] lewp suffix list lewp suffix remove S lewp doctor lewp logs [--lines N] [--grep TEXT] [--follow] [--path] lewp completion bash|zsh|fish lewp version lewp upgrade lewp daemon
Every command accepts --help (alias -h) for command-specific usage, flags, and examples. lewp, lewp help, and lewp --help print the top-level help. An unknown command prints the top-level help to stderr and exits 2.
lewp — local domain router and port leaser for parallel development Lewp leases a stable loopback port and a predictable <instance>.<root>.lewp hostname for the current directory, then reverse-proxies browser traffic to a process you start yourself. It routes and leases only; it never starts apps. Usage: lewp <command> [flags] Commands: setup Install the .lewp DNS resolver, local CA, and launchd service system Manage the daemon: start|stop|status|restart|uninstall lease Lease a stable port and managed hostname for the current directory alias Manage extra hostnames for the current route init Generate a .lewp.local.toml identity file for the current directory info Show routes and bare ports registered for the current directory move Move a route from another directory to the current directory port Lease a bare internal port without a hostname release Release the route and bare ports for the current directory list List active routes and their health suffix List or remove custom managed suffixes doctor Diagnose daemon state, DNS, and CA trust logs Show or tail the daemon logs completion Print a shell completion script (bash|zsh|fish) version Print version upgrade Replace this binary with the latest GitHub release daemon Run the daemon in the foreground (normally launchd-managed)
Configuration is resolved highest priority first: flags (--root, --name, --host), then environment (LEWP_ROOT, LEWP_NAME, LEWP_HOST), then the nearest .lewp.local.toml up the tree, then inference (parent dir → root, current dir → name). The .lewp.local.toml file is meant to stay local/uncommitted — run lewp init to generate one.
lewp setup
One-time macOS setup for .lewp routing and HTTPS. It writes /etc/resolver/lewp (via sudo when needed), creates the local CA under ~/Library/Application Support/lewp/, installs the launchd plist, and trusts the CA with the macOS security tool. Run lewp system start afterward.
lewp setup [--suffix <suffix>] [--allow-domain-mirror] [--start]
[--log-requests errors|all]
.lewp is built in and always installed. --suffix S additively installs a resolver for an owned public dev suffix — useful when an OAuth provider rejects private TLDs. Custom suffixes default to safe-subtree mode; domain-mirror mode must be opted into with --allow-domain-mirror because it shadows public DNS locally.
lewp setup --suffix local.todoordie.com # safe-subtree lewp setup --suffix localkickofflabs.com --allow-domain-mirror # domain mirror
--log-requests errors|all sets how much request traffic the daemon logs (persisted in the launchd plist). The default, errors, logs only failed requests so a steady stream of successful HMR/SSE/websocket traffic cannot grow the log without bound. Restart the daemon for a change to take effect.
DNS=resolver-file HTTPS=enabled LAUNCHD=/Users/scott/Library/LaunchAgents/dev.lewp.daemon.plist RESOLVER=/etc/resolver/lewp CA=/Users/scott/Library/Application Support/lewp/ca.pem LOGS=/Users/scott/Library/Logs/lewp
lewp system
Manage the macOS LaunchAgent: start | stop | restart | status | uninstall. Each action runs the matching launchctl command and prints it. If start sees bootstrap status 5 because the job is already loaded, it falls back to launchctl kickstart -k.
lewp system start lewp system status lewp system uninstall --yes
uninstall is destructive, so it first prints an affected-file summary (launchd job, keychain trust, resolver files, CA material it keeps) before doing any work, then asks for confirmation. Non-interactively it refuses unless you pass --yes (or -y), so an unattended uninstall can never silently tear down the integration.
lewp daemon is running
lewp suffix
List and remove custom managed suffixes. .lewp is built in and cannot be removed. Custom suffixes are added with lewp setup --suffix S; suffix remove S removes the suffix from config and deletes its resolver file after confirming the resolver is Lewp-owned.
lewp suffix list lewp suffix remove local.todoordie.com
SUFFIX MODE lewp built-in local.todoordie.com safe-subtree localkickofflabs.com domain-mirror
lewp lease
Lease a routed app port and a Lewp-managed host for the current directory. This is the command for creating a route. Re-running from the same directory returns the same port and host. By default the host is under .lewp; root and name are inferred from the directory layout unless overridden.
lewp lease lewp lease --root atlas --name feature-1 lewp lease --host atlas.lewp lewp lease --json lewp lease --shell
PORT=42137 URL=http://feature-1.atlas.lewp HTTPS_URL=https://feature-1.atlas.lewp HOST=feature-1.atlas.lewp STATE=new HOST_KIND=instance
URL is always http://<host> and HTTPS_URL is https://<host>; both are emitted whenever the lease has a routable host. STATE is new, reused, or conflict-renamed; HOST_KIND is instance, apex, or custom. --shell prefixes assignable values with export and omits descriptive lines, so eval "$(lewp lease --shell)" sets only the variables you want.
export PORT=42137 export URL=http://feature-1.atlas.lewp export HTTPS_URL=https://feature-1.atlas.lewp export HOST=feature-1.atlas.lewp
Identity is discovered from the first source that provides each of root/name/host: flags, then LEWP_* environment, then the nearest .lewp.local.toml, then path/git-worktree inference. Once you pass --root, --name, or --host, that value is remembered for the directory. Use --reset to clear a bad override without throwing away the port or history.
lewp lease --reset # re-infer everything, keep port + history lewp lease --reset --root atlas # keep root, re-infer name and host
lewp alias
Attach extra hostnames to the current directory’s active route. Aliases reuse the route’s existing port and process — they do not allocate another port, create another primary route, or start anything. Run lewp lease first; alias add requires an active route.
lewp alias add tags.feature-1.atlas.lewp lewp alias add '*.feature-1.atlas.lewp' lewp alias list lewp alias remove tags.feature-1.atlas.lewp
Use aliases when one app process serves multiple hostnames, such as Rails subdomain routing. Wildcards match exactly one label: *.app.lewp matches tags.app.lewp, but not app.lewp and not api.tags.app.lewp. Aliases that conflict with an existing active host are rejected with the conflicting path.
PORT=42137 URL=http://tags.feature-1.atlas.lewp HTTPS_URL=https://tags.feature-1.atlas.lewp HOST=tags.feature-1.atlas.lewp STATE=new HOST_KIND=alias
lewp init
Generate a .lewp.local.toml for the current directory so its identity is explicit and stable regardless of how the folder is laid out. init only writes the file — it never contacts the daemon, allocates a port, or leases a route. Values come from flags first, then path inference.
lewp init lewp init --root atlas --name feature-1 lewp init --host atlas.lewp lewp init --force
root = "atlas" name = "feature-1" host = "atlas.lewp"
The file is meant to stay uncommitted; init prints the .git/info/exclude line that keeps it out of version control. Without --force, init refuses to clobber an existing .lewp.local.toml.
lewp info
Show routes and bare ports registered for the current directory. info reads existing registry data only — it never infers, allocates, or mutates a route or port. Default output is the same table shape as lewp list, scoped to this directory.
HOST NAME KIND PORT STATE PATH feature-1.atlas.lewp feature-1 route 42137 up .../atlas/feature-1 tags.feature-1.atlas.lewp feature-1 alias 42137 up .../atlas/feature-1 - vite port 42138 down .../atlas/feature-1
--name filters by logical name; --host filters by exact host. --port emits only the port digits (requires exactly one match); --shell emits eval-safe exports for exactly one match. If nothing is registered, info exits non-zero and points you at lewp lease, lewp port, or lewp move.
PORT="$(lewp info --host feature-1.atlas.lewp --port)" eval "$(lewp info --host feature-1.atlas.lewp --shell)"
lewp move
Move an existing route from another directory to the current directory, keeping the same primary host, aliases, wildcards, and port. Use this after relocating or renaming a project folder so its stable URLs follow it. --from is required and names the directory that currently owns the route.
lewp move --from ~/projects/atlas/old-feature lewp move --from ~/projects/atlas/old-feature --json
The move is atomic: the source directory is left with no route and the current directory becomes the owner with the same port and hosts. The port is never reallocated. If the source has no active route, or the current directory already owns one, move fails and changes nothing.
lewp port
Lease a stable bare internal port with no hostname or proxy route — useful for sidecar processes (asset bundlers, internal APIs) that need a stable port but no .lewp host. With no --name, port leases under the default name "port", so repeated calls from the same directory return the same number.
lewp port lewp port --name vite eval "$(lewp port --name vite --shell)" export VITE_RUBY_PORT="$(lewp port --name vite --json | jq -r .port)"
port prints the same env-style output as lease minus the host fields. To free a bare port, use lewp release --port <name>; plain lewp release frees the route and every bare port at once.
lewp release
Release everything Lewp holds for the current directory: the route (including its aliases and wildcard hosts) and every bare port. It is the full inverse of lewp lease plus any lewp port leases.
lewp release # everything lewp release --route # only the route + aliases, keep bare ports lewp release --port vite # only one bare port lewp release --forget # also drop remembered identity/history
--route and --port cannot be combined. Without --forget, history stays in the registry. Release is idempotent: when nothing is active it reports "no active route or port for this directory" and exits 0.
lewp list
List registry entries (routes, aliases, and bare ports) with their health. States are derived from TCP checks. The default list hides released routes; --all includes registry history. KIND is route, alias, or port; a bare port has no host, so HOST shows "-".
HOST NAME KIND PORT STATE PATH feature-1.atlas.lewp feature-1 route 42137 up .../atlas/feature-1 tags.feature-1.atlas.lewp feature-1 alias 42137 up .../atlas/feature-1 - vite port 42138 down .../atlas/feature-1
States: up (TCP connect succeeds), down (connect fails), stale (registered path no longer exists), released (route was explicitly released). --json emits a JSON array (always an array, never null), each entry with host, port, state, path, kind, root, and name.
lewp list --json | jq '.[] | select(.kind == "alias")'
lewp doctor
Diagnose daemon state, DNS, and CA trust. It reports daemon and control-socket reachability, resolver configuration, and whether the local CA is trusted in the keychain. It also compares the binary launchd is configured to run against the CLI you are running now.
daemon: ok control socket: ok https: configured (local CA present) keychain: trusted cli binary: /usr/local/bin/lewp installed program matches this CLI current version: 0.1.0 installed version: 0.1.0
When the daemon is up, doctor resolves probe.lewp through the macOS system resolver — the same path browsers use — and warns (with sudo killall -HUP mDNSResponder as the recovery hint) when the name does not resolve. If launchd runs a different binary than the current CLI it prints a mismatch line pointing at bin/reinstall and lewp system restart.
mismatch: launchd runs /old/bin/lewp but this CLI is /usr/local/bin/lewp — run bin/reinstall and lewp system restart
lewp logs
Show or tail the launchd-managed daemon’s logs under ~/Library/Logs/lewp. By default the daemon’s stdout carries a request line only for failed requests; startup failures land on stderr. lewp logs only reads files — it never starts the daemon.
lewp logs # last 200 lines of each log lewp logs --lines 500 lewp logs --grep error # case-insensitive filter lewp logs --grep 502 --lines 50 lewp logs --follow # stream new output (alias: -f) lewp logs --path # print the two log file paths
--grep TEXT keeps only lines containing TEXT (case-insensitive); --lines then bounds the matching lines, so --grep TEXT --lines N behaves like grep TEXT | tail -n N. To log every proxied request, run lewp setup --log-requests=all.
lewp completion
Print a static shell completion script to stdout for bash, zsh, or fish. The script is self-contained and never contacts the daemon. A missing or unsupported shell argument exits 2 with guidance.
lewp completion bash > /usr/local/etc/bash_completion.d/lewp
lewp completion zsh > "${fpath[1]}/_lewp" # then restart zsh
lewp completion fish > ~/.config/fish/completions/lewp.fish
lewp version
Print the public version. Add --detailed for development/build metadata. version, --version, and -v are equivalent. The version is injected at link time by bin/build; an un-stamped go build reports "dev".
lewp version 0.1.0 commit: 0a778ce built: 2026-06-27T23:08:00Z go: go1.25.1
lewp upgrade
Download the latest GitHub release archive for this Mac, extract the lewp binary, and atomically replace the executable currently running the command. If the current binary already matches the latest release, upgrade reports it is up to date and exits 0.
lewp upgrade lewp system restart # so launchd runs the new binary
lewp daemon
Run the daemon in the foreground. Normally launchd starts this command. The daemon owns the SQLite registry, the Unix control socket, HTTP/HTTPS proxy listeners from launchd, and SNI certificate minting from the persisted local CA.
The daemon logs failed requests (proxy errors and statuses ≥ 400) to stdout, captured in daemon.out.log; inspect it with lewp logs. Pass --log-requests=all to log every proxied request instead. Startup errors are written to stderr (daemon.err.log).
.lewp error pages
The proxy serves debuggable HTML rather than blank gateway errors for the two common failure modes. Both pages share a small, framework-neutral inline stylesheet (no external assets, no web fonts, no JavaScript) that renders in light and dark mode.
Registered but not responding: when a host is leased but its target port is closed, the proxy returns 502 with the host, loopback target, project path, root/name, last-seen time, release state, a copyable PORT=<n> <your dev command> start command, and lewp list / lewp doctor hints.
Unregistered managed host: when a host under .lewp or a configured suffix has no route, the proxy returns 404 with parsed labels and next steps. Unmanaged hosts get a plain 404. The host is HTML-escaped so a crafted hostname cannot inject markup.
Browser URLs
DNS resolves all .lewp names and configured custom suffixes to loopback. The proxy routes by full registered host. HTTPS uses Lewp’s local CA, created and trusted by lewp setup, which mints certificates for .lewp SNI names and safe-subtree custom suffixes. Domain-mirror suffixes always route over HTTP.
http://<host> https://<host> # .lewp and safe-subtree custom suffix hosts
Safari and Chromium browsers (Chrome, Brave, Arc, Edge, Helium) trust the macOS system keychain, so https://<host> works as soon as lewp setup has run. Firefox is not supported in V1 — it uses its own NSS trust store, so use http://<host> in Firefox or switch to Safari/Chromium for HTTPS.
Troubleshooting
Daemon not running: lewp daemon is not running → run lewp system start.
Target app not running: lewp list shows the route as down, and the browser shows a Lewp debug page with host, target, path, and hints. Start your app manually with the leased PORT.
HTTPS warnings: run lewp setup, restart the browser if it cached trust state, or run lewp system uninstall to remove Lewp CA trust, the LaunchAgent plist, and resolver file.
Port collisions: Lewp never steals another remembered live assignment. Conflicting primary hosts receive deterministic suffixes; conflicting aliases and wildcards are rejected with the conflicting path. Use lewp release --forget from old folders to remove stale ownership.