Skip to content

Commit bf11f42

Browse files
authored
feat(portless): add Portless plugin to marketplace (#157)
Add Portless plugin from vercel-labs/portless that replaces port numbers with stable, named .localhost URLs for dev servers. Includes marketplace configuration, README entry, release-please config, plugin.json, and skills for intelligent activation.
1 parent a826814 commit bf11f42

6 files changed

Lines changed: 338 additions & 0 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,14 @@
592592
"keywords": ["workflow", "durable", "vercel", "typescript", "ai-agents"],
593593
"tags": ["framework", "workflow"],
594594
"source": "./plugins/workflow"
595+
},
596+
{
597+
"name": "portless",
598+
"description": "Replace port numbers with stable, named local URLs. For humans and agents.",
599+
"category": "development",
600+
"keywords": ["portless", "localhost", "dev-server", "proxy"],
601+
"tags": ["tooling", "dev-server"],
602+
"source": "./plugins/portless"
595603
}
596604
]
597605
}

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,11 @@ Workflow SDK: Build durable, reliable, and observable apps and AI Agents in Type
297297

298298
**Install:** `/plugin install workflow@pleaseai` | **Source:** [plugins/workflow](https://github.com/pleaseai/claude-code-plugins/tree/main/plugins/workflow)
299299

300+
#### Portless
301+
Replace port numbers with stable, named local URLs. For humans and agents.
302+
303+
**Install:** `/plugin install portless@pleaseai` | **Source:** [plugins/portless](https://github.com/pleaseai/claude-code-plugins/tree/main/plugins/portless)
304+
300305
## Quick Start
301306

302307
The fastest way to get started — install the marketplace and let the plugin recommender auto-detect what you need:
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
---
2+
name: portless
3+
description: Set up and use portless for named local dev server URLs (e.g. https://myapp.localhost instead of http://localhost:3000). Use when integrating portless into a project, configuring dev server names, setting up the local proxy, working with .localhost domains, or troubleshooting port/proxy issues.
4+
---
5+
6+
# Portless
7+
8+
Replace port numbers with stable, named .localhost URLs. For humans and agents.
9+
10+
## Why portless
11+
12+
- **Port conflicts**: `EADDRINUSE` when two projects default to the same port
13+
- **Memorizing ports**: which app is on 3001 vs 8080?
14+
- **Refreshing shows the wrong app**: stop one server, start another on the same port, stale tab shows wrong content
15+
- **Monorepo multiplier**: every problem scales with each service in the repo
16+
- **Agents test the wrong port**: AI agents guess or hardcode the wrong port
17+
- **Cookie/storage clashes**: cookies on `localhost` bleed across apps; localStorage lost when ports shift
18+
- **Hardcoded ports in config**: CORS allowlists, OAuth redirects, `.env` files break when ports change
19+
- **Sharing URLs with teammates**: "what port is that on?" becomes a Slack question
20+
- **Browser history is useless**: `localhost:3000` history is a mix of unrelated projects
21+
22+
## Installation
23+
24+
Install globally (recommended) or as a project dev dependency. Do NOT use `npx` or `pnpm dlx` for one-off execution.
25+
26+
```bash
27+
# Global (available everywhere)
28+
npm install -g portless
29+
30+
# Or per-project dev dependency
31+
npm install -D portless
32+
```
33+
34+
When installed per-project, invoke via package.json scripts or `npx portless` (since the package is local, npx will not download anything).
35+
36+
## Quick Start
37+
38+
```bash
39+
# Install globally (or add -D to a project)
40+
npm install -g portless
41+
42+
# Run your app (auto-starts the HTTPS proxy on port 443)
43+
portless run next dev
44+
# -> https://<project>.localhost
45+
46+
# Or with an explicit name
47+
portless myapp next dev
48+
# -> https://myapp.localhost
49+
```
50+
51+
The proxy auto-starts when you run an app. You can also start it explicitly with `portless proxy start`.
52+
53+
## Integration Patterns
54+
55+
### package.json scripts
56+
57+
```json
58+
{
59+
"scripts": {
60+
"dev": "portless run next dev"
61+
}
62+
}
63+
```
64+
65+
The proxy auto-starts when you run an app. Or start it explicitly: `portless proxy start`.
66+
67+
### Multi-app setups with subdomains
68+
69+
```bash
70+
portless myapp next dev # https://myapp.localhost
71+
portless api.myapp pnpm start # https://api.myapp.localhost
72+
portless docs.myapp next dev # https://docs.myapp.localhost
73+
```
74+
75+
By default, only explicitly registered subdomains are routed (strict mode). Start the proxy with `--wildcard` to allow any subdomain of a registered route to fall back to that app (e.g. `tenant1.myapp.localhost` routes to the `myapp` app). Exact matches always take priority over wildcards.
76+
77+
### Git worktrees
78+
79+
`portless run` automatically detects git worktrees. In a linked worktree, the branch name is prepended as a subdomain prefix so each worktree gets a unique URL:
80+
81+
```bash
82+
# Main worktree (no prefix)
83+
portless run next dev # -> https://myapp.localhost
84+
85+
# Linked worktree on branch "fix-ui"
86+
portless run next dev # -> https://fix-ui.myapp.localhost
87+
```
88+
89+
No config changes needed. Put `portless run` in `package.json` once and it works in all worktrees.
90+
91+
### Bypassing portless
92+
93+
Set `PORTLESS=0` to run the command directly without the proxy:
94+
95+
```bash
96+
PORTLESS=0 pnpm dev # Bypasses proxy, uses default port
97+
```
98+
99+
## How It Works
100+
101+
1. `portless proxy start` starts an HTTPS reverse proxy on port 443 as a background daemon. Auto-elevates with sudo on macOS/Linux; falls back to port 1355 if sudo is unavailable. Use `--no-tls` for plain HTTP on port 80. Configurable with `-p` / `--port` or the `PORTLESS_PORT` env var. The proxy also auto-starts when you run an app.
102+
2. `portless <name> <cmd>` assigns a random free port (4000-4999) via the `PORT` env var and registers the app with the proxy
103+
3. The browser hits `https://<name>.localhost`; the proxy forwards to the app's assigned port
104+
105+
`.localhost` domains resolve to `127.0.0.1` natively in Chrome, Firefox, and Edge. Safari relies on the system DNS resolver, which may not handle `.localhost` subdomains on all configurations. Run `portless hosts sync` to add entries to `/etc/hosts` if needed.
106+
107+
Most frameworks (Next.js, Express, Nuxt, etc.) respect the `PORT` env var automatically. For frameworks that ignore `PORT` (Vite, VitePlus, Astro, React Router, Angular, Expo, React Native), portless auto-injects the correct `--port` flag and, when needed, a matching `--host` CLI flag.
108+
109+
### State directory
110+
111+
Portless stores its state (routes, PID file, port file) in a directory that depends on the proxy port:
112+
113+
- **Port < 1024** (sudo required): `/tmp/portless` (macOS/Linux only)
114+
- **Port >= 1024** (no sudo): `~/.portless`
115+
- **Windows**: Always `~/.portless` (no privileged port concept)
116+
117+
Override with the `PORTLESS_STATE_DIR` environment variable.
118+
119+
### Environment variables
120+
121+
| Variable | Description |
122+
| --------------------- | --------------------------------------------------------------------- |
123+
| `PORTLESS_PORT` | Override the default proxy port (default: 443 with HTTPS, 80 without) |
124+
| `PORTLESS_APP_PORT` | Use a fixed port for the app (skip auto-assignment) |
125+
| `PORTLESS_HTTPS` | HTTPS on by default; set to `0` to disable (same as `--no-tls`) |
126+
| `PORTLESS_LAN` | Set to `1` to always enable LAN mode (auto-detects LAN IP) |
127+
| `PORTLESS_TLD` | Use a custom TLD instead of localhost (e.g. test) |
128+
| `PORTLESS_WILDCARD` | Set to `1` to allow unregistered subdomains to fall back to parent |
129+
| `PORTLESS_SYNC_HOSTS` | Set to `0` to disable auto-sync of /etc/hosts (on by default) |
130+
| `PORTLESS_STATE_DIR` | Override the state directory |
131+
| `PORTLESS=0` | Bypass the proxy, run the command directly |
132+
133+
### HTTP/2 + HTTPS
134+
135+
HTTPS with HTTP/2 is enabled by default (faster page loads for dev servers with many files). First run generates a local CA and adds it to the system trust store. After that, no prompts and no browser warnings.
136+
137+
```bash
138+
portless proxy start --cert ./c.pem --key ./k.pem # Use custom certs
139+
portless proxy start --no-tls # Disable HTTPS (plain HTTP)
140+
portless trust # Add CA to trust store later
141+
```
142+
143+
On Linux, `portless trust` supports Debian/Ubuntu, Arch, Fedora/RHEL/CentOS, and openSUSE (via `update-ca-certificates` or `update-ca-trust`). On Windows, it uses `certutil` to add the CA to the system trust store.
144+
145+
### LAN mode
146+
147+
```bash
148+
portless proxy start --lan
149+
portless proxy start --lan --https
150+
portless proxy start --lan --ip 192.168.1.42
151+
```
152+
153+
`--lan` advertises `<name>.local` hostnames over mDNS so any device on the same Wi-Fi can reach your apps. Portless auto-detects your LAN IP and follows network changes automatically, but you can pin a specific address with `--ip <address>` or the `PORTLESS_LAN_IP` environment variable. Set `PORTLESS_LAN=1` to default to LAN mode every time the proxy starts.
154+
155+
Portless remembers LAN mode via `proxy.lan`, so if you stop a LAN proxy and start again, it stays in LAN mode. Other proxy settings still follow the current flags and env vars. Use `PORTLESS_LAN=0` for one start to switch back to `.localhost` mode. If a proxy is already running with different explicit LAN/TLS/TLD settings, portless warns and asks you to stop it first.
156+
157+
LAN mode depends on the system mDNS helpers that portless launches: macOS includes `dns-sd`, while Linux uses `avahi-publish-address` from `avahi-utils` (install via `sudo apt install avahi-utils` or your distro’s tooling).
158+
159+
- **Next.js**: add your `.local` hostnames to `allowedDevOrigins`:
160+
161+
```js
162+
// next.config.js
163+
module.exports = {
164+
allowedDevOrigins: ["myapp.local", "*.myapp.local"],
165+
};
166+
```
167+
168+
- **Expo / React Native**: portless always injects `--port`. React Native also gets `--host 127.0.0.1`. Expo gets `--host localhost` outside LAN mode, but in LAN mode portless leaves Metro on its default LAN host behavior instead of forcing `--host` or `HOST`.
169+
170+
## CLI Reference
171+
172+
| Command | Description |
173+
| -------------------------------------- | -------------------------------------------------------------- |
174+
| `portless run <cmd> [args...]` | Infer name from project, run through proxy (auto-starts) |
175+
| `portless run --name <name> <cmd>` | Override inferred base name (worktree prefix still applies) |
176+
| `portless <name> <cmd> [args...]` | Run app at `https://<name>.localhost` (auto-starts proxy) |
177+
| `portless get <name>` | Print URL for a service (for cross-service wiring) |
178+
| `portless get <name> --no-worktree` | Print URL without worktree prefix |
179+
| `portless list` | Show active routes |
180+
| `portless trust` | Add local CA to system trust store (for HTTPS) |
181+
| `portless clean` | Remove state, CA trust entry, and /etc/hosts block |
182+
| `portless proxy start` | Start HTTPS proxy as a daemon (port 443, auto-elevates) |
183+
| `portless proxy start --no-tls` | Start without HTTPS (plain HTTP on port 80) |
184+
| `portless proxy start --lan` | Start in LAN mode (mDNS `.local`, auto-follows LAN IP changes) |
185+
| `portless proxy start -p <number>` | Start the proxy on a custom port |
186+
| `portless proxy start --tld test` | Use .test instead of .localhost |
187+
| `portless proxy start --foreground` | Start the proxy in foreground (for debugging) |
188+
| `portless proxy start --wildcard` | Allow unregistered subdomains to fall back to parent route |
189+
| `portless proxy stop` | Stop the proxy |
190+
| `portless alias <name> <port>` | Register a static route (e.g. for Docker containers) |
191+
| `portless alias <name> <port> --force` | Overwrite an existing route |
192+
| `portless alias --remove <name>` | Remove a static route |
193+
| `portless hosts sync` | Add routes to /etc/hosts (fixes Safari) |
194+
| `portless hosts clean` | Remove portless entries from /etc/hosts |
195+
| `portless <name> --app-port <n> <cmd>` | Use a fixed port for the app instead of auto-assignment |
196+
| `portless <name> --force <cmd>` | Kill the existing process and take over its route |
197+
| `portless --name <name> <cmd>` | Force `<name>` as app name (bypasses subcommand dispatch) |
198+
| `portless <name> -- <cmd> [args...]` | Stop flag parsing; everything after `--` is passed to child |
199+
| `portless --help` / `-h` | Show help |
200+
| `portless run --help` | Show help for a subcommand (also: alias, hosts, clean) |
201+
| `portless --version` / `-v` | Show version |
202+
203+
**Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `trust`, `clean`, and `proxy` are subcommands and cannot be used as app names directly. Use `portless run <cmd>` to infer the name, or `portless --name <name> <cmd>` to force any name including reserved ones.
204+
205+
## Troubleshooting
206+
207+
### Proxy not running
208+
209+
The proxy auto-starts when you run an app with `portless <name> <cmd>`. If it doesn't start (e.g. port conflict), start it manually:
210+
211+
```bash
212+
portless proxy start
213+
```
214+
215+
### Port already in use
216+
217+
Another process is bound to the proxy port. Either stop it first, or use a different port:
218+
219+
```bash
220+
portless proxy start -p 8080
221+
```
222+
223+
### Framework not respecting PORT
224+
225+
Portless auto-injects the right `--port` flag and, when needed, a matching `--host` flag for frameworks that ignore the `PORT` env var: **Vite**, **VitePlus** (`vp`), **Astro**, **React Router**, **Angular**, **Expo**, and **React Native**. SvelteKit uses Vite internally and is handled automatically.
226+
227+
For other frameworks that don't read `PORT`, pass the port manually:
228+
229+
- **Webpack Dev Server**: use `--port $PORT`
230+
- **Custom servers**: read `process.env.PORT` and listen on it
231+
232+
### Permission errors
233+
234+
The default ports (80 for HTTP, 443 for HTTPS) require `sudo` on macOS and Linux. Portless auto-elevates with sudo when needed. If sudo is unavailable, it falls back to port 1355 (no sudo needed). On Windows, no elevation is required.
235+
236+
```bash
237+
portless proxy start --https # Auto-elevates with sudo for port 443
238+
portless proxy start -p 1355 --https # No sudo needed (URLs include :1355)
239+
portless proxy stop # Stop (use sudo if started with sudo)
240+
```
241+
242+
### Safari can't find .localhost URLs
243+
244+
Safari relies on the system DNS resolver for `.localhost` subdomains, which may not resolve them on all macOS configurations. Chrome, Firefox, and Edge have built-in handling.
245+
246+
Fix:
247+
248+
```bash
249+
portless hosts sync # Adds current routes to /etc/hosts
250+
portless hosts clean # Remove entries later
251+
```
252+
253+
Auto-syncs `/etc/hosts` for route hostnames by default. Set `PORTLESS_SYNC_HOSTS=0` to disable.
254+
255+
### Browser shows certificate warning with --https
256+
257+
The local CA may not be trusted yet. Run:
258+
259+
```bash
260+
portless trust
261+
```
262+
263+
This adds the portless local CA to your system trust store. After that, restart the browser.
264+
265+
### Remove portless from the machine
266+
267+
```bash
268+
portless clean
269+
```
270+
271+
Stops the proxy if needed, removes the portless CA from the trust store (when portless added it), deletes known files under state directories, and removes the portless `/etc/hosts` block. May require `sudo` on macOS/Linux.
272+
273+
### Proxy loop (508 Loop Detected)
274+
275+
If your dev server proxies requests to another portless app (e.g. Vite proxying `/api` to `api.myapp.localhost`), the proxy must rewrite the `Host` header. Without this, portless routes the request back to the original app, creating an infinite loop.
276+
277+
Fix: set `changeOrigin: true` in the proxy config (Vite, webpack-dev-server, etc.):
278+
279+
```ts
280+
// vite.config.ts
281+
proxy: {
282+
"/api": {
283+
target: "https://api.myapp.localhost",
284+
changeOrigin: true,
285+
ws: true,
286+
},
287+
}
288+
```
289+
290+
Portless automatically sets `NODE_EXTRA_CA_CERTS` in child processes so Node.js trusts the portless CA. If you run a separate Node.js process outside portless, point it at the CA manually: `NODE_EXTRA_CA_CERTS=/tmp/portless/ca.pem` (or `~/.portless/ca.pem` when the proxy runs on a non-privileged port like 1355). Alternatively, use `--no-tls` for plain HTTP.
291+
292+
### Requirements
293+
294+
- Node.js 20+
295+
- macOS, Linux, or Windows
296+
- `openssl` (for `--https` cert generation; ships with macOS and most Linux distributions; on Windows, install via `winget install -e --id ShiningLight.OpenSSL.Dev` or use the copy bundled with Git for Windows)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "portless",
3+
"version": "1.0.0",
4+
"description": "Replace port numbers with stable, named local URLs. For humans and agents.",
5+
"license": "Apache-2.0",
6+
"keywords": ["portless", "localhost", "dev-server", "proxy"],
7+
"skills": "./.agents/skills/"
8+
}

plugins/portless/skills-lock.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"version": 1,
3+
"skills": {
4+
"portless": {
5+
"source": "vercel-labs/portless",
6+
"sourceType": "github",
7+
"computedHash": "e36bc6e7b2e2278d3ce84e2cfe43215ecd66589d044d5a6fe1d4546cd3c729da"
8+
}
9+
}
10+
}

release-please-config.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,17 @@
482482
"jsonpath": "$.version"
483483
}
484484
]
485+
},
486+
"plugins/portless": {
487+
"release-type": "simple",
488+
"component": "portless",
489+
"extra-files": [
490+
{
491+
"type": "json",
492+
"path": ".claude-plugin/plugin.json",
493+
"jsonpath": "$.version"
494+
}
495+
]
485496
}
486497
},
487498
"release-type": "node",

0 commit comments

Comments
 (0)