Skip to content

Portless

tavlean edited this page Jul 25, 2026 · 6 revisions

Portless

Custom local domain detection. In src/aliases.ts.

Portless maps custom local domains to localhost ports. We shell out to portless list and surface the named domain (e.g. myapp.localhost) as the row title, demoting localhost:PORT to a pill accessory.

Why parse text output, not state files

Portless's ~/.portless/ state files are internal. The text output is its public, human-readable interface, and far more stable.

Resolve once, execute directly

The portless binary and login-shell PATH are resolved once and persisted in Raycast's cache. Earlier versions spawned /bin/zsh -ilc "portless list" on every poll, which re-sourced ~/.zshrc and made nvm-heavy shells dominate refresh time. On a typical nvm setup this moves the portless step from roughly 1s to 100ms per poll.

The cached PATH still matters because the portless script's #!/usr/bin/env node needs to find the same Node the login shell would have found.

Hard 3s timeout

The call sits inside fetchServers' Promise.all. A hung portless daemon would otherwise stall every dashboard refresh. On timeout we fall back to plain localhost:PORT rows.

Optional, not required

Every failure mode (not installed, command-not-found, timeout, parse error) silently returns an empty map. The UI degrades to plain localhost:PORT rows. Portless is treated as an enhancement, never a runtime dependency.

The proxy must already be running to start a wrapped dev script

A dev script wrapped in portless run needs the portless proxy up before we spawn it. When the proxy is down, portless tries to auto-start it, finds no TTY to run sudo on, and exits before the framework ever boots:

Proxy is not running and no TTY is available for sudo.

Our spawn is always detached with stdio pointed at a log file, so there is never a TTY. This is the common failure right after a reboot, and it is why diagnoseSpawnFailure names it on the failure toast, for starts and restarts alike (see Spawn Flow).

Match on the whole sentence, not the Proxy is not running prefix. Portless prints that prefix from three places, and only this one means a spawn died for want of a TTY:

Source Line Means a failed start?
failed auto-start Proxy is not running and no TTY is available for sudo. yes
portless proxy stop Proxy is not running. no
portless doctor Proxy is not running on port 443. no

A dev script that shells out to doctor or proxy stop around the server would otherwise put the prefix in the log and get a slow boot reported as a downed proxy.

The fix is on the user's machine rather than in the extension: portless service install registers a startup service so the proxy is up from boot. The toast carries it as a Copy Fix Command action rather than printing it, since toast text gets truncated and a button does not.

A NOPASSWD sudoers rule does not help. Portless checks for a TTY before it attempts sudo, so sudo -n never gets tried.

Portless daemon filtered out

The portless proxy daemon itself is a node process out of node_modules/portless/ that binds 80/443/1355. Without filtering it would appear as a phantom dev server row. See the tool === "portless" skip in fetchServers.

Child processes spawned by portless (next, vite, etc.) resolve to their own framework binary, so they're shown correctly as themselves rather than as "portless".

Cross-platform note

The current resolver uses /bin/zsh -ilc once, then executes the resolved binary directly. Windows port would need the equivalent resolution through PowerShell; see Process Detection.

Clone this wiki locally