Rewrites CLAUDE.md as the current-architecture reference, drops the registry from INSTALL/PRODUCT/DESIGN and the web README, deletes the registry Dockerfile, and marks the dated design specs superseded where they describe the registry, JWT auth, or cross-host attach. The installer loses its configure step — every config field now has a default, so a daemon starts with no config file at all.
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
target/
|
||||
worktrees/
|
||||
.git/
|
||||
.github/
|
||||
.claude/
|
||||
docs/
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
*.md
|
||||
.env*
|
||||
@@ -1,33 +1,57 @@
|
||||
# Maestro
|
||||
|
||||
A unified terminal-session orchestrator. One place — laptop, desktop, or phone — to see and attach to every interactive shell or agent (Claude Code, opencode, plain bash) running across every machine you care about. tmux for persistence, WebSocket for transport, daemon-per-host federated by a small always-on registry.
|
||||
A terminal-session orchestrator for one machine. One daemon per host owns every
|
||||
interactive shell or agent (Claude Code, opencode, plain bash) running on it;
|
||||
clients — CLI, TUI, browser — attach over a plain HTTP + WebSocket API. tmux for
|
||||
persistence, WebSocket for transport.
|
||||
|
||||
## Where the spec lives
|
||||
## Architecture
|
||||
|
||||
`docs/superpowers/specs/2026-05-24-maestro-design.md` is the source of truth for architecture, wire protocol, session model, auth, repo layout, and scope boundary. **Read it before writing any code in this repo.**
|
||||
```
|
||||
maestro daemon tmux wrapper + session API on 127.0.0.1:7070
|
||||
├── GET/POST /api/sessions list / spawn
|
||||
├── DELETE /api/sessions/:id kill
|
||||
├── GET /api/info hostname, version, home dir, spawn presets
|
||||
├── GET /api/dirs directory listing (spawn autocomplete)
|
||||
├── GET /api/recent-dirs spawn history
|
||||
├── GET /api/gitroot enclosing repo of a path
|
||||
├── WS /api/events Snapshot + session deltas
|
||||
└── WS /api/attach/:id raw PTY bytes
|
||||
|
||||
maestro list/spawn/attach/kill CLI client
|
||||
maestro tui ratatui client
|
||||
maestro-web serves the SPA, reverse-proxies /api/* to the daemon
|
||||
```
|
||||
|
||||
**No authentication layer, no registry, no peer-to-peer.** Whoever can reach the
|
||||
daemon's bind address can drive every session on the host — same trust model as
|
||||
tmux itself. The operator picks the perimeter: `127.0.0.1` for local-only, a
|
||||
WireGuard/LAN address to share with their own devices, never a public IP.
|
||||
|
||||
Every config field has a default, so `~/.config/maestro/config.toml` is
|
||||
optional.
|
||||
|
||||
## Where the work is
|
||||
|
||||
Linear project **Maestro** under team **Naps62**. Issues `NAP-211..218` cover the prototype; each has `blockedBy` set so the dependency chain is explicit. **`NAP-211` is the only unblocked starting point.**
|
||||
Linear project **Maestro** under team **Naps62**.
|
||||
|
||||
`.claude/linear.json` wires `/yolo` and `/work` to this project — they pick from "available now" (status backlog/pending, not blocked).
|
||||
`.claude/linear.json` wires `/yolo` and `/work` to this project — they pick from
|
||||
"available now" (status backlog/pending, not blocked).
|
||||
|
||||
## Repo layout
|
||||
|
||||
Cargo workspace. NAP-245 unified the per-binary crates behind a single
|
||||
`crates/maestro/` umbrella that ships the `maestro` binary with feature gates
|
||||
(`cli` / `daemon` / `registry`, default = all three). The library crates kept
|
||||
their existing shape; only the per-binary `main.rs` files were retired.
|
||||
(`cli` / `daemon` / `tui`, default = all three).
|
||||
|
||||
```
|
||||
crates/
|
||||
├── maestro/ # umbrella binary `maestro` (cli + daemon + registry, feature-gated) (NAP-245)
|
||||
├── maestro/ # umbrella binary `maestro` (cli + daemon + tui, feature-gated) (NAP-245)
|
||||
├── maestro-protocol/ # shared wire types (NAP-211)
|
||||
├── maestro-config/ # shared on-disk config schema (NAP-233)
|
||||
├── maestro-names/ # orchestra-themed session-handle generator (#23)
|
||||
├── maestro-cli/ # `maestro list/spawn/attach/kill/policy` dispatch (NAP-216)
|
||||
├── maestro-daemon/ # `maestro daemon` — tmux wrapper + WS endpoints (NAP-212, NAP-214)
|
||||
├── maestro-registry/ # `maestro registry` — phone book + HTTP API (NAP-213, NAP-215)
|
||||
├── maestro-cli/ # `maestro list/spawn/attach/kill` dispatch (NAP-216)
|
||||
├── maestro-daemon/ # `maestro daemon` — tmux wrapper + HTTP/WS API (NAP-212, NAP-214)
|
||||
├── maestro-web/ # standalone `maestro-web` browser UI (NAP-241, NAP-249)
|
||||
├── maestro-tui/ # `maestro tui` — ratatui session manager (NAP-275 epic)
|
||||
├── maestro-installer/ # `maestro-install` wizard: cargo install + systemd (NAP-225)
|
||||
@@ -37,18 +61,13 @@ web/ # browser SPA (NAP-218)
|
||||
|
||||
See `docs/INSTALL.md` for end-user install instructions.
|
||||
|
||||
The dated design docs under `docs/superpowers/specs/` predate the single-host
|
||||
refactor; they carry banners marking what no longer applies.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **TDD by default.** Failing test first, then minimal impl. Commit test + impl together.
|
||||
- **Frequent commits.** One logical unit per commit; don't pile work up.
|
||||
- **Conventional commits with `maestro` scope.** Example: `feat(maestro): add Hello message round-trip test`.
|
||||
- **`cargo fmt` + `cargo clippy --all-targets -- -D warnings`** before every commit. (Add to CI as part of NAP-211.)
|
||||
- **`cargo fmt` + `cargo clippy --all-targets -- -D warnings`** before every commit.
|
||||
- **Integration tests use real `tmux` and `git`.** Don't mock them. Isolate via temp `$HOME` and `TMUX_TMPDIR`.
|
||||
|
||||
## Network assumption
|
||||
|
||||
Maestro is network-agnostic. The operator chooses bind addresses; the perimeter is whatever VPN (Wireguard, etc.) or private LAN they've set up. Maestro does not terminate TLS itself or police the network. See spec §Auth.
|
||||
|
||||
## Status
|
||||
|
||||
Empty repo. Spec approved and committed. **NAP-211 is the natural start.**
|
||||
|
||||
@@ -54,7 +54,7 @@ Tints/glow derive from the accent at low alpha: `--accent-tint` (0.12),
|
||||
## Typography
|
||||
|
||||
**Monospace is the UI typeface** (brand identity), system-stack only (offline-
|
||||
safe, zero web-font dependency, embedded clean into the registry binary):
|
||||
safe, zero web-font dependency, embedded clean into the web binary):
|
||||
|
||||
- `--font-mono`: `ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", "Roboto Mono", Menlo, Consolas, monospace` — chrome, labels, data, ids, the wordmark.
|
||||
- `--font-sans`: `system-ui` stack — reserved for the few prose strings (empty-state sentences).
|
||||
@@ -86,9 +86,8 @@ no bounce. Tokens `--dur-fast`/`--dur`/`--dur-slow`.
|
||||
## Layout & structure
|
||||
|
||||
Mobile-first, structural responsiveness (not fluid type). Single `≤520px`
|
||||
breakpoint stacks the header (full-width search, wrapped actions) and drops the
|
||||
daemon chips to their own wrapped line. `≥600px` switches the spawn modal from
|
||||
bottom-sheet to centered dialog. A faint slate grid texture sits on `--bg` as
|
||||
breakpoint stacks the header (full-width search, wrapped actions). `≥600px`
|
||||
switches the spawn modal from bottom-sheet to centered dialog. A faint slate grid texture sits on `--bg` as
|
||||
terminal atmosphere (very low contrast, not decoration).
|
||||
|
||||
## Components
|
||||
@@ -96,10 +95,10 @@ terminal atmosphere (very low contrast, not decoration).
|
||||
Three surfaces, all DOM built in TS, styled by class:
|
||||
|
||||
- **Catalog** — sticky blurred header (pulsing wordmark, search, theme toggle,
|
||||
Spawn); status bar (`N sessions · M live` tally + daemon chips); session tree
|
||||
(collapsible folders with counts, leaf rows with status dot, name, `@daemon`
|
||||
+ command meta, exited badge, reveal-on-hover kill). Empty states are
|
||||
distinct and teach: waiting-for-daemon, no-sessions, no-search-match.
|
||||
Spawn); status bar (`N sessions · M live` tally); session tree (collapsible
|
||||
directory folders with counts, leaf rows with status dot, name, command meta,
|
||||
exited badge, reveal-on-hover kill). Empty states are distinct and teach:
|
||||
no-sessions, no-search-match.
|
||||
- **Attach** — header (back, `session <id>`, live/closed/error state pill) over
|
||||
a framed dark xterm with a green cursor.
|
||||
- **Spawn modal** — bottom-sheet/dialog form; mono inputs with green focus
|
||||
@@ -112,5 +111,5 @@ status — never decoration.
|
||||
## Dev review aids (DEV-only, stripped from production)
|
||||
|
||||
`web/src/mock.ts` + query params seed a representative catalog without a running
|
||||
registry: `?mock` (seed sessions/daemons), `?mock&spawn` (open the spawn
|
||||
modal), `?theme=light|dark` (pin a theme). Guarded by `import.meta.env.DEV`.
|
||||
daemon: `?mock` (seed sessions), `?mock&spawn` (open the spawn modal),
|
||||
`?theme=light|dark` (pin a theme). Guarded by `import.meta.env.DEV`.
|
||||
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Image for the Maestro registry.
|
||||
#
|
||||
# NAP-245 retired the per-binary crates: the registry now ships as the
|
||||
# `registry` subcommand of the unified `maestro` binary. This image builds that
|
||||
# binary with only the `registry` feature enabled (no cli/daemon/tui), so it
|
||||
# stays small and pulls only maestro-registry + protocol + config.
|
||||
#
|
||||
# Build: docker build -t maestro-registry .
|
||||
# Run: docker run --rm -p 8080:8080 -e MAESTRO_TOKEN=<secret> maestro-registry
|
||||
#
|
||||
# On Dokploy: point at this Dockerfile, expose port 8080 (or whatever you
|
||||
# set MAESTRO_REGISTRY_BIND_ADDR to), and set MAESTRO_TOKEN as a secret env.
|
||||
# No persistent volume is required when MAESTRO_TOKEN is provided.
|
||||
|
||||
FROM rust:1-bookworm AS builder
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/build/target \
|
||||
cargo build --release -p maestro --no-default-features --features registry \
|
||||
&& cp /build/target/release/maestro /maestro-registry
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /maestro-registry /usr/local/bin/maestro-registry
|
||||
|
||||
# Default to all-interfaces so Dokploy / Traefik can reach the service.
|
||||
# Override via env on the platform.
|
||||
ENV MAESTRO_REGISTRY_BIND_ADDR=0.0.0.0:8080
|
||||
EXPOSE 8080
|
||||
|
||||
# The on-disk binary is the unified `maestro`; run its `registry` subcommand.
|
||||
ENTRYPOINT ["/usr/local/bin/maestro-registry", "registry"]
|
||||
@@ -8,7 +8,7 @@ web:
|
||||
|
||||
# Run the web bundle in dev mode against a local daemon — Vite serves the
|
||||
# frontend with HMR, and `/api/*` is proxied directly to the daemon at
|
||||
# 127.0.0.1:7070 (or wherever $MAESTRO_REGISTRY points; see vite.config.ts).
|
||||
# 127.0.0.1:7070 (see vite.config.ts).
|
||||
web-dev:
|
||||
pnpm -C web install
|
||||
pnpm -C web dev
|
||||
|
||||
+12
-13
@@ -7,22 +7,21 @@ product
|
||||
## Users
|
||||
|
||||
A single technical operator (a developer) managing every interactive shell and
|
||||
agent — Claude Code, opencode, plain bash — running across multiple machines.
|
||||
The job-to-be-done: from any device (laptop, desktop, **phone**), get an
|
||||
accurate at-a-glance picture of all live sessions across all hosts, then attach
|
||||
to one and drive its terminal. Usage is often glance-and-go: checking on a
|
||||
agent — Claude Code, opencode, plain bash — running on a machine. The
|
||||
job-to-be-done: from any device (laptop, desktop, **phone**), get an accurate
|
||||
at-a-glance picture of all live sessions on that host, then attach to one and
|
||||
drive its terminal. Usage is often glance-and-go: checking on a
|
||||
long-running agent, killing a stray session, or hopping into a shell mid-task.
|
||||
Mobile-first is a hard requirement — touch targets, viewport-fit safe areas,
|
||||
and bottom-sheet modals already reflect this.
|
||||
|
||||
## Product Purpose
|
||||
|
||||
Maestro is a unified terminal-session orchestrator: one place to see and attach
|
||||
to every interactive session running across every machine the operator cares
|
||||
about. tmux for persistence, WebSocket for transport, a daemon-per-host
|
||||
federated by a small always-on registry. The web UI is the browser face of
|
||||
that system — it renders the cross-host session catalog and provides the attach
|
||||
terminal (xterm.js). Success = the operator trusts the picture is accurate and
|
||||
Maestro is a terminal-session orchestrator: one place to see and attach to
|
||||
every interactive session running on a machine. tmux for persistence,
|
||||
WebSocket for transport, one daemon per host. The web UI is the browser face of
|
||||
that system — it renders the session catalog and provides the attach terminal
|
||||
(xterm.js). Success = the operator trusts the picture is accurate and
|
||||
current, and can get from "open the page" to "typing in the right session" in
|
||||
seconds, on any device.
|
||||
|
||||
@@ -51,14 +50,14 @@ efficient, calm / uncluttered, with earned moments of delight.
|
||||
## Design Principles
|
||||
|
||||
1. **The scan is the product.** Optimize for instant comprehension of "what's
|
||||
running, where, and is it healthy" across many sessions and hosts. Status
|
||||
legibility beats everything else.
|
||||
running, where, and is it healthy" across many sessions. Status legibility
|
||||
beats everything else.
|
||||
2. **Calm under load.** Stay uncluttered whether there are 3 sessions or 300;
|
||||
hierarchy and grouping carry the weight, not color-shouting every row.
|
||||
3. **Fast and keyboard-literate.** Low-friction, efficient, gets out of the
|
||||
way; honor keyboard navigation, don't make the operator hunt.
|
||||
4. **Monospace as identity.** Treat the mono/sans pairing as deliberate brand
|
||||
typography, not just terminal output. Ids, hosts, counts, and state read as
|
||||
typography, not just terminal output. Ids, paths, counts, and state read as
|
||||
precise technical data.
|
||||
5. **Earned delight.** Personality lives in motion and micro-detail (live-status
|
||||
pulse, attach transitions, cursor cues), never in noise — always craft.
|
||||
|
||||
@@ -137,9 +137,9 @@ impl DaemonSection {
|
||||
/// `[web]` — `maestro-web` binary settings (NAP-241).
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WebSection {
|
||||
/// Address `maestro-web` binds. Default `127.0.0.1:8080`. Bind a
|
||||
/// perimeter-facing address (LAN/WG) to share with other devices on a
|
||||
/// trusted segment; do NOT bind a public address without adding auth.
|
||||
/// Address `maestro-web` binds. Default `127.0.0.1:8080`. Bind a LAN/VPN
|
||||
/// address to share with your own devices on a trusted segment; there is no
|
||||
/// auth layer, so never bind a public address.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bind_addr: Option<SocketAddr>,
|
||||
/// HTTP base URL of the co-located daemon `maestro-web` proxies to.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
//! - relative `a/b` → home + `a/b`
|
||||
//! - absolute `/a/b` → as-is
|
||||
//!
|
||||
//! The pure [`list_dir`] is unit-tested in isolation; the registry-client task
|
||||
//! The pure [`list_dir`] is unit-tested in isolation; the HTTP handler
|
||||
//! just wraps its `Result` in an `RpcResult` for the wire.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! to its worktree toplevel.
|
||||
//!
|
||||
//! It's a pure filesystem walk with no `git` subprocess — same shape as
|
||||
//! [`crate::dirlist`]: unit-tested in isolation, and the registry-client task /
|
||||
//! [`crate::dirlist`]: unit-tested in isolation, and the HTTP handler /
|
||||
//! HTTP layer (#26) just wraps its `Option` for the wire.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -201,6 +201,7 @@ pub fn state_for(
|
||||
// WS /api/attach/:id — raw PTY bytes for one session
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn attach_ws(
|
||||
Path(id): Path<Uuid>,
|
||||
State(s): State<LocalApiState>,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! worktrees, and PTY bridges.
|
||||
//!
|
||||
//! Owns the daemon's authoritative in-memory map of live sessions and is the
|
||||
//! single point through which the registry client and the per-session WS
|
||||
//! single point through which the API handlers and the per-session WS
|
||||
//! handler interact with tmux.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -23,8 +23,8 @@ use crate::spawn_history::{RecentDir, SpawnHistory, SpawnHistoryRecord};
|
||||
use crate::tmux::{Tmux, TmuxError};
|
||||
use crate::worktree::{WorktreeError, WorktreeManager};
|
||||
|
||||
/// Capacity of the session-events broadcast channel. The registry client
|
||||
/// listens; if it lags by this much we just drop frames (the registry already
|
||||
/// Capacity of the session-events broadcast channel. The `/api/events`
|
||||
/// publisher listens; if it lags by this much we just drop frames (clients
|
||||
/// reconciles on `Hello` via the snapshot). Sourced from the shared
|
||||
/// [`maestro_config::BroadcastTuning`] so all broadcast channels track one
|
||||
/// definition (NAP-273).
|
||||
@@ -467,7 +467,7 @@ impl SessionManager {
|
||||
/// Run a blocking `tmux` subprocess call on the blocking thread pool so it
|
||||
/// never stalls a tokio worker. `tmux` is a fork+exec that can take tens of
|
||||
/// milliseconds on a loaded host, and `send_keys` rides the per-keystroke hot
|
||||
/// path — running it inline would starve HTTP handlers, the registry WS, the
|
||||
/// path — running it inline would starve HTTP handlers, the events WS, the
|
||||
/// liveness watcher, and PTY readers (NAP-256).
|
||||
async fn tmux_blocking<T, F>(f: F) -> T
|
||||
where
|
||||
|
||||
@@ -59,6 +59,7 @@ async fn spawn_daemon_ws() -> Harness {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn connect_attach(
|
||||
addr: SocketAddr,
|
||||
session: Uuid,
|
||||
|
||||
@@ -44,9 +44,7 @@ impl SystemctlAction {
|
||||
/// Build the `cargo install` argument vector.
|
||||
///
|
||||
/// `features`, when `Some`, restricts the build to an explicit feature set via
|
||||
/// `--no-default-features --features <list>`. The installer uses this to skip
|
||||
/// the `registry` feature of the umbrella `maestro` crate: a host runs the CLI
|
||||
/// + daemon (+ TUI); the registry is a separate, centrally-deployed service.
|
||||
/// `--no-default-features --features <list>`.
|
||||
pub fn cargo_install_args(
|
||||
source: &CargoSource,
|
||||
crate_name: &str,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! `maestro-install` — interactive wizard that installs the unified `maestro`
|
||||
//! binary via `cargo install`, optionally drops a systemd user unit, and
|
||||
//! optionally kicks off `maestro daemon configure`.
|
||||
//! binary via `cargo install` and optionally drops a systemd user unit.
|
||||
//!
|
||||
//! Intended bootstrap path:
|
||||
//!
|
||||
@@ -22,13 +21,9 @@ use maestro_installer::{
|
||||
};
|
||||
|
||||
const DEFAULT_GIT_URL: &str = "https://git.naps.pt/naps62/maestro";
|
||||
/// Unified `maestro` crate. The installer builds it with the host feature set
|
||||
/// (CLI + daemon + TUI); the `registry` feature is left out (see
|
||||
/// `MAESTRO_HOST_FEATURES`).
|
||||
/// Unified `maestro` crate, built with the full host feature set.
|
||||
const MAESTRO_CRATE: &str = "maestro";
|
||||
/// Features installed for a host: CLI + per-host daemon + TUI. The `registry`
|
||||
/// feature is deliberately omitted — the registry is a separate, centrally-
|
||||
/// deployed service, not something a host install should compile or run.
|
||||
/// Features installed for a host: CLI + per-host daemon + TUI.
|
||||
const MAESTRO_HOST_FEATURES: &str = "cli,daemon,tui";
|
||||
const WEB_CRATE: &str = "maestro-web";
|
||||
const SYSTEMD_PROBE: &str = "/run/systemd/system";
|
||||
@@ -57,7 +52,7 @@ struct Args {
|
||||
#[arg(long)]
|
||||
no_locked: bool,
|
||||
|
||||
/// Don't prompt — accept all defaults (install + systemd + configure).
|
||||
/// Don't prompt — accept all defaults (install + systemd).
|
||||
#[arg(long, short)]
|
||||
yes: bool,
|
||||
|
||||
@@ -65,10 +60,6 @@ struct Args {
|
||||
#[arg(long)]
|
||||
no_systemd: bool,
|
||||
|
||||
/// Skip the `maestro daemon configure` step.
|
||||
#[arg(long)]
|
||||
no_configure: bool,
|
||||
|
||||
/// Override the ExecStart path written to the systemd unit.
|
||||
/// Default: `~/.cargo/bin/maestro` (invoked as `maestro daemon`).
|
||||
#[arg(long, value_name = "PATH")]
|
||||
@@ -107,51 +98,25 @@ fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 1. Install the unified binary (NAP-245), minus the registry feature.
|
||||
// 1. Install the unified binary (NAP-245).
|
||||
let locked = !args.no_locked;
|
||||
install_crate(&source, MAESTRO_CRATE, locked, Some(MAESTRO_HOST_FEATURES))?;
|
||||
|
||||
// 2. Offer to run `maestro daemon configure` now (must happen before systemd
|
||||
// start, since the daemon refuses to run without registry_url / token).
|
||||
// Re-runs of the wizard auto-skip when the unified config already has a
|
||||
// [daemon] section; that's what makes "re-run to update" pleasant. In
|
||||
// non-interactive mode (--yes / no TTY) we also skip — `maestro daemon
|
||||
// configure` would prompt for required fields and we don't want to
|
||||
// surprise scripts.
|
||||
let configured = if let Some(path) = maestro_config_path() {
|
||||
if daemon_section_present(&path) {
|
||||
eprintln!(
|
||||
"\n[daemon] section already present in {} — skipping configure step.",
|
||||
path.display()
|
||||
);
|
||||
eprintln!("(run `maestro daemon configure --force` to rewrite it.)");
|
||||
true
|
||||
} else {
|
||||
run_configure_step(&args, interactive)?
|
||||
}
|
||||
} else {
|
||||
run_configure_step(&args, interactive)?
|
||||
};
|
||||
|
||||
// 3. systemd integration.
|
||||
// 2. systemd integration.
|
||||
let systemd_detected = is_systemd_host(Path::new(SYSTEMD_PROBE));
|
||||
if args.no_systemd {
|
||||
eprintln!("\nSkipping systemd step (--no-systemd).");
|
||||
} else if !systemd_detected {
|
||||
eprintln!("\nNo systemd detected ({SYSTEMD_PROBE} missing). Skipping unit install.");
|
||||
} else {
|
||||
handle_systemd(&args, interactive, configured)?;
|
||||
handle_systemd(&args, interactive)?;
|
||||
}
|
||||
|
||||
// 4. Optional: maestro-web (NAP-249). Re-runs auto-detect an existing
|
||||
// 3. Optional: maestro-web (NAP-249). Re-runs auto-detect an existing
|
||||
// install and refresh rather than re-prompting, matching the daemon flow.
|
||||
let web_installed = handle_web(&args, interactive, systemd_detected)?;
|
||||
|
||||
print_summary(
|
||||
configured,
|
||||
systemd_detected && !args.no_systemd,
|
||||
web_installed,
|
||||
);
|
||||
print_summary(systemd_detected && !args.no_systemd, web_installed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -170,12 +135,9 @@ fn print_banner(source: &CargoSource) {
|
||||
eprintln!("maestro installer");
|
||||
eprintln!();
|
||||
eprintln!("This wizard will:");
|
||||
eprintln!(
|
||||
" 1. cargo install `{MAESTRO_CRATE}` (CLI + daemon + TUI; registry runs separately)"
|
||||
);
|
||||
eprintln!(" 2. (optional) run `maestro daemon configure` to bootstrap the config file");
|
||||
eprintln!(" 3. (optional) install a systemd user unit and enable it");
|
||||
eprintln!(" 4. (optional) install `{WEB_CRATE}` + its systemd unit (browser UI)");
|
||||
eprintln!(" 1. cargo install `{MAESTRO_CRATE}` (CLI + daemon + TUI)");
|
||||
eprintln!(" 2. (optional) install a systemd user unit and enable it");
|
||||
eprintln!(" 3. (optional) install `{WEB_CRATE}` + its systemd unit (browser UI)");
|
||||
eprintln!();
|
||||
match source {
|
||||
CargoSource::Git { url, branch } => {
|
||||
@@ -205,65 +167,7 @@ fn install_crate(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn maestro_config_path() -> Option<PathBuf> {
|
||||
Some(dirs::config_dir()?.join("maestro").join("config.toml"))
|
||||
}
|
||||
|
||||
/// True iff the unified file exists and already has a `[daemon]` section.
|
||||
/// Used to skip the configure prompt on re-runs without forcing the user
|
||||
/// to manually delete a co-located `[registry]` / `[cli]` section first.
|
||||
fn daemon_section_present(path: &Path) -> bool {
|
||||
if !path.exists() {
|
||||
return false;
|
||||
}
|
||||
let Ok(body) = std::fs::read_to_string(path) else {
|
||||
return false;
|
||||
};
|
||||
body.lines().any(|l| l.trim_start().starts_with("[daemon]"))
|
||||
}
|
||||
|
||||
fn run_configure_step(args: &Args, interactive: bool) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
let want = interactive
|
||||
&& !args.no_configure
|
||||
&& confirm(
|
||||
"Run `maestro daemon configure` now (writes ~/.config/maestro/config.toml)?",
|
||||
true,
|
||||
)?;
|
||||
if want {
|
||||
run_configure_passthrough()
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_configure_passthrough() -> Result<bool, Box<dyn std::error::Error>> {
|
||||
// Run the freshly-installed `maestro daemon configure` interactively. We
|
||||
// don't hard-code the path; rely on it being on PATH after `cargo install`.
|
||||
eprintln!("\n==> maestro daemon configure");
|
||||
let status = Command::new("maestro")
|
||||
.args(["daemon", "configure"])
|
||||
.status();
|
||||
match status {
|
||||
Ok(s) if s.success() => Ok(true),
|
||||
Ok(s) => {
|
||||
eprintln!("`maestro daemon configure` exited {s}");
|
||||
Ok(false)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"couldn't launch `maestro`: {e}. Make sure ~/.cargo/bin is on PATH, \
|
||||
then run `maestro daemon configure` manually."
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_systemd(
|
||||
args: &Args,
|
||||
interactive: bool,
|
||||
configured: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn handle_systemd(args: &Args, interactive: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let unit_path = user_unit_path().ok_or("could not resolve ~/.config/systemd/user/")?;
|
||||
let exec_start = match &args.exec_start {
|
||||
Some(p) => p.clone(),
|
||||
@@ -326,12 +230,6 @@ fn handle_systemd(
|
||||
} else {
|
||||
let want = if !interactive {
|
||||
false
|
||||
} else if !configured {
|
||||
eprintln!(
|
||||
"Note: starting now will fail until you run `maestro daemon configure` \
|
||||
(no registry_url / token yet)."
|
||||
);
|
||||
confirm("Start maestro.service now anyway?", false)?
|
||||
} else {
|
||||
confirm("Start maestro.service now?", true)?
|
||||
};
|
||||
@@ -421,10 +319,10 @@ fn handle_web(
|
||||
} else {
|
||||
eprintln!(
|
||||
"\nOptional: install the browser UI (`maestro-web`)?\n \
|
||||
Reverse-proxies a local maestro daemon and serves the SPA at \
|
||||
Reverse-proxies the local maestro daemon and serves the SPA at \
|
||||
http://127.0.0.1:8080/ by default.\n \
|
||||
Edit [web].bind_addr in ~/.config/maestro/config.toml \
|
||||
to expose it to your perimeter (e.g. WireGuard IP)."
|
||||
to expose it on your LAN/VPN address."
|
||||
);
|
||||
confirm("Install maestro-web?", true)?
|
||||
};
|
||||
@@ -549,21 +447,18 @@ fn confirm(prompt: &str, default_yes: bool) -> io::Result<bool> {
|
||||
})
|
||||
}
|
||||
|
||||
fn print_summary(configured: bool, systemd_enabled: bool, web_installed: bool) {
|
||||
fn print_summary(systemd_enabled: bool, web_installed: bool) {
|
||||
eprintln!();
|
||||
eprintln!("Done.");
|
||||
eprintln!();
|
||||
eprintln!("Next steps:");
|
||||
if !configured {
|
||||
eprintln!(" • Run `maestro daemon configure` to bootstrap your config file.");
|
||||
}
|
||||
if systemd_enabled {
|
||||
eprintln!(" • Check status: `systemctl --user status maestro`");
|
||||
eprintln!(" • Logs: `journalctl --user -u maestro -f`");
|
||||
} else {
|
||||
eprintln!(" • Start the daemon: `maestro daemon`");
|
||||
}
|
||||
eprintln!(" • Set the CLI's registry: `maestro set-registry <URL>`");
|
||||
eprintln!(" • Open the TUI: `maestro tui`");
|
||||
if web_installed {
|
||||
if systemd_enabled {
|
||||
eprintln!(
|
||||
|
||||
@@ -5,7 +5,7 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
publish.workspace = true
|
||||
description = "Shared wire types for Maestro daemon/registry/client communication"
|
||||
description = "Shared wire types for Maestro daemon/client communication"
|
||||
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
|
||||
@@ -142,6 +142,7 @@ async fn dispatch(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn cli_dispatch(
|
||||
command: maestro_cli::cli::Command,
|
||||
config: Option<PathBuf>,
|
||||
|
||||
+24
-67
@@ -16,11 +16,9 @@ The bootstrap script builds `maestro-installer` into a temp directory, runs the
|
||||
wizard, and cleans up — only the unified `maestro` binary ends up persisted in
|
||||
`~/.cargo/bin`.
|
||||
|
||||
**Updating is the same command.** Re-running `./install.sh` detects an
|
||||
existing `[daemon]` section in `~/.config/maestro/config.toml` and an
|
||||
existing systemd unit, skips the configure prompt, refreshes the unit, and
|
||||
runs `systemctl --user restart maestro` instead of `start`. No `--update`
|
||||
flag needed. **The restart keeps your sessions:** the unit sets
|
||||
**Updating is the same command.** Re-running `./install.sh` detects an existing
|
||||
systemd unit, refreshes it, and runs `systemctl --user restart maestro` instead
|
||||
of `start`. No `--update` flag needed. **The restart keeps your sessions:** the unit sets
|
||||
`KillMode=process`, so systemd signals only the daemon — the tmux server (and
|
||||
every session) survives, and the new daemon re-adopts them on start. A unit
|
||||
written before this default lacks the line; re-run `./install.sh` to refresh it.
|
||||
@@ -43,11 +41,9 @@ The wizard:
|
||||
|
||||
1. Runs `cargo install` for the unified `maestro` crate (pinned to the
|
||||
committed `Cargo.lock` via `--locked`). The same binary hosts the CLI,
|
||||
the per-host daemon (`maestro daemon`), and the always-on registry
|
||||
(`maestro registry`), gated by Cargo features.
|
||||
2. Optionally runs `maestro daemon configure` to populate `~/.config/maestro/config.toml`
|
||||
(one shared TOML for the CLI, daemon, registry, and web).
|
||||
3. On systemd hosts, optionally writes a user unit at
|
||||
the per-host daemon (`maestro daemon`), and the TUI, gated by Cargo
|
||||
features.
|
||||
2. On systemd hosts, optionally writes a user unit at
|
||||
`~/.config/systemd/user/maestro.service`, reloads systemd, and offers to
|
||||
enable + start it.
|
||||
|
||||
@@ -55,15 +51,11 @@ The binary lands in `~/.cargo/bin/maestro` — make sure that's on your `$PATH`.
|
||||
|
||||
## Thin builds for single-role hosts
|
||||
|
||||
The default install pulls in everything (`cli` + `daemon` + `registry` + `tui`).
|
||||
Single-role hosts can opt out of unused dependencies:
|
||||
The default install pulls in everything (`cli` + `daemon` + `tui`). Single-role
|
||||
hosts can opt out of unused dependencies:
|
||||
|
||||
```sh
|
||||
# Registry server — no tmux/PTY/jwt-verifier code:
|
||||
cargo install --git https://git.naps.pt/naps62/maestro \
|
||||
--no-default-features --features registry --locked maestro
|
||||
|
||||
# Daemon-only host — no embedded web assets, no registry HTTP surface:
|
||||
# Daemon-only host — no CLI or TUI:
|
||||
cargo install --git https://git.naps.pt/naps62/maestro \
|
||||
--no-default-features --features daemon --locked maestro
|
||||
|
||||
@@ -81,8 +73,7 @@ in the default feature set, so a vanilla `cargo install maestro` builds it.
|
||||
Drop the `tui` feature to skip it on hosts that don't need the UI surface
|
||||
(`--no-default-features --features cli,daemon`).
|
||||
|
||||
It talks only to the **local** `maestro daemon` over `http://127.0.0.1:7070`
|
||||
by default; peer daemons reach you transparently via the catalog mirror.
|
||||
It talks to the local `maestro daemon` over `http://127.0.0.1:7070` by default.
|
||||
Override the daemon URL with `--daemon-url` or `$MAESTRO_DAEMON_URL`.
|
||||
|
||||
The `[tui]` section in `~/.config/maestro/config.toml` (or whatever path you
|
||||
@@ -117,8 +108,8 @@ The full action vocabulary lives in
|
||||
`maestro-web` is a separate binary that serves the SPA bundle and reverse-
|
||||
proxies API calls to a co-located `maestro daemon`.
|
||||
|
||||
**Interactive install (default path).** The wizard now offers it as an
|
||||
optional step right after the daemon flow:
|
||||
**Interactive install (default path).** The wizard offers it as an optional
|
||||
step right after the daemon flow:
|
||||
|
||||
```text
|
||||
Optional: install the browser UI (`maestro-web`)?
|
||||
@@ -150,9 +141,11 @@ http://127.0.0.1:7070`. The `[web]` section of
|
||||
`~/.config/maestro/config.toml` overrides both — see
|
||||
`crates/maestro-web/README.md`.
|
||||
|
||||
**Trust model.** Anyone who can reach the bind address can use it. Pick
|
||||
`127.0.0.1` for local-only, a WireGuard/LAN address for one-segment access,
|
||||
never a public IP without adding auth first.
|
||||
**Trust model.** There is no authentication layer: anyone who can reach the
|
||||
bind address (of either the daemon or `maestro-web`) can drive every session on
|
||||
the host. Pick `127.0.0.1` for local-only, a WireGuard/LAN address for
|
||||
one-segment access, and never a public IP — the network perimeter is the only
|
||||
boundary Maestro has.
|
||||
|
||||
Headless hosts (servers, dev VMs) skip the prompt with `--no-web`. Hosts
|
||||
where you want browser access answer yes — both binaries end up in
|
||||
@@ -168,10 +161,9 @@ where you want browser access answer yes — both binaries end up in
|
||||
| `--no-locked` | Drop `--locked` from `cargo install` (resolves a fresh dep tree). |
|
||||
| `--exec-start <path>` | Override the systemd unit's `ExecStart` (default `~/.cargo/bin/maestro daemon`). |
|
||||
| `--no-systemd` | Skip the systemd step even if detected. |
|
||||
| `--no-configure` | Skip the `maestro daemon configure` step. |
|
||||
| `--no-web` | Skip the optional `maestro-web` install step. |
|
||||
| `--web-exec-start <path>` | Override the web unit's `ExecStart` (default `~/.cargo/bin/maestro-web`). |
|
||||
| `-y`, `--yes` | Non-interactive: install binaries + drop systemd unit + reload + enable, but **don't** auto-run `configure` or auto-start the unit (those need real config). Also skips the web step unless an existing `maestro-web.service` is being refreshed. |
|
||||
| `-y`, `--yes` | Non-interactive: install binaries + drop systemd unit + reload + enable, but **don't** auto-start the unit. Also skips the web step unless an existing `maestro-web.service` is being refreshed. |
|
||||
|
||||
## Where tmux sockets land
|
||||
|
||||
@@ -190,44 +182,6 @@ Pick `/tmp` if the daemon must outlive your login session and you can't run
|
||||
tmux sockets with it). The env var `MAESTRO_DAEMON_TMUX_TMPDIR` takes
|
||||
precedence over the file.
|
||||
|
||||
## Cross-host attach (perimeter setup)
|
||||
|
||||
Attaching to a session that lives on **another** host is brokered over the "T2"
|
||||
transport, which only works when both daemons advertise a **matching
|
||||
`perimeter_id`** and the session's owner advertises a **reachable
|
||||
`private_addr`**. These default to unset, and without them every cross-host
|
||||
attach fails — the registry resolves the session but returns no perimeter
|
||||
candidate, so the attaching daemon rejects it (`no_perimeter_candidate`) and the
|
||||
browser pane shows the reason and closes. Same-host attach is unaffected.
|
||||
|
||||
Set both on **every** host in `~/.config/maestro/config.toml`:
|
||||
|
||||
```toml
|
||||
[daemon]
|
||||
# Identical on every daemon that should reach each other — name it after your
|
||||
# VPN/LAN segment. Two daemons broker to each other only when these match.
|
||||
perimeter_id = "wg-home"
|
||||
# THIS host's address as its peers reach it on that perimeter (host:port of the
|
||||
# daemon's bind_addr). Per-host — never the same value on two daemons.
|
||||
private_addr = "10.0.0.7:7070"
|
||||
```
|
||||
|
||||
It's both-or-neither: setting only one is ignored with a startup warning. A
|
||||
daemon started with no perimeter configured logs a `WARN` saying cross-host
|
||||
attach is unavailable. (T1 direct-WSS was dropped — NAP-235 — and T3 relay isn't
|
||||
implemented — NAP-238 — so T2 is currently the only cross-host path.)
|
||||
|
||||
## Peer attach trust model
|
||||
|
||||
There is no per-peer allow-list. The operator's network perimeter (VPN/firewall)
|
||||
is the trust boundary for *which hosts can connect*; every daemon reachable on
|
||||
the perimeter may broker-attach to any other. Attach is still authenticated: the
|
||||
registry mints a short-lived, pair-scoped JWT bound to the target daemon and the
|
||||
specific session (`iss`/`aud`/`sid`/`exp`), which the target verifies — so a
|
||||
reachable host still can't attach to a session it wasn't handed, or replay a
|
||||
token elsewhere. (NAP-339 dropped the earlier `[daemon.policy].allow_inbound`
|
||||
gate; re-introducing a per-pair allow-list later is a verify-time change.)
|
||||
|
||||
## What the systemd unit looks like
|
||||
|
||||
The wizard writes a *user* unit (`systemctl --user`), since `maestro daemon`
|
||||
@@ -273,13 +227,16 @@ script avoids that by installing to a scratch dir.
|
||||
|
||||
```sh
|
||||
cargo install --git https://git.naps.pt/naps62/maestro --locked maestro
|
||||
maestro daemon configure
|
||||
maestro daemon # foreground; or wire your own service manager
|
||||
```
|
||||
|
||||
Every setting has a default, so `~/.config/maestro/config.toml` is optional —
|
||||
create it only to change the bind address, worktree root, spawn presets, or TUI
|
||||
keymap.
|
||||
|
||||
## Non-Linux hosts
|
||||
|
||||
The wizard works on macOS for the `cargo install` + `maestro daemon configure`
|
||||
parts. The systemd step is automatically skipped (probe of
|
||||
The wizard works on macOS for the `cargo install` part. The systemd step is
|
||||
automatically skipped (probe of
|
||||
`/run/systemd/system`). launchd integration is not yet supported — start
|
||||
`maestro daemon` from your shell or your own service manager.
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
**Status**: Approved (brainstorm)
|
||||
**Scope**: First-pass prototype. No production polish.
|
||||
|
||||
> **Superseded (2026-08-22).** Maestro is now single-host: one daemon per
|
||||
> machine, clients (CLI, TUI, web) talking straight to it, no registry, no
|
||||
> peer-to-peer attach, and no authentication layer — reaching the daemon's bind
|
||||
> address is the authorization. Everything below about the registry, the
|
||||
> daemon↔registry control link, JWTs, cookies, and cross-host transports is
|
||||
> historical. `CLAUDE.md` describes the current architecture.
|
||||
|
||||
## What this is
|
||||
|
||||
A unified terminal-session orchestrator. One place — laptop, desktop, or phone — to see and attach to every interactive shell or agent (Claude Code, opencode, plain bash) running across every machine you care about: workstation, desktop, dev VM, always-on VMs, and small LXC containers.
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
- `docs/superpowers/specs/2026-05-26-connection-orchestration.md` — broker / catalog-mirror surface this TUI rides on
|
||||
- NAP-230 (CLI attach footer) — adjacent, separate; NAP-244 (CLI through broker) — orthogonal
|
||||
|
||||
> **Partly superseded (2026-08-22).** Maestro is now single-host — the registry and cross-host catalog it references no longer exist. See `CLAUDE.md`.
|
||||
|
||||
## 1. Goals & non-goals
|
||||
|
||||
### Goals
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
**Date:** 2026-05-28
|
||||
**Status:** Approved (design)
|
||||
|
||||
> **Partly superseded (2026-08-22).** Maestro is now single-host — the bundle is embedded in `maestro-web`, not a registry binary. See `CLAUDE.md`.
|
||||
|
||||
## Problem
|
||||
|
||||
`maestro-web` embeds the SPA bundle at **compile time** via
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Spawn form: home-dir prefill + live directory autocomplete (NAP-329)
|
||||
|
||||
> **Partly superseded (2026-08-22).** Maestro is now single-host — the endpoint is now `GET /api/dirs` with no daemon id in the path. See `CLAUDE.md`.
|
||||
|
||||
## Problem
|
||||
|
||||
The web spawn form's **Working directory** field is `required` but offers no
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
**Depends on**: NAP-330 (`KillMode=process` — restart without session loss). **Done.**
|
||||
**Touches**: NAP-225 (installer / systemd unit), the "prebuilt binaries for thin hosts" idea.
|
||||
|
||||
> **Partly superseded (2026-08-22).** Maestro is now single-host — the registry it references no longer exists. See `CLAUDE.md`.
|
||||
|
||||
## What this is
|
||||
|
||||
Turn today's yolo-local update hack into a first-class, cross-host maestro
|
||||
|
||||
+18
-24
@@ -1,7 +1,7 @@
|
||||
# maestro-web
|
||||
|
||||
Browser UI for Maestro. Connects to the registry HTTP/WS API for the catalog
|
||||
and to individual daemons (via direct WebSocket) for terminal I/O.
|
||||
Browser UI for Maestro. Talks to a single local daemon over its `/api/*`
|
||||
HTTP/WS surface — session catalog, spawn/kill, and terminal I/O.
|
||||
|
||||
## Framework choice: vanilla TypeScript
|
||||
|
||||
@@ -11,17 +11,17 @@ rendering; everything else is a session tree and a spawn form, both trivial
|
||||
in plain DOM.
|
||||
|
||||
This decision is non-architectural — swap in a framework later without
|
||||
touching the registry or daemon if needed.
|
||||
touching the daemon if needed.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/
|
||||
main.ts # entry point, hash-route dispatch
|
||||
api.ts # registry HTTP API client
|
||||
api.ts # daemon HTTP API client
|
||||
events.ts # `/api/events` WebSocket subscriber
|
||||
catalog.ts # session tree view (groups by metadata.label)
|
||||
attach.ts # xterm.js + daemon WS attach view
|
||||
catalog.ts # session tree view (groups by working directory)
|
||||
attach.ts # xterm.js + attach WS view
|
||||
spawn.ts # spawn-session modal
|
||||
theme.ts # dark/light theme toggle + persistence
|
||||
types.ts # mirror of `maestro-protocol` JSON shapes
|
||||
@@ -39,40 +39,34 @@ follows the OS by default and can be pinned via the header toggle (persisted in
|
||||
`localStorage`).
|
||||
|
||||
`mock.ts` is a DEV-only fixture (stripped from production builds): visit the dev
|
||||
server with `?mock` to seed a representative catalog without a running registry,
|
||||
server with `?mock` to seed a representative catalog without a running daemon,
|
||||
`?mock&spawn` to open the spawn modal, and `?theme=light|dark` to pin a theme.
|
||||
|
||||
Routing is hash-based:
|
||||
- `#/` → catalog
|
||||
- `#/attach/<session-id>` → terminal
|
||||
|
||||
This keeps the registry's static handler dumb (no SPA fallback needed).
|
||||
Concurrent attaches: open the same `#/attach/<id>` URL in another tab.
|
||||
This keeps the static handler dumb (no SPA fallback needed). Concurrent
|
||||
attaches: open the same `#/attach/<id>` URL in another tab.
|
||||
|
||||
## Dev
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm dev # http://localhost:5173, proxies /api → http://127.0.0.1:8080
|
||||
pnpm build # → dist/, embedded into the registry binary at cargo build time
|
||||
pnpm dev # http://localhost:5173, proxies /api → http://127.0.0.1:7070
|
||||
pnpm build # → dist/, embedded into the maestro-web binary at cargo build time
|
||||
```
|
||||
|
||||
The Vite dev server proxies `/api` and the events WebSocket to a
|
||||
`maestro-registry`. Default is `http://127.0.0.1:8080`; point it elsewhere by
|
||||
exporting `MAESTRO_REGISTRY` before `pnpm dev`:
|
||||
The Vite dev server proxies `/api` and the WebSockets to a local daemon.
|
||||
Default is `http://127.0.0.1:7070`; point it elsewhere by exporting
|
||||
`MAESTRO_DAEMON_URL` before `pnpm dev`:
|
||||
|
||||
```sh
|
||||
MAESTRO_REGISTRY=https://maestro-registry.example.com pnpm dev
|
||||
# Daemon-facing URL also works — `wss://...` is rewritten to `https://...`
|
||||
# and a trailing `/daemon` is stripped.
|
||||
MAESTRO_REGISTRY=wss://maestro-registry.example.com/daemon pnpm dev
|
||||
MAESTRO_DAEMON_URL=http://laptop.lan:7070 pnpm dev
|
||||
```
|
||||
|
||||
Direct daemon WS connections (for attach) bypass the proxy and use the URL
|
||||
returned by `GET /api/sessions/:id/connect-info`.
|
||||
|
||||
## Production
|
||||
|
||||
`pnpm build` writes the bundle to `web/dist/`. The Rust registry crate
|
||||
embeds that directory via `rust-embed` at compile time and serves it from
|
||||
`/`. See `crates/maestro-registry/src/web_assets.rs`.
|
||||
`pnpm build` writes the bundle to `web/dist/`. The `maestro-web` crate embeds
|
||||
that directory at compile time and serves it from `/`, reverse-proxying `/api/*`
|
||||
to the daemon. See `crates/maestro-web/src/static_bundle.rs`.
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ const store = new CatalogStore();
|
||||
store.start();
|
||||
|
||||
// Dev-only: `?mock` seeds a representative catalog so the UI can be reviewed
|
||||
// without a running registry. Stripped from production builds by Vite.
|
||||
// without a running daemon. Stripped from production builds by Vite.
|
||||
if (import.meta.env.DEV && location.search.includes("mock")) {
|
||||
void import("./mock.js").then((m) => m.seedMock(store));
|
||||
}
|
||||
|
||||
+6
-8
@@ -1,23 +1,21 @@
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
|
||||
// Where the dev proxy forwards `/api/*` (and the events WS) to. Default to a
|
||||
// local `maestro-registry` on 127.0.0.1:8080. Override by exporting either:
|
||||
// MAESTRO_REGISTRY=https://maestro-registry.example.com (preferred)
|
||||
// MAESTRO_REGISTRY=wss://maestro-registry.example.com/daemon
|
||||
// The second form (the daemon-facing URL) is normalised: ws/wss → http/https
|
||||
// and any trailing `/daemon` is stripped, since the proxy needs the base URL.
|
||||
// local daemon on 127.0.0.1:7070. Override with:
|
||||
// MAESTRO_DAEMON_URL=http://laptop.lan:7070
|
||||
// `ws://`/`wss://` forms are normalised to http/https, since the proxy needs an
|
||||
// HTTP base URL.
|
||||
function resolveProxyTarget(raw: string | undefined): string {
|
||||
if (!raw) return "http://127.0.0.1:8080";
|
||||
if (!raw) return "http://127.0.0.1:7070";
|
||||
return raw
|
||||
.replace(/^wss:\/\//, "https://")
|
||||
.replace(/^ws:\/\//, "http://")
|
||||
.replace(/\/daemon\/?$/, "")
|
||||
.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
const target = resolveProxyTarget(env.MAESTRO_REGISTRY);
|
||||
const target = resolveProxyTarget(env.MAESTRO_DAEMON_URL);
|
||||
const secure = target.startsWith("https://");
|
||||
return {
|
||||
build: {
|
||||
|
||||
Reference in New Issue
Block a user