Skip to content

Merge dev-tunnel CLI into proxy repo + clean up legacy mkcert setup#3

Open
pinetops wants to merge 3 commits into
mainfrom
merge-dev-tunnel
Open

Merge dev-tunnel CLI into proxy repo + clean up legacy mkcert setup#3
pinetops wants to merge 3 commits into
mainfrom
merge-dev-tunnel

Conversation

@pinetops
Copy link
Copy Markdown
Member

@pinetops pinetops commented May 3, 2026

Summary

  • Combines the standalone dev-tunnel script with the Traefik proxy installer into a single CLI: dev-tunnel proxy ... covers the old traefik-dev-proxy commands, and dev-tunnel setup/start/stop/status manages a per-developer Cloudflare Tunnel.
  • dev-tunnel setup creates DNS, SSL, and ingress for both <project>.<user>.u2i.me and *.<project>.<user>.u2i.me on the same tunnel — closes dev-tunnel: support bare hostnames (e.g. retrotool.tom.u2i.me) in addition to wildcards #2.
  • install.sh now installs source files directly (no embedded heredoc) and falls back to downloading from raw GitHub when piped via curl ... | bash.
  • Removes setup.sh and reverse-proxy/ — leftover mkcert/HTTPS scaffolding that was orphaned by ff8250b ("Simplify to HTTP-only setup").

Test plan

  • bash -n bin/dev-tunnel install.sh (syntax)
  • dev-tunnel help shows new combined help
  • Local checkout install: ./install.sh installs binary + compose file, starts proxy
  • Curl-pipe install: curl -sSL .../install.sh | bash (after merge to main) downloads from raw URL and installs
  • dev-tunnel setup <user> <project> creates both bare + wildcard DNS, combined SSL cert, both ingress rules
  • Existing traefik-dev-proxy binary, if present, gets removed by installer

🤖 Generated with Claude Code

pinetops and others added 2 commits May 2, 2026 18:21
Combines the standalone dev-tunnel script with the Traefik proxy installer
into a single CLI. `dev-tunnel proxy ...` covers the old traefik-dev-proxy
commands; `dev-tunnel setup/start/stop/status` manages a per-developer
Cloudflare Tunnel.

`setup` creates DNS, SSL, and ingress for both the bare and wildcard
hostnames (`<project>.<user>.u2i.me` + `*.<project>.<user>.u2i.me`),
closing #2.

install.sh now installs source files directly (no embedded heredoc) and
falls back to downloading from raw GitHub when piped via curl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
setup.sh and reverse-proxy/ are leftovers from the original mkcert-based
HTTPS flow that was replaced by ff8250b ("Simplify to HTTP-only setup").
Nothing in the current install path references them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pinetops
Copy link
Copy Markdown
Member Author

pinetops commented May 3, 2026

Validation against #2

Tried this against retrotool to validate the bare-hostname support promised in #2. The end result works — https://retrotool.tom.u2i.me/ reaches my local dev — but first-time setup didn't complete cleanly because of two bugs in cmd_setup. Both are around the 1Password token save path, and both prevented dev-tunnel start from working until I worked around them by hand.

Bug 1: new-tunnel branch can leave tunnel_token empty

bin/dev-tunnel:98 reads the token from the create response:

tunnel_token=$(echo "$result" | jq -r '.result.token // empty')

Cloudflare's POST /accounts/{id}/cfd_tunnel doesn't always include the token in the response — for me it came back without one, so tunnel_token="" and the save block at line 152 (if [ -n "${tunnel_token:-}" ]; then ...) silently skipped saving.

The existing-tunnel branch (line 91) already does the right thing — fetches via /cfd_tunnel/${id}/token. The fix is to mirror that as a fallback after create:

tunnel_token=$(echo "$result" | jq -r '.result.token // empty')
# Fall back to the dedicated token endpoint when the create response omits it
if [ -z "$tunnel_token" ]; then
  tunnel_token=$(cf_api GET "/accounts/${CF_ACCOUNT_ID}/cfd_tunnel/${tunnel_id}/token" | jq -r '.result // empty')
fi

Bug 2: set -e trips during 1Password lookup when no existing item

Even after fixing Bug 1, the script still exited silently right after the ingress PUT, before printing the "Setup complete" banner. With bash -x, the trace ends at:

op_id=$(op item list --vault Employee 2>/dev/null | grep -F "$op_title" | awk '{print $1}')

When no item with that title exists yet (the normal case for a brand-new tunnel), grep -F exits 1, which propagates out of the substitution. Combined with the script's set -eo pipefail, that aborts before op item create ever runs — so the token never makes it into 1Password, and the next dev-tunnel start retrotool-tom fails with "Token not found in 1Password. Run 'dev-tunnel setup ' first."

Two equivalent fixes:

op_id=$(op item list --vault Employee 2>/dev/null | grep -F "$op_title" | awk '{print $1}' || true)

or — more explicit:

op_id=$(op item list --vault Employee 2>/dev/null | awk -v t="$op_title" '$0 ~ t {print $1}')

Minor: ingress order

Line 149 puts *.${project}.${user}.u2i.me before ${project}.${user}.u2i.me. Cloudflare evaluates ingress rules top-to-bottom and the wildcard wouldn't match the bare hostname, so this is functionally fine — but bare-first reads more naturally and is one less thing to second-guess if the wildcard pattern ever broadens. Cosmetic.

Repro

Fresh setup against a project that doesn't have an existing tunnel:

cd ~/dev/some-project
dev-tunnel setup tom some-project

Without these fixes: setup prints "Configuring ingress -> http://localhost:8080..." and stops; no 1Password item created; start fails on missing token.

With both fixes: setup runs through, prints "Token saved to 1Password.", "Setup complete", and start works on first try.

Validation result for #2 itself

After working around the bugs, the end-to-end picture works exactly as described in #2:

  • DNS for retrotool.tom.u2i.me and *.retrotool.tom.u2i.me resolve to Cloudflare.
  • Single SSL cert covers both.
  • Cloudflare tunnel → local traefik → container.
  • https://retrotool.tom.u2i.me/ returns 200 with the local dev app's content.

So the feature itself is correct — just gated on the two save-path bugs above for first-time users.

The teamology compose.dev.local.yml uses the rewriteheaders plugin to
rewrite the Host header from `<tenant>.teamology.localhost` to
`<tenant>.<APP_HOSTNAME>` so Phoenix's Endpoint.url() generates correct
redirects regardless of access method. Without the plugin loaded,
traefik rejects the entire teamology router at config-load time
("plugin: unknown plugin type: rewriteheaders"), and any request to
`*.teamology.tom.u2i.me` hits traefik's default 404 because no router
matches.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
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.

dev-tunnel: support bare hostnames (e.g. retrotool.tom.u2i.me) in addition to wildcards

1 participant