Skip to content

open: a running web server in the browser, asked first whether it speaks HTTPS or HTTP - #32

Merged
pyjeebz merged 3 commits into
mainfrom
feat/open
Nov 9, 2026
Merged

pyjeebz merged 3 commits into
mainfrom
feat/open

Conversation

@pyjeebz

@pyjeebz pyjeebz commented Nov 8, 2026

Copy link
Copy Markdown
Owner

Closes #24

Why

ports already knows which dev server is yours. Opening it shouldn't mean copying a port number into a browser. ports open 3000 (or ports open shop) does it, and it checks first that the port really is a web server.

What

  • cmd/open.go
    • Matches a port, or a name the way ports stop does.
    • Probes each distinct port concurrently, then opens the page.
    • Exit codes:
      • nothing listening: exit 1;
      • no HTTP answer: exit 1, pointing at ports doctor;
      • a name with several web servers: the picker in a terminal, otherwise exit 2 listing the ports;
      • picker left: exit 130;
      • no browser could start: exit 1, with the URL printed.
    • Launcher: $BROWSER first (its first entry, %s for the URL, no shell). Otherwise:
      • open on macOS;
      • xdg-open on Linux;
      • wslview or explorer.exe in WSL (explorer.exe's exit status is ignored, since it exits 1 even when it worked);
      • rundll32 url.dll,FileProtocolHandler on Windows (not start, which reads & as a command separator).
    • An opener still running after 2s is left to run.
  • internal/inspect/web.go: WebURL
    • Sends one HEAD over HTTPS, then over HTTP. HTTPS goes first because HTTPS servers often answer plain HTTP with a 400.
    • Asks the bound address, or both loopbacks.
    • Never uses a proxy and never follows redirects. Any certificate is accepted, since only a HEAD is sent.
    • HTTP gets 4s, so a cold next dev can compile its first page.
  • Picker: generalised with a pickSpec (verb, confirm, blocked, none). ports stop behaves exactly as before, including its messages.
  • Docs:
    • a ports open section and exit-code rows in commands.mdx;
    • a README feature line;
    • the README principle note the issue asked for: it launches a browser, never a server, so the "observer, never a runner" line holds.

Evidence

  • Live:
    • a python http.server opened at http://localhost:<port>/ through a $BROWSER recorder;
    • a raw TCP listener exits 1 ("doesn't answer HTTP");
    • a free port exits 1;
    • a name with two HTTP servers exits 2 off a TTY;
    • a bad $BROWSER exits 1 and keeps the URL.
  • Unit tests:
    • HTTP vs HTTPS detection, including against a TLS server;
    • a non-HTTP listener;
    • a server that answers slowly;
    • a 127.0.0.2 bind;
    • $BROWSER parsing;
    • a launch failure;
    • the stop picker's "nothing can be stopped" wording.
  • Integration tests:
    • TestOpen: open by port, a non-HTTP port, a free port;
    • TestOpenSeveral: off a TTY it exits 2 listing both ports; on a real PTY, choosing the second server opens it.
  • Gate:
    • unit and integration tests pass;
    • golangci-lint reports 0 issues;
    • the Windows and darwin builds succeed;
    • web builds.
  • Risky tier: it starts a process (the browser) and touches the stop picker. A fresh-context review blocked the first cut because the stop picker's message had changed; that's fixed in the second commit, along with the cold-server timeout, explorer.exe's exit status and an empty process name.
  • Accepted nits:
    • $BROWSER is split on spaces;
    • a plain server may log the HTTPS probe as a bad request (documented);
    • a port bound to two addresses takes the first scanned row's URL.

Rollback

Revert the squash commit. ports open is a new command, and the picker refactor goes with it.


Generated by Claude Code

@pyjeebz
pyjeebz merged commit 4149937 into main Nov 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

open: open a running server in the browser

2 participants