feat(maestro): unify maestro/maestrod/maestro-registry into single binary (NAP-245)

New `crates/maestro/` umbrella binary hosts the CLI, daemon, and registry
behind Cargo features `cli`, `daemon`, `registry` (default = all three).
Library crates keep their structure; per-binary `main.rs` files are removed
and their dispatch logic is exposed as public functions:

- `maestro_cli::dispatch(command, config, registry_url)`
- `maestro_daemon::cli::dispatch(config, cmd)` (with public `DaemonCmd`/`ConfigureArgs`)
- `maestro_registry::run()`

Subcommand surface:

- `maestro list / spawn / attach / kill / set-registry / policy` (unchanged)
- `maestro daemon [run|configure]` (was `maestrod`)
- `maestro registry` (was `maestro-registry`)

Hard cutover — old binary names are not preserved.

Verified: default-feature build, plus `--no-default-features --features {cli|daemon|registry}`
each build clean with `-D warnings`.
This commit is contained in:
2026-05-27 10:35:41 +00:00
parent 8e857e9fe7
commit 35482118b1
13 changed files with 442 additions and 233 deletions
-4
View File
@@ -10,10 +10,6 @@ description = "`maestro` CLI: list, spawn, attach, kill sessions across daemons"
[lib]
path = "src/lib.rs"
[[bin]]
name = "maestro"
path = "src/main.rs"
[dependencies]
chrono.workspace = true
clap.workspace = true
+140
View File
@@ -0,0 +1,140 @@
//! Public dispatch for the `maestro` CLI subcommands.
//!
//! Extracted from the per-binary `main.rs` (NAP-245) so the umbrella `maestro`
//! binary can dispatch to the same logic.
use std::path::{Path, PathBuf};
use crate::attach::{parse_detach_key, run_attach};
use crate::cli::{AttachArgs, Command, KillArgs, ListArgs, PolicyCmd, SetRegistryArgs, SpawnArgs};
use crate::config::{default_config_path, CliConfig, ConfigError};
use crate::http::RegistryClient;
use crate::policy::{run_reload as run_policy_reload, run_show as run_policy_show};
use crate::sessions::{fetch_connect_info, run_kill, run_list, run_spawn};
#[derive(Debug, thiserror::Error)]
#[allow(clippy::result_large_err)]
pub enum CliError {
#[error("config: {0}")]
Config(#[from] ConfigError),
#[error("{0}")]
Http(#[from] crate::http::HttpError),
#[error("{0}")]
Attach(#[from] crate::attach::AttachError),
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Policy(#[from] crate::policy::PolicyError),
}
/// Dispatch a parsed top-level subcommand. The umbrella binary passes the
/// global flags (`--config`, `--registry-url`) in alongside the variant.
#[allow(clippy::result_large_err)]
pub async fn dispatch(
command: Command,
config: Option<PathBuf>,
registry_url_override: Option<String>,
) -> Result<(), CliError> {
let config_path = match config {
Some(p) => p,
None => default_config_path()?,
};
match command {
Command::SetRegistry(args) => set_registry(&config_path, args),
Command::Policy(sub) => policy(&config_path, sub).await,
Command::List(args) => list(&config_path, registry_url_override, args).await,
Command::Spawn(args) => spawn(&config_path, registry_url_override, args).await,
Command::Attach(args) => attach(&config_path, registry_url_override, args).await,
Command::Kill(args) => kill(&config_path, registry_url_override, args).await,
}
}
#[allow(clippy::result_large_err)]
fn set_registry(config_path: &Path, args: SetRegistryArgs) -> Result<(), CliError> {
let url = args.url.trim_end_matches('/');
CliConfig::save_registry_url(config_path, url)?;
println!("registry URL saved to {}", config_path.display());
Ok(())
}
#[allow(clippy::result_large_err)]
async fn policy(config_path: &Path, sub: PolicyCmd) -> Result<(), CliError> {
let mut stdout = std::io::stdout().lock();
match sub {
PolicyCmd::Reload => run_policy_reload(config_path, &mut stdout).await?,
PolicyCmd::Show { json } => run_policy_show(config_path, json, &mut stdout).await?,
}
Ok(())
}
#[allow(clippy::result_large_err)]
async fn list(
config_path: &Path,
registry_url_override: Option<String>,
args: ListArgs,
) -> Result<(), CliError> {
let client = build_client(registry_url_override, config_path)?;
let mut stdout = std::io::stdout().lock();
run_list(&client, &args, &mut stdout).await?;
Ok(())
}
#[allow(clippy::result_large_err)]
async fn spawn(
config_path: &Path,
registry_url_override: Option<String>,
args: SpawnArgs,
) -> Result<(), CliError> {
let client = build_client(registry_url_override, config_path)?;
let attach_after = args.attach;
let session = run_spawn(&client, args).await?;
println!("spawned {}", session.id);
if attach_after {
let info = fetch_connect_info(&client, session.id).await?;
let detach = 0x11; // Ctrl-q
run_attach(info, session.id, detach).await?;
}
Ok(())
}
#[allow(clippy::result_large_err)]
async fn attach(
config_path: &Path,
registry_url_override: Option<String>,
args: AttachArgs,
) -> Result<(), CliError> {
let client = build_client(registry_url_override, config_path)?;
let info = fetch_connect_info(&client, args.session_id).await?;
let detach = parse_detach_key(&args.detach_key)?;
run_attach(info, args.session_id, detach).await?;
Ok(())
}
#[allow(clippy::result_large_err)]
async fn kill(
config_path: &Path,
registry_url_override: Option<String>,
args: KillArgs,
) -> Result<(), CliError> {
let client = build_client(registry_url_override, config_path)?;
run_kill(&client, &args).await?;
println!("killed {}", args.session_id);
Ok(())
}
#[allow(clippy::result_large_err)]
fn build_client(
override_url: Option<String>,
config_path: &Path,
) -> Result<RegistryClient, CliError> {
let cfg = CliConfig::load(config_path)?;
let url = override_url
.or(cfg.registry_url.clone())
.ok_or(ConfigError::NoRegistryUrl)?;
Ok(RegistryClient::new(
url,
cfg.cookie,
config_path.to_path_buf(),
))
}
+3
View File
@@ -8,6 +8,9 @@
pub mod attach;
pub mod cli;
pub mod config;
pub mod dispatch;
pub mod http;
pub mod policy;
pub mod sessions;
pub use dispatch::{dispatch, CliError};
-132
View File
@@ -1,132 +0,0 @@
//! `maestro` — CLI binary entry point.
use std::path::Path;
use std::process::ExitCode;
use clap::Parser;
use maestro_cli::attach::{parse_detach_key, run_attach};
use maestro_cli::cli::{Cli, Command, PolicyCmd};
use maestro_cli::config::{default_config_path, CliConfig, ConfigError};
use maestro_cli::http::RegistryClient;
use maestro_cli::policy::{run_reload as run_policy_reload, run_show as run_policy_show};
use maestro_cli::sessions::{fetch_connect_info, run_kill, run_list, run_spawn};
use tracing_subscriber::EnvFilter;
#[tokio::main(flavor = "current_thread")]
async fn main() -> ExitCode {
let cli = Cli::parse();
init_tracing(cli.verbose);
match dispatch(cli).await {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("maestro: {e}");
ExitCode::FAILURE
}
}
}
fn init_tracing(verbose: u8) {
let filter = match (std::env::var_os("RUST_LOG"), verbose) {
(Some(_), _) => {
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn"))
}
(None, 0) => EnvFilter::new("warn"),
(None, 1) => EnvFilter::new("info"),
(None, 2) => EnvFilter::new("debug"),
(None, _) => EnvFilter::new("trace"),
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
}
#[derive(Debug, thiserror::Error)]
#[allow(clippy::result_large_err)]
enum CliError {
#[error("config: {0}")]
Config(#[from] ConfigError),
#[error("{0}")]
Http(#[from] maestro_cli::http::HttpError),
#[error("{0}")]
Attach(#[from] maestro_cli::attach::AttachError),
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Policy(#[from] maestro_cli::policy::PolicyError),
}
#[allow(clippy::result_large_err)]
async fn dispatch(cli: Cli) -> Result<(), CliError> {
let config_path = match cli.config.clone() {
Some(p) => p,
None => default_config_path()?,
};
// `set-registry` is the only command that writes config first and skips
// the registry-required path below.
if let Command::SetRegistry(args) = &cli.command {
let url = args.url.trim_end_matches('/');
CliConfig::save_registry_url(&config_path, url)?;
println!("registry URL saved to {}", config_path.display());
return Ok(());
}
// `policy` talks to the local daemon's HTTP API, not the registry. Handle
// it here so it works even without a registry URL configured.
if let Command::Policy(sub) = &cli.command {
let mut stdout = std::io::stdout().lock();
match sub {
PolicyCmd::Reload => run_policy_reload(&config_path, &mut stdout).await?,
PolicyCmd::Show { json } => run_policy_show(&config_path, *json, &mut stdout).await?,
}
return Ok(());
}
let client = build_client(cli.registry_url.clone(), &config_path)?;
match cli.command {
Command::List(args) => {
let mut stdout = std::io::stdout().lock();
run_list(&client, &args, &mut stdout).await?;
}
Command::Spawn(args) => {
let attach_after = args.attach;
let session = run_spawn(&client, args).await?;
println!("spawned {}", session.id);
if attach_after {
let info = fetch_connect_info(&client, session.id).await?;
let detach = 0x11; // Ctrl-q
run_attach(info, session.id, detach).await?;
}
}
Command::Attach(args) => {
let info = fetch_connect_info(&client, args.session_id).await?;
let detach = parse_detach_key(&args.detach_key)?;
run_attach(info, args.session_id, detach).await?;
}
Command::Kill(args) => {
run_kill(&client, &args).await?;
println!("killed {}", args.session_id);
}
Command::SetRegistry(_) | Command::Policy(_) => unreachable!("handled above"),
}
Ok(())
}
#[allow(clippy::result_large_err)]
fn build_client(
override_url: Option<String>,
config_path: &Path,
) -> Result<RegistryClient, CliError> {
let cfg = CliConfig::load(config_path)?;
let url = override_url
.or(cfg.registry_url.clone())
.ok_or(ConfigError::NoRegistryUrl)?;
Ok(RegistryClient::new(
url,
cfg.cookie,
config_path.to_path_buf(),
))
}
-4
View File
@@ -10,10 +10,6 @@ description = "Per-host tmux wrapper + session orchestrator that talks to the re
[lib]
path = "src/lib.rs"
[[bin]]
name = "maestrod"
path = "src/main.rs"
[dependencies]
axum.workspace = true
chrono.workspace = true
@@ -1,102 +1,77 @@
//! `maestrod` — the Maestro per-host daemon binary.
//! Clap-derive subcommand for the daemon, plus a runner that wires together
//! everything the binary used to do in `main.rs`.
//!
//! Two subcommands:
//! - `maestrod` / `maestrod run` — start the daemon.
//! - `maestrod configure` — bootstrap the config file.
//! Lifted out of the per-binary `main.rs` (NAP-245) so the umbrella `maestro`
//! binary can compose this subcommand alongside the CLI and registry.
use std::error::Error;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use maestro_daemon::session::DEFAULT_LIVENESS_INTERVAL;
use maestro_daemon::sidecar::SidecarStore;
use maestro_daemon::tmux::Tmux;
use maestro_daemon::worktree::WorktreeManager;
use maestro_daemon::{
use clap::Subcommand;
use tokio::net::TcpListener;
use crate::session::DEFAULT_LIVENESS_INTERVAL;
use crate::sidecar::SidecarStore;
use crate::tmux::Tmux;
use crate::worktree::WorktreeManager;
use crate::{
attach, default_config_path, default_state_dir, load_config, local_api, policy, run_configure,
spawn_session_publisher, spawn_sighup_listener, ConfigureInputs, EnvOverrides, JwtVerifier,
LocalEventBus, PairAttachVerifier, PolicyState, RegistryClient, SessionManager,
};
use tokio::net::TcpListener;
use tracing_subscriber::EnvFilter;
#[derive(Debug, Parser)]
#[command(
name = "maestrod",
version,
about = "Per-host Maestro daemon: tmux wrapper + registry client"
)]
struct Cli {
/// Override the unified Maestro config-file path (default `~/.config/maestro/config.toml`).
#[arg(long, global = true, env = "MAESTRO_CONFIG")]
config: Option<PathBuf>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Debug, Subcommand)]
enum Command {
pub enum DaemonCmd {
/// Start the daemon. Default when no subcommand is given.
Run,
/// Write `~/.config/maestro/config.toml` interactively or from flags.
Configure(ConfigureCmd),
Configure(ConfigureArgs),
}
#[derive(Debug, clap::Args)]
struct ConfigureCmd {
pub struct ConfigureArgs {
/// Registry base URL (e.g. `https://maestro.example.com`). The daemon
/// derives its WS endpoint by scheme-swapping and appending `/daemon`.
#[arg(long)]
registry_url: Option<String>,
pub registry_url: Option<String>,
/// Reachable URL clients dial for this daemon's attach WS.
#[arg(long)]
daemon_url: Option<String>,
pub daemon_url: Option<String>,
/// Shared token (MAESTRO_TOKEN). Tip: pipe from `pass` / `op` / `secret-tool`.
#[arg(long)]
token: Option<String>,
pub token: Option<String>,
/// Stable daemon ID. Defaults to the system hostname.
#[arg(long)]
daemon_id: Option<String>,
pub daemon_id: Option<String>,
/// Local bind address for the per-session WS endpoint.
#[arg(long)]
bind_addr: Option<String>,
pub bind_addr: Option<String>,
/// Override the daemon's state directory.
#[arg(long)]
state_dir: Option<PathBuf>,
pub state_dir: Option<PathBuf>,
/// Override the worktree root.
#[arg(long)]
worktree_root: Option<PathBuf>,
pub worktree_root: Option<PathBuf>,
/// Override the recorded hostname (defaults to `gethostname(2)`).
#[arg(long)]
hostname: Option<String>,
pub hostname: Option<String>,
/// Overwrite an existing config file.
#[arg(long)]
force: bool,
pub force: bool,
/// Disable interactive prompts (errors if any required field is missing).
#[arg(long)]
non_interactive: bool,
pub non_interactive: bool,
}
#[tokio::main]
async fn main() -> ExitCode {
let cli = Cli::parse();
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::fmt().with_env_filter(filter).init();
let result: Result<(), Box<dyn Error>> = match cli.command.unwrap_or(Command::Run) {
Command::Run => run(cli.config).await,
Command::Configure(args) => configure(cli.config, args),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
tracing::error!(error = %e, "maestrod failed");
eprintln!("maestrod: {e}");
ExitCode::FAILURE
}
/// Dispatch a parsed daemon subcommand. `config_override` is the unified
/// `--config` flag from the top-level CLI.
pub async fn dispatch(
config_override: Option<PathBuf>,
cmd: Option<DaemonCmd>,
) -> Result<(), Box<dyn Error>> {
match cmd.unwrap_or(DaemonCmd::Run) {
DaemonCmd::Run => run(config_override).await,
DaemonCmd::Configure(args) => configure(config_override, args),
}
}
@@ -113,7 +88,7 @@ async fn run(config_override: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
"loaded config"
);
let _ = default_state_dir(); // surface XDG diagnostics early but don't override
let _ = default_state_dir();
std::fs::create_dir_all(config.sidecar_dir())?;
std::fs::create_dir_all(config.fifo_dir())?;
std::fs::create_dir_all(&config.worktree_root)?;
@@ -144,14 +119,9 @@ async fn run(config_override: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
config.allow_inbound.iter().cloned(),
);
// Daemon-local event bus (NAP-248): own session events + peer catalog
// deltas funnel through here for `WS /api/events` subscribers.
let events = LocalEventBus::new();
spawn_session_publisher(manager.clone(), events.clone());
// Build the registry client first so we can hand its catalog mirror to
// the local-API state — both halves share the same Arc<Mutex<…>>. Hand
// the event bus to the catalog mirror so peer deltas fan out too.
let client = RegistryClient::new_with_bus(
config.registry_url.clone(),
config.daemon_id.clone(),
@@ -178,8 +148,6 @@ async fn run(config_override: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
self_daemon_id: std::sync::Arc::new(config.daemon_id.clone()),
self_perimeter_id: config.perimeter_id.clone().map(std::sync::Arc::new),
};
// NAP-237: hot-reload `[daemon.policy].allow_inbound` via SIGHUP or
// POST /api/policy/reload. Both swap the same verifier instance.
let policy_state = PolicyState {
verifier: pair_verifier.clone(),
config_path: std::sync::Arc::new(config_path.clone()),
@@ -208,7 +176,7 @@ async fn run(config_override: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
Ok(())
}
fn configure(config_override: Option<PathBuf>, args: ConfigureCmd) -> Result<(), Box<dyn Error>> {
fn configure(config_override: Option<PathBuf>, args: ConfigureArgs) -> Result<(), Box<dyn Error>> {
let path: PathBuf = match config_override {
Some(p) => p,
None => default_config_path()?,
+1
View File
@@ -9,6 +9,7 @@
pub mod attach;
pub mod broker;
pub mod catalog_mirror;
pub mod cli;
pub mod config;
pub mod configure;
pub mod jwt;
-4
View File
@@ -10,10 +10,6 @@ description = "Always-on registry — daemon presence index and session catalog"
[lib]
path = "src/lib.rs"
[[bin]]
name = "maestro-registry"
path = "src/main.rs"
[dependencies]
axum.workspace = true
chrono.workspace = true
+3
View File
@@ -17,11 +17,14 @@ pub mod client_api;
pub mod events;
pub mod handler;
pub mod jwt;
pub mod run;
pub mod server;
pub mod state;
pub mod token;
pub mod web_assets;
pub use run::run;
pub use auth::{issue_cookie, verify_cookie, COOKIE_NAME};
pub use jwt::{issue_attach_jwt, ATTACH_JWT_TTL};
pub use server::{router, router_full, router_with_timeout, RegistryServer};
@@ -1,34 +1,25 @@
//! `maestro-registry` — the Maestro always-on registry binary.
//! Public entry point: load config, bind, serve until SIGINT.
//!
//! Extracted from the per-binary `main.rs` (NAP-245) so the umbrella `maestro`
//! binary can dispatch `maestro registry` here.
use std::error::Error;
use std::path::PathBuf;
use std::process::ExitCode;
use maestro_registry::{default_config_path, load_config, router, Registry};
use tokio::net::TcpListener;
use tracing_subscriber::EnvFilter;
#[tokio::main]
async fn main() -> ExitCode {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::fmt().with_env_filter(filter).init();
use crate::{
default_config_path, load_config, router, Registry, CONFIG_PATH_ENV, ENV_REGISTRY_BIND_ADDR,
ENV_TOKEN,
};
match run().await {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
tracing::error!(error = %e, "maestro-registry failed");
ExitCode::FAILURE
}
}
}
async fn run() -> Result<(), Box<dyn Error>> {
let config_path: PathBuf = match std::env::var_os(maestro_registry::CONFIG_PATH_ENV) {
pub async fn run() -> Result<(), Box<dyn Error>> {
let config_path: PathBuf = match std::env::var_os(CONFIG_PATH_ENV) {
Some(p) => PathBuf::from(p),
None => default_config_path()?,
};
let env_token = std::env::var(maestro_registry::ENV_TOKEN).ok();
let env_bind = std::env::var(maestro_registry::ENV_REGISTRY_BIND_ADDR).ok();
let env_token = std::env::var(ENV_TOKEN).ok();
let env_bind = std::env::var(ENV_REGISTRY_BIND_ADDR).ok();
let token_from_env = env_token.is_some();
let bind_from_env = env_bind.is_some();
let config = load_config(&config_path, env_token, env_bind.as_deref())?;
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "maestro"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
description = "Unified `maestro` binary: CLI + per-host daemon + always-on registry, feature-gated."
[[bin]]
name = "maestro"
path = "src/main.rs"
[features]
default = ["cli", "daemon", "registry"]
cli = ["dep:maestro-cli"]
daemon = ["dep:maestro-daemon"]
registry = ["dep:maestro-registry"]
[dependencies]
clap.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] }
tracing-subscriber.workspace = true
maestro-cli = { path = "../maestro-cli", optional = true }
maestro-daemon = { path = "../maestro-daemon", optional = true }
maestro-registry = { path = "../maestro-registry", optional = true }
+209
View File
@@ -0,0 +1,209 @@
//! `maestro` — unified binary that hosts the CLI, the per-host daemon, and the
//! always-on registry behind feature-gated subcommands (NAP-245).
//!
//! ```text
//! cargo install --no-default-features --features registry maestro # registry server
//! cargo install --no-default-features --features daemon maestro # daemon-only host
//! cargo install maestro # default: cli + daemon + registry
//! ```
#[cfg(not(any(feature = "cli", feature = "daemon", feature = "registry")))]
compile_error!("at least one of the `cli`, `daemon`, `registry` features must be enabled");
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use tracing_subscriber::EnvFilter;
#[derive(Debug, Parser)]
#[command(
name = "maestro",
version,
about = "Unified CLI / daemon / registry for the Maestro terminal-session orchestrator",
arg_required_else_help = true
)]
struct Cli {
/// Override the registry URL for this invocation (e.g. `https://maestro.example.com`).
/// Used by CLI subcommands; ignored by `daemon` / `registry`.
#[arg(long, global = true, env = "MAESTRO_REGISTRY_URL")]
registry_url: Option<String>,
/// Path to the unified Maestro config file. Default `~/.config/maestro/config.toml`.
#[arg(long, global = true, env = "MAESTRO_CONFIG")]
config: Option<PathBuf>,
/// Increase log verbosity (CLI subcommands only). Repeat for more: `-v` = debug, `-vv` = trace.
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
verbose: u8,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
/// List all sessions, grouped by daemon.
#[cfg(feature = "cli")]
List(maestro_cli::cli::ListArgs),
/// Spawn a new session on a given daemon.
#[cfg(feature = "cli")]
Spawn(maestro_cli::cli::SpawnArgs),
/// Attach to a session over WebSocket. Bytes flow through the local TTY.
#[cfg(feature = "cli")]
Attach(maestro_cli::cli::AttachArgs),
/// Kill a session by ID.
#[cfg(feature = "cli")]
Kill(maestro_cli::cli::KillArgs),
/// Set the default registry URL persisted in the CLI config.
#[cfg(feature = "cli")]
SetRegistry(maestro_cli::cli::SetRegistryArgs),
/// Inspect or hot-reload the local daemon's `[daemon.policy]` (NAP-237).
#[cfg(feature = "cli")]
#[command(subcommand)]
Policy(maestro_cli::cli::PolicyCmd),
/// Per-host daemon: tmux wrapper + registry client.
#[cfg(feature = "daemon")]
Daemon {
#[command(subcommand)]
cmd: Option<maestro_daemon::cli::DaemonCmd>,
},
/// Always-on registry: presence index + session catalog.
#[cfg(feature = "registry")]
Registry,
}
fn main() -> ExitCode {
let cli = Cli::parse();
init_tracing(&cli);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("build tokio runtime");
match rt.block_on(dispatch(cli)) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("maestro: {e}");
ExitCode::FAILURE
}
}
}
async fn dispatch(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
match cli.command {
#[cfg(feature = "cli")]
Command::List(args) => {
cli_dispatch(
maestro_cli::cli::Command::List(args),
cli.config,
cli.registry_url,
)
.await?
}
#[cfg(feature = "cli")]
Command::Spawn(args) => {
cli_dispatch(
maestro_cli::cli::Command::Spawn(args),
cli.config,
cli.registry_url,
)
.await?
}
#[cfg(feature = "cli")]
Command::Attach(args) => {
cli_dispatch(
maestro_cli::cli::Command::Attach(args),
cli.config,
cli.registry_url,
)
.await?
}
#[cfg(feature = "cli")]
Command::Kill(args) => {
cli_dispatch(
maestro_cli::cli::Command::Kill(args),
cli.config,
cli.registry_url,
)
.await?
}
#[cfg(feature = "cli")]
Command::SetRegistry(args) => {
cli_dispatch(
maestro_cli::cli::Command::SetRegistry(args),
cli.config,
cli.registry_url,
)
.await?
}
#[cfg(feature = "cli")]
Command::Policy(sub) => {
cli_dispatch(
maestro_cli::cli::Command::Policy(sub),
cli.config,
cli.registry_url,
)
.await?
}
#[cfg(feature = "daemon")]
Command::Daemon { cmd } => maestro_daemon::cli::dispatch(cli.config, cmd).await?,
#[cfg(feature = "registry")]
Command::Registry => maestro_registry::run().await?,
}
Ok(())
}
#[cfg(feature = "cli")]
async fn cli_dispatch(
command: maestro_cli::cli::Command,
config: Option<PathBuf>,
registry_url: Option<String>,
) -> Result<(), maestro_cli::CliError> {
maestro_cli::dispatch(command, config, registry_url).await
}
fn init_tracing(cli: &Cli) {
let filter = if is_cli_subcommand(&cli.command) {
cli_filter(cli.verbose)
} else {
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
}
#[cfg(feature = "cli")]
fn is_cli_subcommand(command: &Command) -> bool {
matches!(
command,
Command::List(_)
| Command::Spawn(_)
| Command::Attach(_)
| Command::Kill(_)
| Command::SetRegistry(_)
| Command::Policy(_)
)
}
#[cfg(not(feature = "cli"))]
fn is_cli_subcommand(_command: &Command) -> bool {
false
}
fn cli_filter(verbose: u8) -> EnvFilter {
match (std::env::var_os("RUST_LOG"), verbose) {
(Some(_), _) => {
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn"))
}
(None, 0) => EnvFilter::new("warn"),
(None, 1) => EnvFilter::new("info"),
(None, 2) => EnvFilter::new("debug"),
(None, _) => EnvFilter::new("trace"),
}
}