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