Skip to content

feat: kiqr share — expose your local site via a Cloudflare quick tunnel - #28

Merged
kjellberg merged 1 commit into
kiqr:mainfrom
kjellbergzoey:feat/kiqr-share
Jun 8, 2026
Merged

feat: kiqr share — expose your local site via a Cloudflare quick tunnel#28
kjellberg merged 1 commit into
kiqr:mainfrom
kjellbergzoey:feat/kiqr-share

Conversation

@kjellbergzoey

@kjellbergzoey kjellbergzoey commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

What this adds

kiqr share exposes a developer's running local WordPress site at a public URL through a Cloudflare quick tunnel — an instant, no-account, no-server tunnel. This is the MVP of the collaboration story: run kiqr up, then kiqr share, and hand the printed https://<sub>.trycloudflare.com URL to a collaborator.

The command:

  1. Reads project config (graceful error on an invalid kiqr.yaml); tells you to run kiqr init/kiqr up if not initialized.
  2. Verifies the project's wordpress container is running (reuses isContainerRunning + containerNameFor); otherwise asks you to run kiqr up.
  3. Checks for the cloudflared binary; if missing, prints a per-OS install hint and exits.
  4. Spawns cloudflared, parses its output for the public URL, prints it boldly, and stays attached. Ctrl+C (unmount) kills the child tunnel process.

The architecture (the crux)

The kiqr agent's Traefik proxy listens on :5477 and routes purely by Host header — the project router matches Host(<project hostname>). A naive cloudflared --url http://localhost:5477 forwards Host: <random>.trycloudflare.com, which matches no project router and falls through to the splash/404 page. So:

  1. Force the host header Traefik expects. We run:
    cloudflared tunnel --no-autoupdate --url http://localhost:5477 --http-host-header <project hostname>
    Now Traefik routes the tunneled request to the right project.

  2. But WordPress must generate links with the public host, not the internal Traefik hostname, or every asset/link breaks for the remote visitor. cloudflared still forwards the original public host as X-Forwarded-Host (and X-Forwarded-Proto: https). So WORDPRESS_CONFIG_EXTRA in BitnamiRuntimeProvider now derives WP_HOME/WP_SITEURL from X-Forwarded-Host when present, falling back to HTTP_HOST, and treats the request as https when X-Forwarded-Proto is https:

    $host = !empty($_SERVER['HTTP_X_FORWARDED_HOST']) ? $_SERVER['HTTP_X_FORWARDED_HOST'] : $_SERVER['HTTP_HOST'];
    $proto = (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') ? 'https' : 'http';
    define('WP_HOME', $proto.'://'.$host);
    define('WP_SITEURL', $proto.'://'.$host);

    Local behavior is unchanged: with no X-Forwarded-Host, it uses HTTP_HOST over http, exactly as before. The $$$ compose escaping is preserved (the string is embedded in the docker-compose YAML).

Requires a restart to take effect

The WORDPRESS_CONFIG_EXTRA change only lands in a container's environment when the compose file is regenerated and the container recreated. On an existing project, run kiqr restart (or a fresh kiqr up) before kiqr share so the X-Forwarded-Host-aware config is applied.

External dependency

cloudflared is an external tool the user installs themselves (Homebrew on macOS, winget on Windows, or the Cloudflare download page on Linux). kiqr share detects its absence and prints the right hint per platform.

Tests / gates

All four gates green locally:

  • typecheck (tsc --noEmit) ✅
  • test (vitest run) ✅ — 207 passed
  • build (tsup) ✅
  • lint (biome check .) ✅

New tests cover parseTunnelUrl (clean line, box-drawing/ANSI noise, no-url, http vs https), buildCloudflaredArgs, isCloudflaredInstalled (mocked exec found/not-found), and the per-platform install hint. The BitnamiRuntimeProvider test now asserts the new X-Forwarded-Host-aware string keeps the HTTP_HOST fallback, the https proto check, both URL constants, and the $$ escaping.

NOT verified here

This was implemented and validated without Docker, cloudflared, or a display — only pure logic via tests + typecheck/build/lint. The live end-to-end tunnel was not run. Specifically un-verified:

  • That a real cloudflared quick tunnel routes through Traefik with the forced host header and reaches the project (not the splash page).
  • That WordPress, recreated after the env change, actually emits public https://*.trycloudflare.com URLs for assets/links/redirects via X-Forwarded-Host/X-Forwarded-Proto.
  • The interactive Ink rendering / Ctrl+C teardown of the spawned child against a real cloudflared process.

@kjellberg — please smoke-test on a real machine: kiqr restart an existing project (or fresh kiqr up), then kiqr share, and confirm the public URL loads the site with working assets.

Docs

Not touching README.md to avoid conflicting with the open README rewrite (#26); docs for share will follow in a separate PR.

🤖 Generated with Claude Code

…nels

`kiqr share` opens an instant, account-less Cloudflare quick tunnel
(via the external `cloudflared` binary) so a developer can hand a public
URL to a collaborator while their local WordPress site keeps running.

The kiqr agent's Traefik proxy on :5477 routes purely by Host header, so
the tunnel is pointed at Traefik with `--http-host-header <project host>`
to force a matching route. cloudflared still forwards the real public
host as `X-Forwarded-Host` (and the scheme as `X-Forwarded-Proto`), so
WORDPRESS_CONFIG_EXTRA now derives WP_HOME/WP_SITEURL from those headers
when present, falling back to HTTP_HOST over http for normal local
requests -- keeping local behavior identical. The PHP `$$` compose
escaping is preserved.

- src/lib/share.ts: pure, injectable helpers (isCloudflaredInstalled,
  buildCloudflaredArgs, parseTunnelUrl, cloudflaredInstallHint,
  TRAEFIK_BASE_URL) with unit tests.
- src/commands/share.tsx: thin Ink command that validates config and a
  running wordpress container, checks for cloudflared, spawns the tunnel,
  prints the public URL, and kills the child on Ctrl+C/unmount.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kjellberg
kjellberg merged commit 08a1569 into kiqr:main Jun 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants