The browser reached the daemon through maestro-web, a second binary that served the SPA and reverse-proxied /api/* — two TCP connections and a WebSocket frame re-encode on every keystroke and every byte of PTY output. On one machine that hop buys nothing. The daemon now embeds the bundle and serves it off its own listener, behind a default-on `web` feature. `--no-default-features` builds without pnpm or node; MAESTRO_WEB_DIST still takes a prebuilt bundle. The router's fallback answers an unmatched /api/* with 404 rather than the SPA shell, so a client expecting JSON gets a status it can read instead of HTML. Removes maestro-web and the [web] config section. An existing [web] block is ignored, not rejected. The installer deletes a leftover maestro-web.service on upgrade. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
228 lines
6.8 KiB
Rust
228 lines
6.8 KiB
Rust
//! Builders for the external commands the wizard shells out to.
|
|
//!
|
|
//! Kept pure so unit tests can assert the exact argv we'd hand to
|
|
//! `std::process::Command` without ever exec'ing `cargo` or `systemctl`.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
/// Where to fetch the maestro crates from when running `cargo install`.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum CargoSource {
|
|
/// `cargo install --git <url> [--branch <branch>] <crate>`.
|
|
Git { url: String, branch: Option<String> },
|
|
/// `cargo install --path <workspace_root> -p <crate>`.
|
|
Path(PathBuf),
|
|
}
|
|
|
|
/// `systemctl --user` verbs the wizard runs.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum SystemctlAction {
|
|
DaemonReload,
|
|
Enable,
|
|
Disable,
|
|
Start,
|
|
Stop,
|
|
Restart,
|
|
}
|
|
|
|
impl SystemctlAction {
|
|
pub fn verb(self) -> &'static str {
|
|
match self {
|
|
Self::DaemonReload => "daemon-reload",
|
|
Self::Enable => "enable",
|
|
Self::Disable => "disable",
|
|
Self::Start => "start",
|
|
Self::Stop => "stop",
|
|
Self::Restart => "restart",
|
|
}
|
|
}
|
|
|
|
fn takes_unit_arg(self) -> bool {
|
|
!matches!(self, Self::DaemonReload)
|
|
}
|
|
}
|
|
|
|
/// Build the args for `cargo install <args...>` (excluding the `cargo` and
|
|
/// `install` words themselves). `locked` adds `--locked` so the install uses
|
|
/// the committed `Cargo.lock`.
|
|
/// Build the `cargo install` argument vector.
|
|
///
|
|
/// `features`, when `Some`, restricts the build to an explicit feature set via
|
|
/// `--no-default-features --features <list>`.
|
|
pub fn cargo_install_args(
|
|
source: &CargoSource,
|
|
crate_name: &str,
|
|
locked: bool,
|
|
features: Option<&str>,
|
|
) -> Vec<String> {
|
|
let mut args: Vec<String> = Vec::new();
|
|
match source {
|
|
CargoSource::Git { url, branch } => {
|
|
args.push("--git".into());
|
|
args.push(url.clone());
|
|
if let Some(b) = branch {
|
|
args.push("--branch".into());
|
|
args.push(b.clone());
|
|
}
|
|
args.push(crate_name.to_string());
|
|
}
|
|
CargoSource::Path(root) => {
|
|
args.push("--path".into());
|
|
args.push(root.display().to_string());
|
|
args.push("-p".into());
|
|
args.push(crate_name.to_string());
|
|
}
|
|
}
|
|
if let Some(list) = features {
|
|
args.push("--no-default-features".into());
|
|
args.push("--features".into());
|
|
args.push(list.to_string());
|
|
}
|
|
if locked {
|
|
args.push("--locked".into());
|
|
}
|
|
args
|
|
}
|
|
|
|
/// Build the args for `systemctl --user <verb> [unit]`.
|
|
pub fn systemctl_user_args(action: SystemctlAction, unit: &str) -> Vec<String> {
|
|
let mut args = vec!["--user".to_string(), action.verb().to_string()];
|
|
if action.takes_unit_arg() {
|
|
args.push(unit.to_string());
|
|
}
|
|
args
|
|
}
|
|
|
|
/// `cargo` binary path. We prefer `$CARGO` (set by `cargo install` itself when
|
|
/// it invokes child processes, and by `rustup`), then `~/.cargo/bin/cargo`, and
|
|
/// finally a bare `cargo` so PATH lookup applies.
|
|
pub fn cargo_bin() -> PathBuf {
|
|
if let Ok(p) = std::env::var("CARGO") {
|
|
return PathBuf::from(p);
|
|
}
|
|
if let Some(home) = dirs::home_dir() {
|
|
let in_cargo_home = home.join(".cargo").join("bin").join("cargo");
|
|
if in_cargo_home.exists() {
|
|
return in_cargo_home;
|
|
}
|
|
}
|
|
PathBuf::from("cargo")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn git_args_no_branch() {
|
|
let src = CargoSource::Git {
|
|
url: "https://git.naps.pt/naps62/maestro".into(),
|
|
branch: None,
|
|
};
|
|
let args = cargo_install_args(&src, "maestro", false, None);
|
|
assert_eq!(
|
|
args,
|
|
vec!["--git", "https://git.naps.pt/naps62/maestro", "maestro",]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn git_args_with_features_omit_defaults() {
|
|
let src = CargoSource::Git {
|
|
url: "https://git.naps.pt/naps62/maestro".into(),
|
|
branch: None,
|
|
};
|
|
let args = cargo_install_args(&src, "maestro", true, Some("cli,daemon,tui"));
|
|
assert_eq!(
|
|
args,
|
|
vec![
|
|
"--git",
|
|
"https://git.naps.pt/naps62/maestro",
|
|
"maestro",
|
|
"--no-default-features",
|
|
"--features",
|
|
"cli,daemon,tui",
|
|
"--locked",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn git_args_with_branch_and_locked() {
|
|
let src = CargoSource::Git {
|
|
url: "https://git.example/x".into(),
|
|
branch: Some("main".into()),
|
|
};
|
|
let args = cargo_install_args(&src, "maestro", true, None);
|
|
assert_eq!(
|
|
args,
|
|
vec![
|
|
"--git",
|
|
"https://git.example/x",
|
|
"--branch",
|
|
"main",
|
|
"maestro",
|
|
"--locked",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn path_args_use_p_flag() {
|
|
let src = CargoSource::Path(PathBuf::from("/repos/maestro"));
|
|
let args = cargo_install_args(&src, "maestro", true, None);
|
|
assert_eq!(
|
|
args,
|
|
vec!["--path", "/repos/maestro", "-p", "maestro", "--locked",]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn systemctl_daemon_reload_omits_unit() {
|
|
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, "maestro.service");
|
|
assert_eq!(args, vec!["--user", "enable", "maestro.service"]);
|
|
}
|
|
|
|
#[test]
|
|
fn systemctl_start_includes_unit() {
|
|
let args = systemctl_user_args(SystemctlAction::Start, "maestro.service");
|
|
assert_eq!(args, vec!["--user", "start", "maestro.service"]);
|
|
}
|
|
|
|
#[test]
|
|
fn systemctl_stop_and_disable_include_unit() {
|
|
assert_eq!(
|
|
systemctl_user_args(SystemctlAction::Stop, "maestro-web.service"),
|
|
vec!["--user", "stop", "maestro-web.service"]
|
|
);
|
|
assert_eq!(
|
|
systemctl_user_args(SystemctlAction::Disable, "maestro-web.service"),
|
|
vec!["--user", "disable", "maestro-web.service"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn systemctl_restart_includes_unit() {
|
|
let args = systemctl_user_args(SystemctlAction::Restart, "maestro.service");
|
|
assert_eq!(args, vec!["--user", "restart", "maestro.service"]);
|
|
}
|
|
|
|
#[test]
|
|
fn cargo_bin_honors_env_var() {
|
|
// Save & restore $CARGO to keep the test hermetic.
|
|
let prev = std::env::var("CARGO").ok();
|
|
std::env::set_var("CARGO", "/opt/rust/bin/cargo");
|
|
assert_eq!(cargo_bin(), PathBuf::from("/opt/rust/bin/cargo"));
|
|
match prev {
|
|
Some(v) => std::env::set_var("CARGO", v),
|
|
None => std::env::remove_var("CARGO"),
|
|
}
|
|
}
|
|
}
|