From 529e935deaf638a52f4a9fac7d5976410b9d80b3 Mon Sep 17 00:00:00 2001 From: ZePedroResende Date: Tue, 23 Jun 2026 13:11:47 +0100 Subject: [PATCH 1/2] chore(maestro): add Nix flake dev shell for reproducible toolchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo shipped no committed dev environment, so building required a working personal Nix/home-manager setup on PATH. Add a flake providing the full toolchain pinned via flake.lock: stable rust (per rust-toolchain.toml) + rustfmt/clippy/rust-analyzer, clang + mold (the linker the cargo config expects), nodejs 22, pnpm 9, tmux, git, and just. The shell sets the per-target linker/rustflags so it is self-contained and hermetic: cargo takes the first matching rustflags source (env > target > build), so this replaces any global `[build] rustflags` — in particular the nightly-only `-Z threads=8` that would otherwise break the stable toolchain — while keeping the fast mold link. Use with `nix develop` or `direnv allow` (.envrc). Co-Authored-By: Claude Opus 4.8 (1M context) --- .envrc | 1 + .gitignore | 3 ++ flake.lock | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 78 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 88b236b..06d5349 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ Cargo.lock.bak # Local git worktrees for in-flight branches (never tracked) worktrees/ + +# direnv / nix dev shell +/.direnv diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..71442e2 --- /dev/null +++ b/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1781577229, + "narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "567a49d1913ce81ac6e9582e3553dd90a955875f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1782184651, + "narHash": "sha256-4XHdXp3MRa++XiGkltJChCtdbCmSlNTwQRfWQOhqbRw=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "f59bc28dd0b89e9c0240d1b80195559fc79c2471", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..38d8d5f --- /dev/null +++ b/flake.nix @@ -0,0 +1,78 @@ +{ + description = "Maestro — unified terminal-session orchestrator: reproducible dev shell"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + rust-overlay, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }; + + # Honor the repo's own pin (`rust-toolchain.toml` → stable + rustfmt + + # clippy) so a contributor builds the project as declared, independent of + # whatever toolchain happens to be installed via rustup. + rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + in + { + devShells.default = pkgs.mkShell { + packages = [ + rustToolchain + pkgs.rust-analyzer + + # Fast linker the project's cargo config expects (clang driver + + # `-fuse-ld=mold`). Shipped here so the shell is self-contained and + # does not depend on a personal ~/.cargo/config.toml. + pkgs.clang + pkgs.mold + + # Web frontend (web/ pins pnpm@9.15.4 via package.json#packageManager). + pkgs.nodejs_22 + pkgs.pnpm_9 + + # Integration tests drive real tmux + git (see CLAUDE.md). + pkgs.tmux + pkgs.git + + # Task runner for the Justfile (just web / web-dev / test / fmt). + pkgs.just + ]; + + # Make linking self-contained and hermetic. Cargo picks the FIRST + # matching rustflags source (env > target. > target. > + # build.rustflags) — it does NOT merge them. Setting the per-target + # rustflags here therefore *replaces* any `[build] rustflags` coming + # from a global ~/.cargo/config.toml — in particular the nightly-only + # `-Z threads=8` that would otherwise break this stable toolchain — + # while keeping the fast mold link. + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER = "clang"; + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS = "-C link-arg=-fuse-ld=mold"; + + shellHook = '' + # Use the pnpm pinned in web/package.json (corepack) if present; + # otherwise fall back to the nixpkgs pnpm_9 on PATH. + export RUST_BACKTRACE=1 + echo "maestro dev shell" + echo " rust $(rustc --version 2>/dev/null | cut -d' ' -f2)" + echo " node $(node --version 2>/dev/null)" + echo " pnpm $(pnpm --version 2>/dev/null)" + echo " tmux $(tmux -V 2>/dev/null | cut -d' ' -f2)" + echo "Run the web stack: just web-dev (Vite → /api proxied to a local registry/daemon)" + ''; + }; + } + ); +} -- 2.47.3 From 257773fb3b1c6f4c673ce8c25fefb56798914747 Mon Sep 17 00:00:00 2001 From: ZePedroResende Date: Tue, 23 Jun 2026 13:11:47 +0100 Subject: [PATCH 2/2] feat(maestro): add `just dev` full-stack recipe and fix dev attach proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `just dev` builds maestro and runs the whole local stack in one command — registry (:8080), daemon (:7070), and the Vite web UI (:5173) — waits for each to be healthy, and tears all three down on Ctrl-C. Previously the Justfile only had frontend/CI recipes; running the project meant hand-starting the registry and daemon in separate terminals. Also fix attach under the Vite dev server: `/api/attach/:id` is a daemon-served broker WebSocket (NAP-243), not a registry route, so the old catch-all `/api` proxy sent it to the registry and got 404. Route `/api/attach` to the daemon (overridable via MAESTRO_DAEMON) and keep everything else on the registry — the same split the production maestro-web binary does. Verified: handshake now returns 101 and PTY bytes stream. Co-Authored-By: Claude Opus 4.8 (1M context) --- Justfile | 37 +++++++++++++++++++++++++++++++++++++ web/vite.config.ts | 16 ++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/Justfile b/Justfile index 473bc89..e56b658 100644 --- a/Justfile +++ b/Justfile @@ -1,5 +1,42 @@ # Common dev / build entry points. Run `just` with no args to list. +# The registry (:8080) is the phone book, the daemon (:7070) runs the tmux +# sessions, and Vite serves the UI at http://localhost:5173 (attach is proxied +# to the daemon; see web/vite.config.ts). Override the secret with MAESTRO_TOKEN. +# Run the full local stack — registry + daemon + web UI; Ctrl-C stops all three. +dev: + #!/usr/bin/env bash + set -euo pipefail + token="${MAESTRO_TOKEN:-devtoken}" + bin="target/debug/maestro" + + echo "▶ building maestro…" + cargo build --bin maestro + + pids=() + cleanup() { echo; echo "▶ stopping stack…"; for p in "${pids[@]:-}"; do kill "$p" 2>/dev/null || true; done; } + trap cleanup EXIT INT TERM + + echo "▶ registry → http://127.0.0.1:8080" + MAESTRO_TOKEN="$token" MAESTRO_REGISTRY_BIND_ADDR=127.0.0.1:8080 "$bin" registry & + pids+=($!) + # Wait for the registry to accept connections before starting the daemon. + for _ in $(seq 1 50); do (exec 3<>/dev/tcp/127.0.0.1/8080) 2>/dev/null && break || sleep 0.2; done + + echo "▶ daemon → ws://127.0.0.1:7070 (id: $(hostname))" + MAESTRO_TOKEN="$token" \ + MAESTRO_REGISTRY_URL=http://127.0.0.1:8080 \ + MAESTRO_DAEMON_BIND_ADDR=127.0.0.1:7070 \ + MAESTRO_DAEMON_URL=ws://127.0.0.1:7070 \ + "$bin" daemon run & + pids+=($!) + for _ in $(seq 1 50); do (exec 3<>/dev/tcp/127.0.0.1/7070) 2>/dev/null && break || sleep 0.2; done + + echo "▶ web UI → http://localhost:5173 (Ctrl-C to stop everything)" + pnpm -C web install + MAESTRO_REGISTRY=http://127.0.0.1:8080 MAESTRO_DAEMON=http://127.0.0.1:7070 \ + pnpm -C web dev + # Build the maestro-web binary together with the SPA bundle it embeds. web: pnpm -C web install --frozen-lockfile diff --git a/web/vite.config.ts b/web/vite.config.ts index b4fe962..534be3d 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -19,6 +19,14 @@ export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); const target = resolveProxyTarget(env.MAESTRO_REGISTRY); const secure = target.startsWith("https://"); + // `/api/attach/:id` is a DAEMON-served broker WebSocket (NAP-243), not a + // registry route — the registry returns 404 for it. In production the + // `maestro-web` binary splits this traffic; in dev we replicate that here by + // forwarding `/api/attach` straight to the local daemon. Override with + // MAESTRO_DAEMON; defaults to the standard local daemon bind. + const daemonTarget = env.MAESTRO_DAEMON + ? resolveProxyTarget(env.MAESTRO_DAEMON) + : "http://127.0.0.1:7070"; return { build: { target: "es2022", @@ -28,7 +36,15 @@ export default defineConfig(({ mode }) => { }, server: { port: 5173, + // NOTE: order matters — Vite uses the first matching key, so the more + // specific `/api/attach` must come before the catch-all `/api`. proxy: { + "/api/attach": { + target: daemonTarget, + changeOrigin: false, + ws: true, + secure: false, + }, "/api": { target, changeOrigin: secure, -- 2.47.3