mod app; mod input; mod markdown; mod modal; mod render; mod state; mod terminal; use std::path::PathBuf; use harness_core::event::RunOutcome; const DEFAULT_MODEL: &str = "anthropic/claude-sonnet-4-5"; fn print_usage() { eprintln!("usage: harness [run -p \"\" [-m provider/model]] | [tui]"); } fn parse_run_args(args: &[String]) -> Option<(String, Option)> { let mut prompt = None; let mut model = None; let mut i = 0; while i < args.len() { match args[i].as_str() { "-p" | "--prompt" => { prompt = args.get(i + 1).cloned(); i += 2; } "-m" | "--model" => { model = args.get(i + 1).cloned(); i += 2; } _ => i += 1, } } prompt.map(|p| (p, model)) } async fn run_headless(args: &[String]) -> i32 { let Some((prompt, model_arg)) = parse_run_args(args) else { print_usage(); return 2; }; let cwd = match std::env::current_dir() { Ok(cwd) => cwd, Err(e) => { eprintln!("error: could not read cwd: {e}"); return 1; } }; let app = match harness_app::App::init(cwd) { Ok(app) => app, Err(e) => { eprintln!("error: {e}"); return 1; } }; let model_ref = model_arg .or_else(|| app.config().model.clone()) .unwrap_or_else(|| DEFAULT_MODEL.to_string()); match app.run_prompt(prompt, &model_ref).await { Ok((RunOutcome::Stopped, session_id)) => { let text = app.final_text(&session_id).await.unwrap_or_default(); println!("{text}"); 0 } Ok((RunOutcome::Aborted, _)) => { eprintln!("run aborted"); 1 } Ok((RunOutcome::Errored { message }, _)) => { eprintln!("error: {message}"); 1 } Err(e) => { eprintln!("error: {e}"); 1 } } } fn log_dir() -> PathBuf { dirs::data_dir() .unwrap_or_else(std::env::temp_dir) .join("ai-harness") .join("log") } fn setup_tracing() -> anyhow::Result { let log_dir = log_dir(); std::fs::create_dir_all(&log_dir)?; let file_appender = tracing_appender::rolling::daily(log_dir, "harness-tui.log"); let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); tracing_subscriber::fmt() .with_writer(non_blocking) .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .init(); Ok(guard) } async fn run_tui() -> i32 { let cwd = match std::env::current_dir() { Ok(cwd) => cwd, Err(e) => { eprintln!("error: could not read cwd: {e}"); return 1; } }; let _guard = match setup_tracing() { Ok(guard) => guard, Err(e) => { eprintln!("error: could not initialize logging: {e}"); return 1; } }; let mut app = match crate::app::App::new(cwd).await { Ok(app) => app, Err(e) => { tracing::error!(error = %e, "failed to start TUI"); eprintln!("error: {e}"); return 1; } }; if let Err(e) = app.run().await { tracing::error!(error = %e, "TUI error"); eprintln!("error: {e}"); return 1; } 0 } #[tokio::main(flavor = "current_thread")] async fn main() { let args: Vec = std::env::args().skip(1).collect(); let exit_code = match args.first().map(String::as_str) { Some("run") => run_headless(&args[1..]).await, Some("tui") | None => run_tui().await, Some("help") | Some("--help") | Some("-h") => { print_usage(); 0 } Some(cmd) => { eprintln!("unknown command: {cmd}"); print_usage(); 2 } }; std::process::exit(exit_code); }