pm
pm run -- npm run dev puts the process in a systemd unit, discovers the port it opened on its own, resolves collisions and publishes project.localhost via Caddy. One core, four faces: CLI, web, HTTP API and 13 MCP tools. Built for WSL2 + systemd + Chrome.
Everyone wants port 3000. pm gives each project a name and handles the port itself.
A Go binary that knows what runs on which port, starts and stops projects of any stack and publishes them as project.localhost. People use it through the CLI and the web, agents through MCP, all with the same verbs.
From a command in the terminal to a name in the browser
Click through the steps or let them advance. Each step lights up the piece it talks about.
The minimal version that works: the same command as always, with a prefix in front.
One core, four faces
CLI, web dashboard, HTTP API and MCP server are projections of the same internal/api layer. A feature is implemented once and shows up in all four.
A morning with three projects and a container
A made-up case with small numbers, to see what happens at each stage before getting into the challenges.
pm run -- pnpm dev. There is no record: scan sees next in package.json and saves a draft with port 3000 and the -p strategy. pm-tienda.service starts, the detector sees the LISTEN on 3000 and Caddy publishes tienda.localhost.port 3000 taken → using 3001 and injects -- -p 3001. landing.localhost works the same. Ana did not open a single config file.manage.py runserver the port is positional: 0.0.0.0:8000. The stack decides that, not Ana. api.localhost is routed.pm claim 8080 adopts it and from then on it has a name and a stop button.systemctl --user stop pm-landing takes pnpm, node and their workers down in one go. Before leaving, pm promote api: the unit becomes permanent and will start on its own at the next boot.pm_list before assuming a port is free, starts tienda with pm_start instead of improvising a background dev server, and reads pm_logs when something does not come up.Five times the obvious solution was wrong
Each challenge follows the same pattern: what anyone would try first, why it does not work, what was done and the number that sums it up. The technical detail stays folded.
The whole tool answers that question for the entire machine, and answers it the same way to a person in the terminal, a browser tab and an agent over MCP. What the system already solves (systemd, Caddy, Chrome with
*.localhost) is not reimplemented.A single filter: does the system already do this?
If the answer was yes, the system was used. If not, the minimum was written and a seam was left to test it with fakes.
Go deeper
pm promote and pm demote are a change of unit type, not a custom state machine. From Windows, a pm.cmd shim on the PATH forwards to wsl.exe -e pm; pm ui and pm open open Windows Chrome at pm.localhost.
*.localhost on its own, and a proxy on :80 inside WSL turns tienda.localhost into the real port. Caddy already knows how to reload configuration through its admin API without dropping connections; writing that by hand would mean maintaining a reverse proxy inside a port manager.Go deeper
The daemon reconciles the Caddyfile with the state on every change and Reload is a silent no-op if Caddy is not installed: everything else keeps working by port. PM_NO_PROXY=1 disables it for secondary daemons and tests.
:2019.go:embed. Edit, make build, done. Real time goes over SSE, not polling: state, start/stop, promote/demote, streaming logs, adoption of unmanaged listeners, groups and full CRUD. The tone is that of an always-open tab with a live table.Go deeper
A frontend with a toolchain would have pulled node into the build of a Go binary whose whole point is installing with make install. The price is a hand-written HTML file that grows; as long as it fits on one screen of code, it pays off.
.mcp.json. The 13 tools are the CLI verbs over the same internal/api layer: an agent can do exactly what a person would do in the terminal, without a second implementation that drifts.Go deeper
Streamable HTTP transport with mark3labs/mcp-go. What an agent gains is real freedom to operate processes: check what is running before assuming a port is free, start the project instead of improvising a background dev server, and read the logs when something does not come up.
*.localhost names to Node servers. pm names any stack, resolves collisions and syncs Caddy from the live state, not from a wrapper.