use std::path::{Path, PathBuf};
use clap::{Args as ClapArgs, ValueEnum};
use crossbeam_channel::unbounded;
use rite_runtime::{
ExecEvent, ExecutionError, ExecutionSummary, Executor, InMemorySink, JsonlFileSink,
StartupSnapshot, TranscriptSink, UiCommand,
};
use crate::common::{
InputArgs, build_inputs_or_exit, preflight_check_materials, prompt_missing_params,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum Frontend {
#[cfg(feature = "tui")]
Tui,
Console,
Headless,
}
#[derive(ClapArgs, Debug)]
#[command(after_long_help = crate::common::INPUT_ENV_HELP)]
pub struct Args {
pub file: PathBuf,
#[command(flatten)]
pub input: InputArgs,
#[arg(long)]
pub no_prompt: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(long)]
pub no_transcript: bool,
#[arg(long, value_enum)]
pub frontend: Option<Frontend>,
}
pub fn run(args: Args) {
let mut inputs = build_inputs_or_exit(&args.input);
if !args.no_prompt {
let (ceremony_opt, _diags) = rite_resolver::analyze(&args.file, None);
if let Some(ceremony) = ceremony_opt {
prompt_missing_params(&mut inputs, &ceremony);
}
}
let result = rite_resolver::resolve_files(&args.file, Some(&inputs));
if !result.errors.is_empty() {
eprintln!("Validation errors:");
for error in &result.errors {
eprintln!(" - {error}");
}
std::process::exit(1);
}
let Ok(resolved) = result.into_result() else {
eprintln!("Internal error: resolution failed with no errors");
std::process::exit(1);
};
let unsupported = crate::common::unsupported_action_names(&resolved);
if !unsupported.is_empty() {
eprintln!("This build of rite cannot run this ceremony.");
eprintln!(" Unsupported action(s): {}", unsupported.join(", "));
eprintln!(" Rebuild rite with the required feature(s) enabled.");
std::process::exit(1);
}
if let Err(e) = preflight_check_materials(&resolved) {
eprintln!("Error: {e}");
std::process::exit(1);
}
let output_config =
rite_runtime::OutputConfig::for_ceremony(args.output, &resolved.metadata.name);
let output_dir = output_config.base_dir().to_path_buf();
if let Err(e) = std::fs::create_dir_all(&output_dir) {
eprintln!("Failed to create output directory: {e}");
std::process::exit(1);
}
let mut backend_registry =
rite_runtime::BackendRegistry::with_factory(rite_stdlib::stdlib_backend_factory());
for (name, config) in &resolved.backends {
let mut config = config.clone();
if args.dry_run {
config.provider = "mock".to_string();
}
backend_registry.declare(name.clone(), config);
}
let registry = rite_stdlib::default_registry();
let sink: Box<dyn TranscriptSink> = if args.no_transcript {
Box::new(InMemorySink::new())
} else {
let file_sink = JsonlFileSink::create(&output_dir).unwrap_or_else(|e| {
eprintln!("Failed to create transcript: {e}");
std::process::exit(1);
});
Box::new(file_sink)
};
let frontend = args.frontend.unwrap_or_else(|| {
if args.dry_run {
Frontend::Headless
} else {
default_frontend()
}
});
let (cmd_tx, cmd_rx) = unbounded::<UiCommand>();
let (event_tx, event_rx) = unbounded::<ExecEvent>();
let startup = StartupSnapshot {
system: crate::system_info::gather_system(),
environment: crate::system_info::gather_environment(),
};
let executor = Executor::new(
resolved,
registry,
backend_registry,
output_config,
args.dry_run,
startup,
);
let exec_handle = std::thread::spawn(move || executor.run(&cmd_rx, &event_tx, sink));
let frontend_result = run_frontend(frontend, &cmd_tx, event_rx);
drop(cmd_tx);
let Ok(exec_result) = exec_handle.join() else {
eprintln!("Executor thread panicked");
std::process::exit(2);
};
if let Err(e) = frontend_result {
eprintln!("Frontend error: {e}");
}
report_outcome(exec_result, &output_dir, args.no_transcript);
}
fn report_outcome(
exec_result: Result<ExecutionSummary, ExecutionError>,
output_dir: &Path,
no_transcript: bool,
) -> ! {
match exec_result {
Ok(summary) => {
if !no_transcript {
println!("Output directory: {}", output_dir.display());
println!("Transcript fingerprint: {}", summary.transcript_fingerprint);
}
std::process::exit(0);
}
Err(e) => {
if matches!(e, ExecutionError::Aborted) {
eprintln!("Ceremony aborted by the operator.");
} else {
eprintln!("Ceremony failed: {e}");
}
if !no_transcript {
eprintln!("Output directory: {}", output_dir.display());
}
std::process::exit(1);
}
}
}
fn run_frontend(
frontend: Frontend,
cmd_tx: &crossbeam_channel::Sender<UiCommand>,
event_rx: crossbeam_channel::Receiver<ExecEvent>,
) -> std::io::Result<()> {
match frontend {
#[cfg(feature = "tui")]
Frontend::Tui => rite_tui::run(cmd_tx, event_rx),
Frontend::Console => crate::console::run(cmd_tx, &event_rx),
Frontend::Headless => crate::headless::run(cmd_tx, &event_rx),
}
}
fn default_frontend() -> Frontend {
#[cfg(feature = "tui")]
{
if is_stdout_tty() {
return Frontend::Tui;
}
}
Frontend::Console
}
fn is_stdout_tty() -> bool {
use std::io::IsTerminal;
std::io::stdout().is_terminal()
}