From c99f23c7053860e0c59df1e661d879ba976797a4 Mon Sep 17 00:00:00 2001 From: naps62-yolo Date: Thu, 28 May 2026 19:24:34 +0000 Subject: [PATCH] feat(maestro): scaffold maestro-tui crate + `maestro tui` subcommand (NAP-276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `crates/maestro-tui/` library behind a `tui` cargo feature on the umbrella binary (default-on). Ships a clap-derive `TuiArgs` and a `run()` stub that returns `NotImplemented` — exercises the full dispatch path so phases 2+ can fill in the catalog, session liveness, widgets, and event loop without touching scaffolding. Acceptance: - `cargo build -p maestro --features tui` succeeds - `maestro tui` prints "maestro: not implemented" and exits 1 - `cargo test -p maestro-tui` passes (2 clap-parse tests) - `cargo fmt --check` + `cargo clippy --all-targets --features tui -- -D warnings` clean Phase 1 of the Maestro TUI epic (NAP-275); plan in `docs/superpowers/plans/2026-05-28-maestro-tui.md`. --- Cargo.lock | 9 ++++++++ Cargo.toml | 1 + crates/maestro-tui/Cargo.toml | 12 +++++++++++ crates/maestro-tui/src/cli.rs | 40 +++++++++++++++++++++++++++++++++++ crates/maestro-tui/src/lib.rs | 32 ++++++++++++++++++++++++++++ crates/maestro/Cargo.toml | 4 +++- crates/maestro/src/main.rs | 20 ++++++++++++++++-- 7 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 crates/maestro-tui/Cargo.toml create mode 100644 crates/maestro-tui/src/cli.rs create mode 100644 crates/maestro-tui/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e059ae2..958559e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -947,6 +947,7 @@ dependencies = [ "maestro-cli", "maestro-daemon", "maestro-registry", + "maestro-tui", "tokio", "tracing-subscriber", ] @@ -1080,6 +1081,14 @@ dependencies = [ "uuid", ] +[[package]] +name = "maestro-tui" +version = "0.1.0" +dependencies = [ + "clap", + "thiserror 1.0.69", +] + [[package]] name = "maestro-web" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 178dd90..17b9b34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ jsonwebtoken = { version = "9", default-features = false } libc = "0.2" maestro-config = { path = "crates/maestro-config" } maestro-protocol = { path = "crates/maestro-protocol" } +maestro-tui = { path = "crates/maestro-tui" } nix = { version = "0.29", default-features = false, features = ["term", "signal"] } rand = "0.8" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "cookies", "stream"] } diff --git a/crates/maestro-tui/Cargo.toml b/crates/maestro-tui/Cargo.toml new file mode 100644 index 0000000..9bf8e12 --- /dev/null +++ b/crates/maestro-tui/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "maestro-tui" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true +description = "Terminal UI client over the local maestro daemon's `/api/*` surface." + +[dependencies] +clap.workspace = true +thiserror.workspace = true diff --git a/crates/maestro-tui/src/cli.rs b/crates/maestro-tui/src/cli.rs new file mode 100644 index 0000000..6737f47 --- /dev/null +++ b/crates/maestro-tui/src/cli.rs @@ -0,0 +1,40 @@ +//! Clap-derive subcommand definition for `maestro tui`. + +#[derive(Debug, clap::Args)] +pub struct TuiArgs { + /// Override the local daemon's HTTP base URL (default `http://127.0.0.1:7070`). + #[arg(long, env = "MAESTRO_DAEMON_URL")] + pub daemon_url: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + + #[derive(Debug, Parser)] + struct Harness { + #[command(subcommand)] + cmd: Cmd, + } + + #[derive(Debug, clap::Subcommand)] + enum Cmd { + Tui(TuiArgs), + } + + #[test] + fn parses_with_daemon_url() { + let h = Harness::try_parse_from(["maestro", "tui", "--daemon-url", "http://10.0.0.1:7070"]) + .unwrap(); + let Cmd::Tui(args) = h.cmd; + assert_eq!(args.daemon_url.as_deref(), Some("http://10.0.0.1:7070")); + } + + #[test] + fn parses_without_daemon_url() { + let h = Harness::try_parse_from(["maestro", "tui"]).unwrap(); + let Cmd::Tui(args) = h.cmd; + assert!(args.daemon_url.is_none()); + } +} diff --git a/crates/maestro-tui/src/lib.rs b/crates/maestro-tui/src/lib.rs new file mode 100644 index 0000000..4cb1e6d --- /dev/null +++ b/crates/maestro-tui/src/lib.rs @@ -0,0 +1,32 @@ +//! `maestro tui` — terminal UI client over the local daemon's `/api/*` surface. +//! +//! Design: `docs/superpowers/specs/2026-05-28-maestro-tui-design.md` +//! Implementation: `docs/superpowers/plans/2026-05-28-maestro-tui.md` +//! +//! Phase 1 (NAP-276) scaffolds the crate and a `Tui` subcommand stub. The real +//! event loop, rendering, and `/api/*` plumbing land in NAP-277 onward. + +#![deny(missing_debug_implementations)] + +pub mod cli; + +/// Inputs for [`run`]. Built by the umbrella binary from the parsed +/// `TuiArgs` plus any global flags. +#[derive(Debug, Clone)] +pub struct RunConfig { + /// HTTP base URL of the local daemon, e.g. `http://127.0.0.1:7070`. + /// No trailing slash. + pub daemon_url: String, +} + +#[derive(Debug, thiserror::Error)] +pub enum TuiError { + #[error("not implemented")] + NotImplemented, +} + +/// Entry point. Phase-1 stub — returns `NotImplemented` so the subcommand +/// dispatch path is exercisable end-to-end before the renderer lands. +pub async fn run(_cfg: RunConfig) -> Result<(), TuiError> { + Err(TuiError::NotImplemented) +} diff --git a/crates/maestro/Cargo.toml b/crates/maestro/Cargo.toml index c8dfac1..c941dfa 100644 --- a/crates/maestro/Cargo.toml +++ b/crates/maestro/Cargo.toml @@ -12,10 +12,11 @@ name = "maestro" path = "src/main.rs" [features] -default = ["cli", "daemon", "registry"] +default = ["cli", "daemon", "registry", "tui"] cli = ["dep:maestro-cli"] daemon = ["dep:maestro-daemon"] registry = ["dep:maestro-registry"] +tui = ["dep:maestro-tui"] [dependencies] clap.workspace = true @@ -24,3 +25,4 @@ 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 } +maestro-tui = { path = "../maestro-tui", optional = true } diff --git a/crates/maestro/src/main.rs b/crates/maestro/src/main.rs index 7a325b4..34174e6 100644 --- a/crates/maestro/src/main.rs +++ b/crates/maestro/src/main.rs @@ -7,8 +7,13 @@ //! 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"); +#[cfg(not(any( + feature = "cli", + feature = "daemon", + feature = "registry", + feature = "tui" +)))] +compile_error!("at least one of the `cli`, `daemon`, `registry`, `tui` features must be enabled"); use std::path::PathBuf; use std::process::ExitCode; @@ -73,6 +78,10 @@ enum Command { /// Always-on registry: presence index + session catalog. #[cfg(feature = "registry")] Registry, + + /// Open the terminal UI. + #[cfg(feature = "tui")] + Tui(maestro_tui::cli::TuiArgs), } fn main() -> ExitCode { @@ -153,6 +162,13 @@ async fn dispatch(cli: Cli) -> Result<(), Box> { Command::Daemon { cmd } => maestro_daemon::cli::dispatch(cli.config, cmd).await?, #[cfg(feature = "registry")] Command::Registry => maestro_registry::run(cli.config).await?, + #[cfg(feature = "tui")] + Command::Tui(args) => { + let daemon_url = args + .daemon_url + .unwrap_or_else(|| "http://127.0.0.1:7070".to_string()); + maestro_tui::run(maestro_tui::RunConfig { daemon_url }).await? + } } Ok(()) }