A local dev orchestrator, written in Rust. Starling is a fork/port of Tilt with portless-style named URLs built in, redesigned for scaled, agent-first engineering — many humans and AI agents running many environments in parallel. It's organized around a central daemon with a k9s-style TUI dashboard.
- A single background daemon owns one shared named-URL proxy, allocates ports centrally (so multiple projects never collide), and aggregates every running instance's resources.
starling upruns the engine for one project (executes real Starlingfiles, watches files, runslocal_resourcecommands, builds docker images, applies Kubernetes manifests) and reports to the daemon.starling(orstarling dash) opens a k9s-style terminal dashboard of every instance's resources, with live logs and trigger.
Serving resources get stable, named <resource>.<project>.<tld> URLs through
the shared proxy instead of raw localhost:PORT. It also remains
protocol-compatible with Tilt's React frontend (starling up --web serves
the original web UI for a single instance).
Architecture
┌──────────────────────────┐
starling (TUI) ───────►│ starling daemon │
│ • shared proxy :1360 │◄─── browser
starling up (proj A) ─►│ • central port allocation │ <name>.A.localhost
starling up (proj B) ─►│ • aggregated dashboard │ <name>.B.localhost
└──────────────────────────┘
Control plane: newline-JSON over a Unix socket at ~/.starling/daemon.sock
(one request/response per connection). The daemon is auto-started by up/dash
if not already running.
Lineage: the web UI and the wire protocol come from Tilt (Apache-2.0), and the named-URL proxy is ported from portless. Because the frontend is Tilt's, a few on-the-wire identifiers keep their original names (
tiltStartTime,tiltfileKey, the/api/set_tiltfile_argsroute) — those are the frontend contract, not Starling branding.
Why Starling — built for agent-first engineering
Starling is a fork/port of Tilt, but with a different design goal: work well when much of the engineering is done by AI agents, at scale, in parallel. Tilt was built around one developer at one web UI. Starling assumes a fleet of humans and agents spinning up many environments at once — across projects, git worktrees, and concurrent tasks — and optimizes for that:
- Names, not ports. Every service is addressable as
<resource>.<project>.localhostinstead of an ephemerallocalhost:PORT. Agents reference services by stable name, so prompts, configs, and generated scripts don't break when ports shuffle. (This is portless's "for humans and agents" idea, built in.) - A central daemon that coordinates many instances. Dozens of
starling upprocesses — one per project / worktree / agent task — share a single proxy and a single port-allocation authority, so parallel agents never collide on ports or step on each other's URLs. - A machine-readable control plane. The daemon speaks newline-delimited JSON
over a Unix socket (
~/.starling/daemon.sock): an agent can register environments, query the aggregated state of every running instance, stream logs, and trigger builds programmatically — the same API the dashboard uses. No scraping a web UI to find out what's running. - One pane of glass for the whole fleet. The k9s-style TUI shows every instance's resources together, so a human supervising a swarm of agents has a single place to watch builds, statuses, and logs.
Honest scope: Starling keeps Tilt's wire protocol and an optional web UI for compatibility, but the default experience is daemon + TUI + named URLs. It is not yet a drop-in replacement for all of Tilt — see the roadmap below.
What's here
web/— Tilt's React frontend, vendored unchanged. Built with Yarn (Berry) + Create React App intoweb/build.src/— the Rust server + engine:api/v1alpha1.rs— Kubernetes-style resource types (UISession,UIResource,UIButton,Cluster) matchingweb/src/core.d.ts.api/webview.rs— theViewenvelope and log model (web/src/webview.d.ts).store.rs— in-memory object store + change-notification channel; serves a fullViewon connect and incremental deltas (the log-checkpoint protocol).server.rs— axum routes + the/ws/viewwebsocket.starlingfile/— Starlark Starlingfile execution (starlarkcrate) producingManifests. Builtins match Tilt's API:local_resource(full kwargs +trigger_mode),local,read_file,watch_file,docker_build,custom_build,k8s_yaml,k8s_resource,filter_yaml,kustomize,helm,docker_compose,port_forward, live_update steps (sync/run/fall_back_on/restart_container/initial_sync),include,load(),load_dynamic,default_registry,allow_k8s_contexts,k8s_kind, plus thealiasextension.TRIGGER_MODE_AUTO/_MANUALconstants.k8s.rs— multi-doc YAML parsing → workloads, container images, selectors.proxy.rs— embedded named-URL reverse proxy (ported from portless): a Host-header router mapping<name>.<tld>→127.0.0.1:<port>, withX-Forwarded-*injection, loop detection, WebSocket/streaming, route registry.engine.rs— the build/run loop: runs update/serve commands, watches deps (notify), builds images (docker build), deploys (kubectl apply), watches pod status + streams pod logs (kubectl logs -f), and assigns eachserve_cmdaPORT+ named proxy route.daemon/— the central daemon:protocol.rs(UDS request/response + snapshot types),client.rs(client + auto-start),mod.rs(state, port leasing, shared proxy, command queue, instance pruning).tui/— the k9s-style dashboard (ratatui+crossterm): resource table across all instances, live log pane, navigation, trigger.seed.rs— session + cluster environment info.main.rs— CLI:up,daemon,dash(+ the per-instance reporter loop).
API surface (matches Tilt's internal/hud/server/server.go)
| Method | Path | Purpose |
|---|---|---|
| GET | /api/websocket_token |
CSRF token required by the websocket |
| GET | /ws/view |
streams View JSON (full, then deltas); needs ?csrf=<token> |
| GET | /api/view |
full View as JSON |
| GET | /api/snapshot/:id |
a Snapshot wrapping the view |
| POST | /api/trigger |
queue a build ({manifest_names, build_reason}) |
| POST | /api/override/trigger_mode |
set trigger mode on manifests |
| POST | /api/set_tiltfile_args |
replace Starlingfile args (route name fixed by the frontend) |
| POST | /api/analytics / /api/analytics_opt |
accepted, no-op |
| * | (fallback) | static frontend assets with SPA index fallback |
Running
The crate is published as starling-devex; the installed CLI is starling:
# In each project directory (auto-starts the daemon on first run):
# Open the shared dashboard (k9s-style TUI) from anywhere:
# Run the daemon explicitly (optional; up/dash auto-start it):
Drop-in for existing Tilt projects: starling up loads ./Starlingfile if
present, otherwise falls back to ./Tiltfile — so you can run it in an existing
Tilt repo with no renaming. (--file <path> overrides the auto-detection.)
Starling implements Tilt's Tiltfile builtins, so most existing Tiltfiles run
unchanged.
In the TUI: j/k (or ↑/↓) move, ↵ opens a detail view, o opens the
selected resource's URL in the browser, l opens full-screen logs, t triggers,
R restarts, p changes the preferred backend port, / filters resources,
r refreshes, q quits. The table shows
every instance's resources (instance · resource · type · update · runtime · pod ·
backend port · URL) with a live log pane for the selection. In full-screen logs,
/ filters log lines by regex (case-insensitive, with substring fallback) and
PgUp/PgDn scroll.
The bundled ./Starlingfile demonstrates local_resource (one-shot cmd,
dependency ordering, a serve_cmd that gets a named URL, and deps file-watch
rebuilds). Run starling up in two different project directories to see
central port allocation (distinct ports, no conflicts) and per-project named
URLs in one dashboard.
Legacy web UI
starling up --web --port 10360 additionally serves Tilt's original React UI
for that one instance (the websocket View protocol is still implemented).
Kubernetes (local cluster)
Starling deploys to whatever cluster your current kube-context points at, via
kubectl apply + pod-status watch + kubectl logs. For local development the
expectation — same as Tilt — is a local cluster (kind / k3d / minikube /
Docker Desktop k8s), not a remote/production cluster.
# one-time: a local cluster (kind shown; k3d/minikube/Docker Desktop also work)
# point Starling at it (kind set your context to kind-starling); then:
To target a cluster without changing your default context, run Starling with an
explicit KUBECONFIG (the engine shells kubectl, which respects it):
KUBECONFIG=/.kube/kind.yaml
--dry-run
kubectl apply is invoked with --dry-run=client --validate=false, so nothing
on the cluster is mutated. Useful for validating the deploy pipeline against any
context — including when you don't have a local cluster up yet. (Pod watching is
skipped in dry-run since nothing is deployed.)
Named URLs (integrated portless)
portless's functionality is built in: instead of juggling random localhost:PORT
numbers, every serving resource gets a stable, named URL through an embedded
reverse proxy.
- Each
local_resourcewith aserve_cmdis assigned a free port (passed as$PORT/$HOSTto the child) and registered as<name>.<tld>. Its UI link becomes e.g.http://webserver.localhost:1360. - The Starling UI itself is mounted at
starling.<tld>. alias(name, port)(Starlingfile builtin) registers a static route to any already-running server — a Docker container, a k8s port-forward, etc.local_resource(..., serve_port=N)prefers a fixed port; if that port is busy or already claimed by another route, Starling falls back to a free$PORTand logs a warning.
.localhost hostnames resolve to 127.0.0.1 automatically in browsers, so the
URLs just work. Flags: --proxy-port (default 1360), --tld (default
localhost), --no-proxy to disable. The proxy injects X-Forwarded-*
headers, detects forwarding loops, and proxies WebSockets/streaming.
HTTPS: pass --tls (to up/daemon) and the daemon mints a per-hostname
cert on the fly from a local CA; run starling trust once to install the CA and
avoid browser warnings. Plain HTTP on the TLS port 308-redirects to HTTPS.
# webserver serve_cmd reachable at its named URL through the proxy
# with --tls:
Default ports
| Service | Port |
|---|---|
| Web UI / HUD | 10360 (--port) |
| Named-URL proxy | 1360 (--proxy-port) |
(Tilt's own defaults are 10350/1355; Starling uses 10360/1360 so it can
run alongside a real Tilt without colliding.)
Status & roadmap
A working dev tool for local + Kubernetes resources.
- ✅ HTTP/websocket server, full
Viewtype model, frontend served & rendering. - ✅ Starlingfile (Starlark) execution + file watching → real
local_resource(cmd,serve_cmd,deps,resource_deps, links),local(),read_file. - ✅ Docker image builds (matched to workload images).
- ✅ Kubernetes deploy via
kubectl apply+ pod status watch +kubectl logsstreaming, with automatickind load docker-imagefor local kind clusters. Verified end-to-end against kind (examples/k8s). - ✅ Starlingfile live reload: editing it re-executes and reconciles resources (adds/removes), then rebuilds.
- ✅ Integrated portless: embedded reverse proxy + named URLs for serving
resources,
alias()/serve_port, WebSocket/streaming support. - ✅ Central daemon + k9s-style TUI: shared proxy, central port allocation (no cross-instance conflicts), per-project named URLs, aggregated dashboard with live logs and trigger.
- ✅ Embedded apiserver subset (
/proxy/apis/tilt.dev/v1alpha1/uibuttons+/status): the web UI can click buttons and toggle resource disable. - ✅
load()/include()multi-file Starlingfiles; every read file (includes, load targets,read_file,watch_file) is watched for reload. - ✅
docker_compose()(each service becomes a resource) andlive_update(sync()/run()steps thatkubectl cp/execinto a live pod instead of a full rebuild). live_update's in-pod execution needs a running cluster to exercise; the Starlark model + watch wiring are complete. - ✅ Native Docker builds via bollard (Docker API) instead of shelling out.
k8s stays on
kubectl— see note below. - ✅ HTTPS proxy: per-hostname certs minted on the fly (SNI) from a local CA,
starling trustto install the CA,starling hoststo sync/etc/hostsfor non-.localhostTLDs, plain-HTTP→HTTPS redirect on the same port.--lan(mDNS) and--tailscalemodes are wired but experimental. - ✅ TUI:
/filter, Enter detail view,ttrigger,Rrestart,pchange preferred backend port, PgUp/PgDn log scroll. - ✅ Tiltfile API parity: corrected
local_resourcearg order,trigger_mode(+ manual-mode pending behavior), fulllocal/local_resourcekwargs (env/dir/serve_env/serve_dir/labels),custom_build,kustomize/helm,filter_yaml,port_forward(),k8s_resourceextras (labels, objects, extra_pod_selectors), all live_update steps,load_dynamic,k8s_kind.
Notes / honest limitations
- Native k8s client: Kubernetes deploys by shelling out to
kubectl(apply / get pods / logs) pluskind load docker-imageto load locally-built images into a kind cluster (like Tilt). The full inner loop — build (bollard) → kind load → apply → pod-status watch →live_updatesync into the running pod — is verified end-to-end against a local kind cluster (seeexamples/k8s). Swapping the shell-outs for a nativekubeclient is a clean follow-up. - live_update /
--lan/--tailscale: implemented but not exercised by the test suite (they need a live pod / LAN mDNS / a tailnet respectively). - Partial-fidelity builtins:
load_dynamicruns the target's side effects but returns an empty symbol dict (useload()for imports);k8s_kind/k8s_image_json_pathare accepted but don't yet inject images into CRDs;docker_build(target=…)is accepted but bollard's classic builder ignores it.
Tests
cargo test covers the k8s YAML parser (workload/image/selector extraction),
docker-image↔build-ref matching, proxy hostname/URL formatting, and the route
registry. Daemon, reload, named-URL proxy (HTTP+HTTPS), docker_compose, and
native image builds are verified end-to-end against a local Docker daemon and a
kind cluster.