Porthub #electron #macos #dev-tools

A menu bar app whose API is three files in /tmp

Every project on this machine now runs behind portless — a local proxy that replaces localhost:5173 with stable names like agentz.vite.localhost. Which immediately created a small tooling gap: the proxy is invisible. Is it running? What's registered? Why is that route dead? Answering meant a terminal and a portless list, forty times a day.

So this week's side-quest: porthub, a macOS menu bar app for the proxy. Four days of commits, ~750 lines of vanilla JavaScript, no build step, no framework. It's the smallest codebase I've shipped in years and it contains more distilled Electron folklore per line than anything else I've written.

The API is the filesystem

Porthub never talks to the proxy. There's no RPC, no socket, no admin endpoint. Portless keeps its state in /tmp/portless/routes.json and proxy.pid — and porthub simply watches those files:

watcher = fs.watch(PORTLESS_STATE_DIR, (_ev, filename) => {
  if (filename === 'routes.json' || filename === 'proxy.pid') {
    debouncedPushUpdate();   // 300ms debounce
  }
});

with a 5-second polling fallback if fs.watch refuses, and a debounce because file watchers fire in bursts. State-files-as-API has real virtues: the "protocol" is cat-able, the tools compose (anything can read those files), and porthub works even for proxies it didn't start. The cost is that writes are unguarded — porthub edits routes.json directly when it kills a route, and nothing arbitrates concurrent writers. At this scale, a fair trade; worth being honest that it is a trade.

Unix trivia, load-bearing

Determining "is the proxy alive" turned into a tiny museum of process lore. The pid file might be stale, so process.kill(pid, 0) probes the process — but the proxy runs as root (it binds :443), so the probe throws EPERM. Which is the answer:

} catch (e) {
  // EPERM = process exists but is owned by another user (root)
  // ESRCH = no such process
  return e.code === 'EPERM';
}

Permission denied means alive. And because someone (me) sometimes runs the proxy in foreground mode where no pid file is written, there's a pgrep fallback behind the pid file. Route liveness gets the same treatment — routes.json entries carry the registering process's pid, probed the same way, with pid: 0 reserved for static aliases that have no process to probe and therefore can't be killed from the UI.

Starting the proxy needs root, and porthub holds no privileges. The answer is the macOS native move: osascript's do shell script ... with administrator privileges, which produces the system password dialog, runs the one command, and retains nothing. One wrinkle worth recording: under sudo, the NVM-installed portless binary vanishes from PATH — so porthub resolves absolute paths for both portless and node's bin directory before elevating, and passes them through explicitly. Everyone who has ever combined sudo with a version manager just nodded.

Menu bar apps are their own genre

The UI work was its own education. The window is frameless, transparent, vibrancy-blurred, positioned under the tray icon with clamping so it never hangs off a screen edge, and hides on blur — unless the user has popped it out into a floating always-on-top panel, a mode toggle that mostly consists of remembering not to hide on blur anymore. The window resizes itself to its content: the renderer measures its DOM and asks the main process for new bounds over IPC, clamped to sane limits, so the popup is exactly as tall as the route list.

Routes group by DNS prefix — everything under agentz.* folds together, with a tray-menu toggle for showing full hostnames versus short names. And the tray icon itself consumed an embarrassing commit: macOS wants a menu bar icon sized in points (18pt, with a 2x retina variant), not "whatever pixels you have", and until you comply your icon renders as either a postage stamp or a billboard.

The whole thing sits in the tray showing a route count, one click from a route list where each row is click-to-open, one right-click from start/stop/restart. The proxy stopped being invisible, which was the entire brief.

The meta-note

Porthub is a tool for a tool — infrastructure for infrastructure — and the temptation with those is always to skip them ("it's just for me"). The four days argue otherwise: every project here now leans on portless, and the cost of the proxy being opaque was a dozen tiny investigations a week, each starting from zero. A control surface converts that recurring cost into a glance. Tools for your tools pay rent faster than almost anything, if you keep them small enough to finish. 750 lines. No framework. Done.