diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a146dd4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,15 @@ +name: ci +on: + pull_request: + push: + branches: [main] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - run: go vet ./... + - run: go test -race ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8c91361 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,24 @@ +name: release +on: + push: + tags: ["v*"] +permissions: + contents: write +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - run: echo "GOFLAGS=-tags=$(cat build-tags.txt)" >> "$GITHUB_ENV" + - uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 634feba..d277665 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /iris +/dist *.db *.db-* diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..a636e29 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,21 @@ +# Release build. Build tags come from build-tags.txt via GOFLAGS, which the +# release workflow exports; for a local snapshot: +# GOFLAGS=-tags=$(cat build-tags.txt) goreleaser release --snapshot --clean +version: 2 +project_name: iris +builds: + - binary: iris + env: + - CGO_ENABLED=0 + goos: [linux, darwin] + goarch: [amd64, arm64] + flags: [-trimpath] + ldflags: ["-s -w -X main.version={{ .Version }}"] +archives: + - formats: [tar.gz] + name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" + files: [LICENSE] +checksum: + name_template: checksums.txt +changelog: + use: github diff --git a/PLAN.md b/PLAN.md index 0d2e9cc..c5805d7 100644 --- a/PLAN.md +++ b/PLAN.md @@ -131,7 +131,7 @@ MUST NOT: - **Phase 1 — relay core (same-machine, scene 2).** Go HTTP server; SQLite schema (`sessions`, `messages`, `files`); the eight endpoints; in-process long-poll wakeup (per-session broadcast channel); caps + 429s; lifecycle sweeper (ticker) + system events; curl-based conformance script that doubles as the demo. Fully testable with zero networking. **Done 2026-09-01**: `relay/` package (pure-Go SQLite, stdlib router, no framework), `main.go` CLI, `scripts/conformance.sh`. `iris serve` listens on `127.0.0.1:7433`, data in `~/.iris` (`-addr`, `-data` flags). `iris connect` is a stub until Phase 3. - **Phase 2 — iris skill.** Authored by Parth; a harness-neutral doc, not a Claude Code-specific skill. - **Phase 3 — tailcat (cross-machine, scenes 1 & 3).** Embed tailcat, pinned; `iris serve` opens the tunnel and prints the bundled pairing token; `iris connect` parses it, dials, binds localhost, prints local URL + key. Flags for self-hosted DERP passthrough. **Done 2026-09-01**: `tunnel/` package on tailcat v0.4.0. Verified against the source: one token admits any number of peers (each handshake adds a WireGuard peer), and every inbound TCP stream gets its own handler, so HTTP fan-in just works. The relay is served inside the tunnel on port 7433 with provisioning removed; `iris connect` pings the host first (30s), then forwards a localhost listener (`-addr`, default OS-assigned port). `-derp host,...` on serve pins self-hosted relays; `-v` on either side prints tunnel internals. Scene 3 verified live: peer joined in under a second, 3MB file round-tripped intact. -- **Phase 4 — readme + packaging.** goreleaser; `go install`; curl install script; brew (`iris-tl`) if warranted. Release. +- **Phase 4 — readme + packaging.** goreleaser; `go install`; curl install script; brew (`iris-tl`) if warranted. Release. **Packaging done 2026-09-01**: `.goreleaser.yaml` (darwin/linux × amd64/arm64, tailcat's `build-tags.txt`, stripped, version injected), `ci.yml` (vet + race tests on PRs) and `release.yml` (goreleaser on `v*` tags), `install.sh` at the repo root (checksum-verified, `IRIS_VERSION`, `IRIS_INSTALL_DIR`), `iris -version`. First tag is `v1.0.0`. Brew deferred until a tap is worth a second repo. README still open. - **Phase 5 — website.** **Done bar:** all three scenes working — two people's agents over the tunnel, two agents on one machine, one person across two machines. Phases are build order, not release gates; nothing ships or gets dogfood-blessed on Phase 1 alone. That's the product. **Cross-network verified 2026-09-01** between a laptop behind home NAT and a Vercel sandbox in iad1, both directions: peer joined in ~1s, messages and 2MB files round-tripped, provisioning blocked over the tunnel, and magicsock upgraded from DERP to a direct UDP path within 5s. Scene 1 differs from scene 3 only in who holds the token, so the transport is proven; what remains is agents driving it (the skill). diff --git a/build-tags.txt b/build-tags.txt new file mode 100644 index 0000000..37af7fb --- /dev/null +++ b/build-tags.txt @@ -0,0 +1 @@ +netgo,omitidna,omitpemdecrypt,osusergo,ts_omit_ace,ts_omit_acme,ts_omit_advertiseexitnode,ts_omit_advertiseroutes,ts_omit_appconnectors,ts_omit_aws,ts_omit_bird,ts_omit_cachenetmap,ts_omit_captiveportal,ts_omit_capture,ts_omit_cliconndiag,ts_omit_clientmetrics,ts_omit_clientupdate,ts_omit_cloud,ts_omit_colorable,ts_omit_completion,ts_omit_completion_scripts,ts_omit_conn25,ts_omit_debug,ts_omit_debugeventbus,ts_omit_debugportmapper,ts_omit_desktop_sessions,ts_omit_dns,ts_omit_doctor,ts_omit_drive,ts_omit_favorites,ts_omit_flashappliance,ts_omit_health,ts_omit_hujsonconf,ts_omit_identityfederation,ts_omit_ipnbus,ts_omit_iptables,ts_omit_kube,ts_omit_linkspeed,ts_omit_linuxdnsfight,ts_omit_listenrawdisco,ts_omit_logtail,ts_omit_netlog,ts_omit_networkmanager,ts_omit_oauthkey,ts_omit_osrouter,ts_omit_outboundproxy,ts_omit_peerapiclient,ts_omit_peerapiserver,ts_omit_portlist,ts_omit_portmapper,ts_omit_posture,ts_omit_qrcodes,ts_omit_relayserver,ts_omit_remoteconfig,ts_omit_resolved,ts_omit_routecheck,ts_omit_runtimemetrics,ts_omit_sdnotify,ts_omit_serve,ts_omit_serviceclientprefs,ts_omit_synology,ts_omit_syslog,ts_omit_syspolicy,ts_omit_systray,ts_omit_taildrop,ts_omit_tailnetlock,ts_omit_tap,ts_omit_tpm,ts_omit_tundevstats,ts_omit_unixsocketidentity,ts_omit_useexitnode,ts_omit_useproxy,ts_omit_usermetrics,ts_omit_useroutes,ts_omit_wakeonlan,ts_omit_webbrowser,ts_omit_webclient diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..b92649c --- /dev/null +++ b/install.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# Installs iris from its GitHub release. +# +# curl -fsSL https://iris-tl.dev/install.sh | sh +# +# IRIS_VERSION=v1.0.0 pins a release (default: latest). +# IRIS_INSTALL_DIR=/path overrides the target directory. +set -eu + +repo=psrth/iris +os=$(uname -s | tr '[:upper:]' '[:lower:]') +arch=$(uname -m) +case "$os" in + linux | darwin) ;; + *) echo "iris: unsupported OS: $os" >&2; exit 1 ;; +esac +case "$arch" in + x86_64 | amd64) arch=amd64 ;; + arm64 | aarch64) arch=arm64 ;; + *) echo "iris: unsupported architecture: $arch" >&2; exit 1 ;; +esac + +version=${IRIS_VERSION:-latest} +if [ "$version" = latest ]; then + base="https://github.com/$repo/releases/latest/download" +else + base="https://github.com/$repo/releases/download/$version" +fi + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +file="iris_${os}_${arch}.tar.gz" +for f in "$file" checksums.txt; do + curl -fsL -o "$tmp/$f" "$base/$f" || { echo "iris: no release $version for $os/$arch at $base/$f" >&2; exit 1; } +done + +if command -v sha256sum >/dev/null 2>&1; then + sum=$(sha256sum "$tmp/$file" | cut -d' ' -f1) +else + sum=$(shasum -a 256 "$tmp/$file" | cut -d' ' -f1) +fi +expected=$(grep " $file\$" "$tmp/checksums.txt" | cut -d' ' -f1) +[ -n "$expected" ] && [ "$sum" = "$expected" ] || { echo "iris: checksum mismatch for $file" >&2; exit 1; } +tar -xzf "$tmp/$file" -C "$tmp" iris + +dir=${IRIS_INSTALL_DIR:-} +if [ -z "$dir" ]; then + if [ -w /usr/local/bin ]; then dir=/usr/local/bin; else dir="$HOME/.local/bin"; fi +fi +mkdir -p "$dir" +install -m 755 "$tmp/iris" "$dir/iris" +echo "installed $("$dir/iris" -version) to $dir/iris" +case ":$PATH:" in + *":$dir:"*) ;; + *) echo "add $dir to your PATH" ;; +esac diff --git a/main.go b/main.go index 45e1b46..54329dd 100644 --- a/main.go +++ b/main.go @@ -20,11 +20,15 @@ import ( "tailscale.com/types/logger" ) +// version is set by the release build. +var version = "dev" + const usage = `usage: iris serve [-addr host:port] [-data dir] [-derp host,...] [-v] start a relay, open a session, print its pairing token iris connect [-addr host:port] [-v] join a session; it appears on localhost + iris -version ` func main() { @@ -37,6 +41,8 @@ func main() { serve(os.Args[2:]) case "connect": connect(os.Args[2:]) + case "-version", "--version": + fmt.Println("iris", version) default: fmt.Fprint(os.Stderr, usage) os.Exit(2)