ci / rust (push) Successful in 1m7s
Closes NAP-216. Thin client for the registry HTTP API + per-session daemon WebSocket. Becomes the headless test harness for NAP-217 and unblocks the demo-by-script path before the web client lands (NAP-218). Crate layout (`crates/maestro-cli/`): - `cli.rs` — clap-derive parser. Commands: `list`, `spawn`, `attach`, `kill`, `set-registry`. Global `--registry-url` (env `MAESTRO_REGISTRY_URL`), `--config`, repeatable `-v`. - `config.rs` — `CliConfig` persisted at `~/.config/maestro/cli.toml`, storing the default registry URL + the session cookie issued by the registry's cookie middleware. - `http.rs` — `RegistryClient` wrapping `reqwest`. Attaches the persisted cookie, captures `Set-Cookie` responses (first-contact bootstrap and rotation), atomically writes the new value back to the config file. - `sessions.rs` — `list/spawn/kill` + connect-info fetch. `list` prints a per-daemon grouped table; `--json` for scripting. - `attach.rs` — raw-TTY WS proxy. Termios cfmakeraw with an RAII guard for restore-on-drop, SIGWINCH → `ClientControl::Resize` text frame, configurable detach key (default Ctrl-q; accepts `^X` form), echoes `Ping`/`Pong` for keepalive symmetry with the daemon side. Workspace deps added: clap (derive+env), reqwest (rustls+cookies), nix (term+signal). Smoke-tested against the live registry at maestro-registry.n62.casa: `maestro list` issued + persisted the cookie and printed the empty catalog cleanly. 19 new unit tests (parser, config round-trip, table-printing, cookie extraction, attach URL/detach-key parsing). Full workspace `cargo test`: 97 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
3.7 KiB
Rust
120 lines
3.7 KiB
Rust
//! `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};
|
|
use maestro_cli::config::{default_config_path, CliConfig, ConfigError};
|
|
use maestro_cli::http::RegistryClient;
|
|
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),
|
|
}
|
|
|
|
#[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 mut cfg = CliConfig::load(&config_path)?;
|
|
cfg.registry_url = Some(args.url.trim_end_matches('/').to_string());
|
|
cfg.save(&config_path)?;
|
|
println!("registry URL saved to {}", config_path.display());
|
|
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(_) => 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(),
|
|
))
|
|
}
|