A small, standalone TUI wizard that helps a user:
- Connect to Tailscale (auto-login with QR if needed)
- Launch an OpenCode server bound to localhost
- Publish it to the tailnet via
tailscale serve - Show a compact URL + QR for phone access
- Runtime: Bun
- Effects: Effect (4.0.0-beta.11)
- UI: @opentui/core + @opentui/solid
- State: @effect/atom-solid (Solid integration, from npm — not a local vendor)
src/
main.tsx # Entry point — renders App, defines atoms (phase, step, log, url, error)
app.tsx # Root UI component — all screens, keyboard handling
flow.ts # Effect flow: tailscale → opencode → publish; signalExit()
runtime.ts # appRuntime (Effect runtime with service layers)
qr.ts # renderQR, copyToClipboard, openInBrowser, trim
services/
tailscale.ts # Tailscale service: ensure(), publish()
opencode.ts # OpenCode service: start()
config.ts # AppConfig (port, password)
errors.ts # Tagged error types
The app uses a phase atom: "welcome" | "running" | "error" | "done"
And a step atom: "tailscale" | "opencode" | "publish"
- Pixel wordmark logo (TAIL / CODE)
- Bullet list of what will happen
enterto start,qto quit
- Step list with ▸ active / ✓ done / · pending indicators
- Scrolling log panel (last 6 lines)
- Same step list (✕ on failed step)
- Error message
enterto retry,qto quit
- Remote URL + QR code
- Local attach command:
opencode attach http://127.0.0.1:4096 - Keys:
1copy URL,2open in browser,3copy attach command
tailscale.ensure(append)— verifies/connects tailscale, returns binary pathopencode.start(port, password, append)— starts OpenCode server in scopetailscale.publish(bin, port, append)— runstailscale serve --bg --yes, returns remote URL- Sets
phase = "done", then awaitsexitSignalto keep scope alive (finalizers run on exit)
flowFnis anappRuntime.fnatom — re-triggering it interrupts any prior run- QR rendered as Unicode blocks (no ANSI escapes) to avoid OpenTUI corruption
- OpenCode server always bound to
127.0.0.1to avoid LAN exposure - Tailscale serve reset before re-publishing to avoid "listener already exists" errors
signalExit()resolves the exit Deferred, which unblocks the flow and lets scope finalize
bun run dev # with HMR
bun run start # production
bun run showcase # component showcaseReleases are fully automated via .github/workflows/release-binaries.yml. Never manually upload binaries or publish to npm.
- Update
"version"inpackage.json - Commit:
git commit -m "chore: bump version to X.Y.Z" - Push to main:
git push origin main - Tag and push:
git tag vX.Y.Z && git push origin vX.Y.Z
The tag push triggers the workflow. That's it.
| Job | Runner | Output |
|---|---|---|
build-macos |
macos-latest (arm64) |
tailcode-darwin-arm64 + tailcode-darwin-x64 (cross-compiled) |
build-linux |
ubuntu-latest |
tailcode-linux-x64 |
github-release |
ubuntu | GitHub Release with all binaries + SHA256SUMS |
publish-npm |
ubuntu | @kitlangton/tailcode on npm |
update-homebrew |
ubuntu | Updates kitlangton/homebrew-tap formula |
The npm package ships dist/tailcode.js (pre-bundled with the Solid JSX plugin) rather than raw
.tsx source. bunfig.toml's preload only applies in the local project directory — globally
installed packages don't inherit it — so shipping pre-built JS is required for correct JSX rendering.