chore(maestro): retarget installer, e2e harness, and docs at unified maestro binary (NAP-245)

- Installer: one `cargo install maestro` instead of two; ExecStart becomes
  `~/.cargo/bin/maestro daemon`; user unit renamed `maestrod.service` →
  `maestro.service`; configure step is `maestro daemon configure`.
- E2E harness: both `Daemon` and `Registry` subprocess wrappers shell out to
  the unified `maestro` binary with the appropriate subcommand.
- State dir: `~/.local/state/maestrod` → `~/.local/state/maestro`.
- Docs: CLAUDE.md repo layout, design spec repo layout + "Daemon (`maestro daemon`)"
  heading, INSTALL.md (full rewrite of CLI references + new thin-builds section).
This commit is contained in:
2026-05-27 10:43:51 +00:00
parent 35482118b1
commit ade0d49096
12 changed files with 170 additions and 141 deletions
+12 -6
View File
@@ -12,19 +12,25 @@ Linear project **Maestro** under team **Naps62**. Issues `NAP-211..218` cover th
`.claude/linear.json` wires `/yolo` and `/work` to this project — they pick from "available now" (status backlog/pending, not blocked).
## Repo layout (target)
## Repo layout
Cargo workspace, populated incrementally starting with NAP-211:
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.
```
crates/
├── maestro/ # umbrella binary `maestro` (cli + daemon + registry, feature-gated) (NAP-245)
├── maestro-protocol/ # shared wire types (NAP-211)
├── maestro-daemon/ # per-host tmux wrapper + WS endpoints (NAP-212, NAP-214)
├── maestro-registry/ # always-on phone book + HTTP API (NAP-213, NAP-215)
├── maestro-cli/ # `maestro list/spawn/attach/kill` (NAP-216)
├── maestro-config/ # shared on-disk config schema (NAP-233)
├── 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-web/ # standalone `maestro-web` browser UI (NAP-241, NAP-249)
├── maestro-installer/ # `maestro-install` wizard: cargo install + systemd (NAP-225)
└── maestro-e2e/ # cross-process integration tests (NAP-217)
web/ # browser client (NAP-218, plan 2)
web/ # browser SPA (NAP-218)
```
See `docs/INSTALL.md` for end-user install instructions.
+2 -2
View File
@@ -245,7 +245,7 @@ pub fn default_state_dir() -> Result<PathBuf, ConfigError> {
let base = dirs::state_dir()
.or_else(dirs::data_local_dir)
.ok_or(maestro_config::ConfigError::NoConfigDir)?;
Ok(base.join("maestrod"))
Ok(base.join("maestro"))
}
/// Read the kernel hostname via `gethostname(2)`. Falls back to `"unknown"`
@@ -285,7 +285,7 @@ daemon_url = "ws://localhost:7070"
assert_eq!(cfg.daemon_url, "ws://localhost:7070");
assert_eq!(cfg.token, "abc");
assert_eq!(cfg.bind_addr, default_bind_addr());
assert!(cfg.state_dir.ends_with("maestrod"));
assert!(cfg.state_dir.ends_with("maestro"));
assert!(cfg.worktree_root.ends_with("worktrees"));
assert_eq!(cfg.daemon_id, cfg.hostname);
assert!(!cfg.hostname.is_empty());
+2 -1
View File
@@ -189,8 +189,9 @@ fn launch(
tmux_tmpdir: &std::path::Path,
opts: &DaemonOptions,
) -> Child {
let bin = workspace_target_bin("maestro-daemon", "maestrod");
let bin = workspace_target_bin("maestro", "maestro");
let mut cmd = Command::new(&bin);
cmd.arg("daemon");
cmd.env_clear()
.env(
"PATH",
+2 -1
View File
@@ -27,8 +27,9 @@ impl Registry {
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, port));
let home = TempDir::new().expect("home tmpdir");
let bin = workspace_target_bin("maestro-registry", "maestro-registry");
let bin = workspace_target_bin("maestro", "maestro");
let child = Command::new(&bin)
.arg("registry")
.env_clear()
// PATH so tmux etc. resolve. We need a few essentials.
.env(
+1 -1
View File
@@ -5,7 +5,7 @@ edition.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
description = "Interactive install wizard for maestrod + maestro: `cargo install` both binaries and (on systemd hosts) drop a user unit"
description = "Interactive install wizard for the unified `maestro` binary; cargo-installs it and (on systemd hosts) drops a user unit"
[lib]
path = "src/lib.rs"
+13 -23
View File
@@ -101,14 +101,10 @@ mod tests {
url: "https://git.naps.pt/naps62/maestro".into(),
branch: None,
};
let args = cargo_install_args(&src, "maestro-daemon", false);
let args = cargo_install_args(&src, "maestro", false);
assert_eq!(
args,
vec![
"--git",
"https://git.naps.pt/naps62/maestro",
"maestro-daemon",
]
vec!["--git", "https://git.naps.pt/naps62/maestro", "maestro",]
);
}
@@ -118,7 +114,7 @@ mod tests {
url: "https://git.example/x".into(),
branch: Some("main".into()),
};
let args = cargo_install_args(&src, "maestro-cli", true);
let args = cargo_install_args(&src, "maestro", true);
assert_eq!(
args,
vec![
@@ -126,7 +122,7 @@ mod tests {
"https://git.example/x",
"--branch",
"main",
"maestro-cli",
"maestro",
"--locked",
]
);
@@ -135,41 +131,35 @@ mod tests {
#[test]
fn path_args_use_p_flag() {
let src = CargoSource::Path(PathBuf::from("/repos/maestro"));
let args = cargo_install_args(&src, "maestro-daemon", true);
let args = cargo_install_args(&src, "maestro", true);
assert_eq!(
args,
vec![
"--path",
"/repos/maestro",
"-p",
"maestro-daemon",
"--locked",
]
vec!["--path", "/repos/maestro", "-p", "maestro", "--locked",]
);
}
#[test]
fn systemctl_daemon_reload_omits_unit() {
let args = systemctl_user_args(SystemctlAction::DaemonReload, "maestrod.service");
let args = systemctl_user_args(SystemctlAction::DaemonReload, "maestro.service");
assert_eq!(args, vec!["--user", "daemon-reload"]);
}
#[test]
fn systemctl_enable_includes_unit() {
let args = systemctl_user_args(SystemctlAction::Enable, "maestrod.service");
assert_eq!(args, vec!["--user", "enable", "maestrod.service"]);
let args = systemctl_user_args(SystemctlAction::Enable, "maestro.service");
assert_eq!(args, vec!["--user", "enable", "maestro.service"]);
}
#[test]
fn systemctl_start_includes_unit() {
let args = systemctl_user_args(SystemctlAction::Start, "maestrod.service");
assert_eq!(args, vec!["--user", "start", "maestrod.service"]);
let args = systemctl_user_args(SystemctlAction::Start, "maestro.service");
assert_eq!(args, vec!["--user", "start", "maestro.service"]);
}
#[test]
fn systemctl_restart_includes_unit() {
let args = systemctl_user_args(SystemctlAction::Restart, "maestrod.service");
assert_eq!(args, vec!["--user", "restart", "maestrod.service"]);
let args = systemctl_user_args(SystemctlAction::Restart, "maestro.service");
assert_eq!(args, vec!["--user", "restart", "maestro.service"]);
}
#[test]
+2 -1
View File
@@ -1,4 +1,5 @@
//! `maestro-installer` — install wizard for maestrod + maestro.
//! `maestro-installer` — install wizard for the unified `maestro` binary
//! (NAP-245).
//!
//! See `docs/INSTALL.md` for the user-facing flow. This crate exposes the
//! testable pieces:
+37 -36
View File
@@ -1,6 +1,6 @@
//! `maestro-install` — interactive wizard that installs `maestrod` + `maestro`
//! via `cargo install`, optionally drops a systemd user unit, and optionally
//! kicks off `maestrod configure`.
//! `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`.
//!
//! Intended bootstrap path:
//!
@@ -22,8 +22,8 @@ use maestro_installer::{
};
const DEFAULT_GIT_URL: &str = "https://git.naps.pt/naps62/maestro";
const DAEMON_CRATE: &str = "maestro-daemon";
const CLI_CRATE: &str = "maestro-cli";
/// Unified `maestro` crate — single `cargo install` covers CLI + daemon + registry.
const MAESTRO_CRATE: &str = "maestro";
const WEB_CRATE: &str = "maestro-web";
const SYSTEMD_PROBE: &str = "/run/systemd/system";
@@ -31,7 +31,7 @@ const SYSTEMD_PROBE: &str = "/run/systemd/system";
#[command(
name = "maestro-install",
version,
about = "Install maestrod + maestro CLI, and (optionally) a systemd user unit"
about = "Install the unified `maestro` binary, and (optionally) a systemd user unit"
)]
struct Args {
/// Git URL to install from (default: this repo).
@@ -59,12 +59,12 @@ struct Args {
#[arg(long)]
no_systemd: bool,
/// Skip the `maestrod configure` step.
/// Skip the `maestro daemon configure` step.
#[arg(long)]
no_configure: bool,
/// Override the ExecStart path written to the systemd unit.
/// Default: `~/.cargo/bin/maestrod`.
/// Default: `~/.cargo/bin/maestro` (invoked as `maestro daemon`).
#[arg(long, value_name = "PATH")]
exec_start: Option<PathBuf>,
@@ -101,16 +101,15 @@ fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}
// 1. Install both binaries.
// 1. Install the unified binary (NAP-245).
let locked = !args.no_locked;
install_crate(&source, DAEMON_CRATE, locked)?;
install_crate(&source, CLI_CRATE, locked)?;
install_crate(&source, MAESTRO_CRATE, locked)?;
// 2. Offer to run `maestrod configure` now (must happen before systemd
// 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 — `maestrod
// 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() {
@@ -119,7 +118,7 @@ fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
"\n[daemon] section already present in {} — skipping configure step.",
path.display()
);
eprintln!("(run `maestrod configure --force` to rewrite it.)");
eprintln!("(run `maestro daemon configure --force` to rewrite it.)");
true
} else {
run_configure_step(&args, interactive)?
@@ -165,8 +164,8 @@ fn print_banner(source: &CargoSource) {
eprintln!("maestro installer");
eprintln!();
eprintln!("This wizard will:");
eprintln!(" 1. cargo install both `{DAEMON_CRATE}` and `{CLI_CRATE}`");
eprintln!(" 2. (optional) run `maestrod configure` to bootstrap the config file");
eprintln!(" 1. cargo install `{MAESTRO_CRATE}` (unified CLI + daemon + registry)");
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!();
@@ -218,7 +217,7 @@ fn run_configure_step(args: &Args, interactive: bool) -> Result<bool, Box<dyn st
let want = interactive
&& !args.no_configure
&& confirm(
"Run `maestrod configure` now (writes ~/.config/maestro/config.toml)?",
"Run `maestro daemon configure` now (writes ~/.config/maestro/config.toml)?",
true,
)?;
if want {
@@ -229,20 +228,22 @@ fn run_configure_step(args: &Args, interactive: bool) -> Result<bool, Box<dyn st
}
fn run_configure_passthrough() -> Result<bool, Box<dyn std::error::Error>> {
// Run the freshly-installed `maestrod configure` interactively. We don't
// hard-code the path; rely on it being on PATH after `cargo install`.
eprintln!("\n==> maestrod configure");
let status = Command::new("maestrod").arg("configure").status();
// 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!("`maestrod configure` exited {s}");
eprintln!("`maestro daemon configure` exited {s}");
Ok(false)
}
Err(e) => {
eprintln!(
"couldn't launch `maestrod`: {e}. Make sure ~/.cargo/bin is on PATH, \
then run `maestrod configure` manually."
"couldn't launch `maestro`: {e}. Make sure ~/.cargo/bin is on PATH, \
then run `maestro daemon configure` manually."
);
Ok(false)
}
@@ -297,7 +298,7 @@ fn handle_systemd(
let want_enable = if !interactive {
true
} else {
confirm("Enable maestrod.service (start on login)?", true)?
confirm("Enable maestro.service (start on login)?", true)?
};
if want_enable {
run_systemctl(SystemctlAction::Enable, UNIT_FILE_NAME)?;
@@ -310,7 +311,7 @@ fn handle_systemd(
let want = if !interactive {
true
} else {
confirm("Restart maestrod.service to pick up the new binary?", true)?
confirm("Restart maestro.service to pick up the new binary?", true)?
};
(SystemctlAction::Restart, want)
} else {
@@ -318,12 +319,12 @@ fn handle_systemd(
false
} else if !configured {
eprintln!(
"Note: starting now will fail until you run `maestrod configure` \
"Note: starting now will fail until you run `maestro daemon configure` \
(no registry_url / token yet)."
);
confirm("Start maestrod.service now anyway?", false)?
confirm("Start maestro.service now anyway?", false)?
} else {
confirm("Start maestrod.service now?", true)?
confirm("Start maestro.service now?", true)?
};
(SystemctlAction::Start, want)
};
@@ -335,7 +336,7 @@ fn handle_systemd(
}
fn default_exec_start() -> Result<PathBuf, Box<dyn std::error::Error>> {
maestro_installer::systemd::default_maestrod_path()
maestro_installer::systemd::default_maestro_path()
.ok_or_else(|| "could not resolve $HOME (needed for ExecStart)".into())
}
@@ -378,7 +379,7 @@ fn handle_web(
} else {
eprintln!(
"\nOptional: install the browser UI (`maestro-web`)?\n \
Reverse-proxies a local maestrod and serves the SPA at \
Reverse-proxies a 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)."
@@ -390,7 +391,7 @@ fn handle_web(
}
// Install the binary. Even if cargo-install fails we keep going so the
// operator can fix the cause and re-run; the existing maestrod work
// operator can fix the cause and re-run; the existing daemon work
// doesn't need to be undone.
let source = build_source(args);
let locked = !args.no_locked;
@@ -491,13 +492,13 @@ fn print_summary(configured: bool, systemd_enabled: bool, web_installed: bool) {
eprintln!();
eprintln!("Next steps:");
if !configured {
eprintln!(" • Run `maestrod configure` to bootstrap your config file.");
eprintln!(" • Run `maestro daemon configure` to bootstrap your config file.");
}
if systemd_enabled {
eprintln!(" • Check status: `systemctl --user status maestrod`");
eprintln!(" • Logs: `journalctl --user -u maestrod -f`");
eprintln!(" • Check status: `systemctl --user status maestro`");
eprintln!(" • Logs: `journalctl --user -u maestro -f`");
} else {
eprintln!(" • Start the daemon: `maestrod`");
eprintln!(" • Start the daemon: `maestro daemon`");
}
eprintln!(" • Set the CLI's registry: `maestro set-registry <URL>`");
if web_installed {
+34 -28
View File
@@ -1,15 +1,16 @@
//! systemd detection + user-unit rendering.
//!
//! The wizard only ever writes a *user* unit (`~/.config/systemd/user/`), since
//! `maestrod` runs per-user and uses tmux sockets in the user's runtime dir.
//! `maestro daemon` runs per-user and uses tmux sockets in the user's runtime dir.
use std::path::{Path, PathBuf};
/// Standard probe path. Present on hosts booted under systemd.
pub const SYSTEMD_PROBE_PATH: &str = "/run/systemd/system";
/// Filename written under `~/.config/systemd/user/` for the daemon.
pub const UNIT_FILE_NAME: &str = "maestrod.service";
/// Filename written under `~/.config/systemd/user/` for the daemon (NAP-245
/// renamed this from `maestrod.service`).
pub const UNIT_FILE_NAME: &str = "maestro.service";
/// Filename for the optional `maestro-web` browser UI unit (NAP-249).
pub const WEB_UNIT_FILE_NAME: &str = "maestro-web.service";
@@ -27,7 +28,7 @@ pub fn user_unit_path_for(unit_name: &str) -> Option<PathBuf> {
Some(base.join("systemd").join("user").join(unit_name))
}
/// Convenience: path for `maestrod.service`. Kept as a free function so
/// Convenience: path for `maestro.service`. Kept as a free function so
/// existing callers don't need to thread the unit name around.
pub fn user_unit_path() -> Option<PathBuf> {
user_unit_path_for(UNIT_FILE_NAME)
@@ -35,23 +36,27 @@ pub fn user_unit_path() -> Option<PathBuf> {
/// Convenience: path for `maestro-web.service` (NAP-249).
pub fn user_web_unit_path() -> Option<PathBuf> {
user_unit_path_for(WEB_UNIT_FILE_NAME)
user_web_unit_path_for(WEB_UNIT_FILE_NAME)
}
/// Render the daemon unit. `exec_start` is the absolute path to `maestrod` —
/// we default to `~/.cargo/bin/maestrod` because that's where `cargo install`
fn user_web_unit_path_for(unit_name: &str) -> Option<PathBuf> {
user_unit_path_for(unit_name)
}
/// Render the daemon unit. `exec_start` is the absolute path to `maestro` —
/// we default to `~/.cargo/bin/maestro` because that's where `cargo install`
/// lands binaries; systemd doesn't inherit the user's PATH, so we can't rely
/// on a bare `maestrod`.
/// on a bare `maestro`. The unit runs `maestro daemon` (NAP-245).
pub fn render_user_unit(exec_start: &Path) -> String {
format!(
"[Unit]\n\
Description=Maestro per-host daemon (maestrod)\n\
Description=Maestro per-host daemon\n\
Documentation=https://git.naps.pt/naps62/maestro\n\
After=network-online.target\n\
\n\
[Service]\n\
Type=simple\n\
ExecStart={exec}\n\
ExecStart={exec} daemon\n\
ExecReload=/bin/kill -HUP $MAINPID\n\
Restart=on-failure\n\
RestartSec=5\n\
@@ -62,7 +67,7 @@ pub fn render_user_unit(exec_start: &Path) -> String {
)
}
/// Render the `maestro-web` user unit (NAP-249). Orders after `maestrod`
/// Render the `maestro-web` user unit (NAP-249). Orders after `maestro.service`
/// so the proxy doesn't return 502s on startup; `Wants=` keeps it best-effort
/// — operators with daemon-less hosts can still install web alone if they
/// later remove the dependency.
@@ -71,8 +76,8 @@ pub fn render_web_unit(exec_start: &Path) -> String {
"[Unit]\n\
Description=Maestro web UI (maestro-web)\n\
Documentation=https://git.naps.pt/naps62/maestro\n\
After=network-online.target maestrod.service\n\
Wants=maestrod.service\n\
After=network-online.target maestro.service\n\
Wants=maestro.service\n\
\n\
[Service]\n\
Type=simple\n\
@@ -86,15 +91,10 @@ pub fn render_web_unit(exec_start: &Path) -> String {
)
}
/// Default `ExecStart` target: `~/.cargo/bin/maestrod`. `None` if `$HOME` is
/// Default `ExecStart` target: `~/.cargo/bin/maestro`. `None` if `$HOME` is
/// unresolvable.
pub fn default_maestrod_path() -> Option<PathBuf> {
Some(
dirs::home_dir()?
.join(".cargo")
.join("bin")
.join("maestrod"),
)
pub fn default_maestro_path() -> Option<PathBuf> {
Some(dirs::home_dir()?.join(".cargo").join("bin").join("maestro"))
}
/// Default `ExecStart` target for the web unit: `~/.cargo/bin/maestro-web`.
@@ -128,8 +128,8 @@ mod tests {
#[test]
fn unit_renders_with_absolute_exec_start() {
let body = render_user_unit(Path::new("/home/alice/.cargo/bin/maestrod"));
assert!(body.contains("ExecStart=/home/alice/.cargo/bin/maestrod"));
let body = render_user_unit(Path::new("/home/alice/.cargo/bin/maestro"));
assert!(body.contains("ExecStart=/home/alice/.cargo/bin/maestro daemon"));
assert!(body.contains("[Unit]"));
assert!(body.contains("[Service]"));
assert!(body.contains("[Install]"));
@@ -139,9 +139,9 @@ mod tests {
#[test]
fn unit_wires_execreload_to_sighup() {
// NAP-237: `systemctl --user reload maestrod` must trigger our
// NAP-237: `systemctl --user reload maestro` must trigger our
// SIGHUP handler so operators can hot-reload `[daemon.policy]`.
let body = render_user_unit(Path::new("/x/maestrod"));
let body = render_user_unit(Path::new("/x/maestro"));
assert!(body.contains("ExecReload=/bin/kill -HUP $MAINPID"));
}
@@ -154,7 +154,7 @@ mod tests {
#[test]
fn user_unit_path_points_under_config_dir() {
let p = user_unit_path().expect("config dir resolvable in test env");
assert!(p.ends_with("systemd/user/maestrod.service"), "got {p:?}");
assert!(p.ends_with("systemd/user/maestro.service"), "got {p:?}");
}
#[test]
@@ -167,12 +167,18 @@ mod tests {
fn web_unit_renders_with_dependency_on_daemon() {
let body = render_web_unit(Path::new("/home/alice/.cargo/bin/maestro-web"));
assert!(body.contains("ExecStart=/home/alice/.cargo/bin/maestro-web"));
assert!(body.contains("After=network-online.target maestrod.service"));
assert!(body.contains("Wants=maestrod.service"));
assert!(body.contains("After=network-online.target maestro.service"));
assert!(body.contains("Wants=maestro.service"));
assert!(body.contains("WantedBy=default.target"));
assert!(body.ends_with('\n'));
}
#[test]
fn default_maestro_path_under_cargo_bin() {
let p = default_maestro_path().expect("HOME resolvable in test env");
assert!(p.ends_with(".cargo/bin/maestro"), "got {p:?}");
}
#[test]
fn default_maestro_web_path_under_cargo_bin() {
let p = default_maestro_web_path().expect("HOME resolvable in test env");
+49 -31
View File
@@ -13,13 +13,13 @@ The repo is private, so grab `install.sh` while signed in to gitea, then run it:
```
The bootstrap script builds `maestro-installer` into a temp directory, runs the
wizard, and cleans up — only `maestrod` and `maestro` end up persisted in
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 maestrod` instead of `start`. No `--update`
runs `systemctl --user restart maestro` instead of `start`. No `--update`
flag needed.
Inside the script, `cargo install --git https://git.naps.pt/naps62/maestro …`
@@ -38,20 +38,39 @@ Pass flags through to the wizard after `./install.sh`:
The wizard:
1. Runs `cargo install` for `maestro-daemon` and `maestro-cli` (pinned to the
committed `Cargo.lock` via `--locked`).
2. Optionally runs `maestrod configure` to populate `~/.config/maestro/config.toml`
(one shared TOML for `maestrod`, `maestro-registry`, and the `maestro` CLI).
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
`~/.config/systemd/user/maestrod.service`, reloads systemd, and offers to
`~/.config/systemd/user/maestro.service`, reloads systemd, and offers to
enable + start it.
Both binaries land in `~/.cargo/bin` — make sure that's on your `$PATH`.
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. Registry-only and daemon-only 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:
cargo install --git https://git.naps.pt/naps62/maestro \
--no-default-features --features daemon --locked maestro
```
The wizard always installs the default-feature build.
## Adding the browser UI (optional)
`maestro-web` is a separate binary that serves the SPA bundle and reverse-
proxies API calls to a co-located `maestrod`.
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:
@@ -63,7 +82,7 @@ Install maestro-web? [Y/n]
Press `Y` and it `cargo install`s `maestro-web`, drops a
`~/.config/systemd/user/maestro-web.service` unit (ordered after
`maestrod.service`), reloads systemd, and offers to enable + start it. Re-runs
`maestro.service`), reloads systemd, and offers to enable + start it. Re-runs
of `./install.sh` detect the existing unit and refresh + restart silently.
Pass `--no-web` to skip the prompt entirely (headless hosts), or `--yes`
@@ -100,18 +119,18 @@ where you want browser access answer yes — both binaries end up in
|---|---|
| `--git <url>` | Install from a different git repo (default: this one). |
| `--branch <name>` | Pin a branch with `--git`. |
| `--path <dir>` | Install from a local workspace checkout (`--path` + `-p <crate>`). |
| `--path <dir>` | Install from a local workspace checkout (`--path` + `-p maestro`). |
| `--no-locked` | Drop `--locked` from `cargo install` (resolves a fresh dep tree). |
| `--exec-start <path>` | Override the systemd unit's `ExecStart` (default `~/.cargo/bin/maestrod`). |
| `--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 `maestrod configure` step. |
| `--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. |
## Where tmux sockets land
`maestrod` defaults `TMUX_TMPDIR` to `$XDG_RUNTIME_DIR` (= `/run/user/$UID`)
`maestro daemon` defaults `TMUX_TMPDIR` to `$XDG_RUNTIME_DIR` (= `/run/user/$UID`)
so a plain `tmux ls` in your shell — which typically uses the same env var
— sees maestro-managed sessions. Override in
`~/.config/maestro/config.toml`:
@@ -129,13 +148,13 @@ precedence over the file.
## Hot-reloading policy
`[daemon.policy].allow_inbound` (the list of peer daemons allowed to broker-
attach into this one) can be changed without restarting `maestrod` — restarting
would kill every live session. Two ways to apply an edit:
attach into this one) can be changed without restarting the daemon — restarting
would kill every live session. Three ways to apply an edit:
```sh
maestro policy reload # POSTs to the local daemon's /api/policy/reload
maestro policy show # prints the current effective list
kill -HUP <maestrod-pid> # equivalent; what `systemctl --user reload` does
kill -HUP <maestro-pid> # equivalent; what `systemctl --user reload` does
```
Only newly-arriving attach attempts see the change — sessions already
@@ -143,18 +162,18 @@ attached stay attached. To boot a live attacher out, kill the session.
## What the systemd unit looks like
The wizard writes a *user* unit (`systemctl --user`), since `maestrod` runs
per-user and uses tmux sockets under `$XDG_RUNTIME_DIR`. Body:
The wizard writes a *user* unit (`systemctl --user`), since `maestro daemon`
runs per-user and uses tmux sockets under `$XDG_RUNTIME_DIR`. Body:
```ini
[Unit]
Description=Maestro per-host daemon (maestrod)
Description=Maestro per-host daemon
Documentation=https://git.naps.pt/naps62/maestro
After=network-online.target
[Service]
Type=simple
ExecStart=%h/.cargo/bin/maestrod
ExecStart=%h/.cargo/bin/maestro daemon
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
@@ -166,8 +185,8 @@ WantedBy=default.target
Check status / logs:
```sh
systemctl --user status maestrod
journalctl --user -u maestrod -f
systemctl --user status maestro
journalctl --user -u maestro -f
```
## Running the wizard without the bootstrap script
@@ -185,15 +204,14 @@ script avoids that by installing to a scratch dir.
## Fully manual install (no wizard)
```sh
cargo install --git https://git.naps.pt/naps62/maestro --locked maestro-daemon
cargo install --git https://git.naps.pt/naps62/maestro --locked maestro-cli
maestrod configure
maestrod # foreground; or wire your own service manager
cargo install --git https://git.naps.pt/naps62/maestro --locked maestro
maestro daemon configure
maestro daemon # foreground; or wire your own service manager
```
## Non-Linux hosts
The wizard works on macOS for the `cargo install` + `maestrod configure` parts.
The systemd step is automatically skipped (probe of `/run/systemd/system`).
launchd integration is not yet supported — start `maestrod` from your shell or
your own service manager.
The wizard works on macOS for the `cargo install` + `maestro daemon configure`
parts. 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.
@@ -61,7 +61,7 @@ UI polish, layout/tiling, edge-case terminals — all out of scope.
Three components, all reachable over an operator-managed network (LAN, VPN, mesh — maestro is network-agnostic; the operator configures bind addresses and ensures clients can route to them):
- **Registry** — one always-on instance. Knows who's online and what sessions exist. Serves the web client bundle. Never touches PTY bytes.
- **Daemon (`maestrod`)** — one per host. Wraps local tmux. Streams sessions directly to clients over WebSocket.
- **Daemon (`maestro daemon`)** — one per host. Wraps local tmux. Streams sessions directly to clients over WebSocket.
- **Web client** — one TS bundle. Connects to the registry for the catalog, then directly to whichever daemon owns the session being attached.
The registry is a phone book, not a relay. This keeps it tiny and pushes byte traffic to a direct daemon ↔ client link. Confidentiality of that link is the operator's responsibility (Wireguard, a private LAN, mTLS in a future iteration — maestro itself does not encrypt at the transport layer beyond what the operator's network provides).
@@ -85,7 +85,7 @@ Non-responsibilities:
Runs as a single Rust binary on a designated VM. Listens on a configured `bind_addr` (default `127.0.0.1:8080`; operator sets the actual reachable address — e.g. a Wireguard interface IP — in `[registry].bind_addr` of the unified config at `~/.config/maestro/config.toml`).
### Daemon (`maestrod`)
### Daemon (`maestro daemon`)
Responsibilities:
@@ -253,16 +253,21 @@ Single-user trust model. The operator's network (Wireguard / private LAN / etc.)
## Repo layout
Cargo workspace:
Cargo workspace. NAP-245 collapsed the three binaries (`maestro`, `maestrod`, `maestro-registry`) into a single `maestro` binary with feature-gated subcommands. Library crates kept their existing shape; per-binary `main.rs` files are gone.
```
maestro/
├── Cargo.toml # workspace
├── crates/
│ ├── maestro-protocol/ # message types, shared between binaries
│ ├── maestro-registry/ # registry binary
│ ├── maestro-daemon/ # daemon binary
│ └── maestro-cli/ # `maestro` CLI for scripting (spawn/list/kill from shell)
│ ├── maestro/ # umbrella binary: `maestro <list|spawn|attach|kill|set-registry|policy|daemon|registry>` (features: cli/daemon/registry)
│ ├── maestro-protocol/ # message types, shared between subsystems
│ ├── maestro-config/ # unified on-disk config schema
│ ├── maestro-cli/ # CLI subcommand types + dispatch (library)
│ ├── maestro-daemon/ # daemon subsystem (library + `maestro daemon` dispatch)
│ ├── maestro-registry/ # registry subsystem (library + `maestro registry` dispatch)
│ ├── maestro-web/ # standalone `maestro-web` browser-UI binary
│ ├── maestro-installer/ # `maestro-install` wizard
│ └── maestro-e2e/ # cross-process integration tests
├── web/
│ ├── src/
│ ├── package.json
@@ -273,7 +278,7 @@ maestro/
└── CLAUDE.md
```
`maestro-cli` is a low-priority bonus — handy for scripted spawning and for testing the registry HTTP API. Drops out of scope if it slows the plan.
Thin builds for single-role hosts: `cargo install --no-default-features --features {cli|daemon|registry} maestro` produces a binary with only that subsystem's dependencies compiled in.
## Out of scope (prototype)
+3 -3
View File
@@ -1,7 +1,7 @@
#!/bin/sh
# Bootstrap installer for maestro. Builds the `maestro-installer` crate into a
# scratch dir, runs the wizard, and cleans up — leaving only the binaries the
# wizard installs (`maestrod`, `maestro`) in `~/.cargo/bin`.
# scratch dir, runs the wizard, and cleans up — leaving only the unified
# `maestro` binary in `~/.cargo/bin`.
#
# Usage:
# curl -sSf https://git.naps.pt/naps62/maestro/raw/branch/main/install.sh | sh
@@ -9,7 +9,7 @@
# Pass flags through to the wizard with `sh -s --`:
# curl -sSf <...>/install.sh | sh -s -- --yes --no-systemd
#
# Re-run any time to update — `cargo install` overwrites the existing binaries.
# Re-run any time to update — `cargo install` overwrites the existing binary.
set -eu