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:
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user