Implement cross-platform grid agent operator TUI (#128)
This commit is contained in:
@@ -22,11 +22,25 @@ impl fmt::Display for CliError {
|
||||
|
||||
impl Error for CliError {}
|
||||
|
||||
fn color_disabled() -> bool {
|
||||
std::env::var_os("NO_COLOR").is_some()
|
||||
|| std::env::var_os("TERM").is_some_and(|value| value == "dumb")
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Eq, PartialEq)]
|
||||
enum Operation {
|
||||
#[default]
|
||||
Serve,
|
||||
CheckConfig,
|
||||
RunOnce,
|
||||
Tui,
|
||||
TuiClient,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Options {
|
||||
config: Option<PathBuf>,
|
||||
check_config: bool,
|
||||
run_once: bool,
|
||||
operation: Operation,
|
||||
}
|
||||
|
||||
fn options() -> Result<Option<Options>, CliError> {
|
||||
@@ -42,15 +56,19 @@ fn options() -> Result<Option<Options>, CliError> {
|
||||
}
|
||||
if argument == "--help" || argument == "-h" {
|
||||
println!(
|
||||
"metacrate-grid-agent [--config PATH] [--check-config | --run-once]\n\
|
||||
"metacrate-grid-agent [--config PATH] [--check-config | --run-once | --tui | --tui-client]\n\
|
||||
Configuration precedence: defaults < JSON < secret files < environment."
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
if argument == "--check-config" {
|
||||
result.check_config = true;
|
||||
set_operation(&mut result, Operation::CheckConfig)?;
|
||||
} else if argument == "--run-once" {
|
||||
result.run_once = true;
|
||||
set_operation(&mut result, Operation::RunOnce)?;
|
||||
} else if argument == "--tui" {
|
||||
set_operation(&mut result, Operation::Tui)?;
|
||||
} else if argument == "--tui-client" {
|
||||
set_operation(&mut result, Operation::TuiClient)?;
|
||||
} else if argument == "--config" {
|
||||
let path = arguments
|
||||
.next()
|
||||
@@ -65,14 +83,17 @@ fn options() -> Result<Option<Options>, CliError> {
|
||||
)));
|
||||
}
|
||||
}
|
||||
if result.check_config && result.run_once {
|
||||
return Err(CliError(
|
||||
"--check-config and --run-once are mutually exclusive".into(),
|
||||
));
|
||||
}
|
||||
Ok(Some(result))
|
||||
}
|
||||
|
||||
fn set_operation(options: &mut Options, operation: Operation) -> Result<(), CliError> {
|
||||
if options.operation != Operation::Serve {
|
||||
return Err(CliError("operation flags are mutually exclusive".into()));
|
||||
}
|
||||
options.operation = operation;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let Some(options) = options()? else {
|
||||
@@ -83,13 +104,33 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
loader = loader.with_file(path);
|
||||
}
|
||||
let config = loader.load()?;
|
||||
if options.check_config {
|
||||
if options.operation == Operation::CheckConfig {
|
||||
println!("configuration is valid for {:?} mode", config.mode);
|
||||
return Ok(());
|
||||
}
|
||||
if options.operation == Operation::TuiClient {
|
||||
let token = config
|
||||
.control
|
||||
.operator_token
|
||||
.as_ref()
|
||||
.ok_or_else(|| CliError("split TUI requires a configured operator token".into()))?;
|
||||
let client = metacrate_grid_agent::ReconnectingTcpTransport::new(
|
||||
config.control.listen,
|
||||
token.clone(),
|
||||
config.control.limits,
|
||||
);
|
||||
metacrate_grid_agent::tui::run_terminal(std::sync::Arc::new(client), !color_disabled())
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
if config.mode != OperatingMode::OfflineFake {
|
||||
#[cfg(feature = "live-grid")]
|
||||
return run_live(config, options.run_once).await;
|
||||
return run_live(
|
||||
config,
|
||||
options.operation == Operation::RunOnce,
|
||||
options.operation == Operation::Tui,
|
||||
)
|
||||
.await;
|
||||
#[cfg(not(feature = "live-grid"))]
|
||||
return Err(CliError(
|
||||
"live grid mode requires rebuilding with --features live-grid".into(),
|
||||
@@ -97,9 +138,12 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
.into());
|
||||
}
|
||||
|
||||
if options.operation == Operation::Tui {
|
||||
return Err(CliError("embedded TUI requires live-grid mode".into()).into());
|
||||
}
|
||||
let startup_timeout = config.timeouts.startup;
|
||||
let mut handle = AgentService::offline(config)?.start()?;
|
||||
if options.run_once {
|
||||
if options.operation == Operation::RunOnce {
|
||||
tokio::time::timeout(startup_timeout, async {
|
||||
loop {
|
||||
match handle.next_event().await {
|
||||
@@ -145,6 +189,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
async fn run_live(
|
||||
config: metacrate_grid_agent::AgentConfig,
|
||||
run_once: bool,
|
||||
tui: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
use metacrate_grid_agent::{
|
||||
AgentControlTarget, BehaviorObservation, ControlEventKind, ControlPlane, ControlTarget,
|
||||
@@ -248,7 +293,7 @@ async fn run_live(
|
||||
control_target.attach_observability(live.observability.clone());
|
||||
control_target.update_session(handle.status());
|
||||
let erased_target: Arc<dyn ControlTarget> = control_target.clone();
|
||||
let (control_plane, _integrated_client, control_server) = match config.mode {
|
||||
let (control_plane, integrated_client, control_server) = match config.mode {
|
||||
OperatingMode::Integrated => {
|
||||
let (plane, client) = ControlPlane::integrated(erased_target, config.control.limits)?;
|
||||
(plane, Some(client), None)
|
||||
@@ -286,6 +331,18 @@ async fn run_live(
|
||||
}
|
||||
};
|
||||
|
||||
let _tui_task = if tui {
|
||||
let client = integrated_client.ok_or_else(|| {
|
||||
CliError("--tui requires integrated mode; use --tui-client for split mode".into())
|
||||
})?;
|
||||
Some(tokio::spawn(metacrate_grid_agent::tui::run_terminal(
|
||||
Arc::new(client),
|
||||
!color_disabled(),
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
println!("grid agent session supervisor started; press Ctrl-C to stop");
|
||||
let mut signal_error = None;
|
||||
let mut control_failure = None;
|
||||
|
||||
Reference in New Issue
Block a user