use anyhow::{Context, Result};
use clap::Parser;
use ralph::{cli, redaction, sanity};
use std::ffi::OsString;
fn main() {
let args = normalize_repo_prompt_args(std::env::args_os());
let is_machine_command = is_machine_command_args(&args);
if let Err(err) = run(args) {
if is_machine_command {
if let Err(print_err) = ralph::cli::machine::print_machine_error(&err) {
use colored::Colorize;
let msg = format!(
"{:#}\nfailed to emit machine error JSON: {print_err:#}",
err
);
let redacted = redaction::redact_text(&msg);
eprintln!("{} {}", "Error:".red().bold(), redacted);
}
} else {
use colored::Colorize;
let msg = format!("{:#}", err);
let redacted = redaction::redact_text(&msg);
eprintln!("{} {}", "Error:".red().bold(), redacted);
}
std::process::exit(1);
}
}
fn run(args: Vec<OsString>) -> Result<()> {
if let Err(e) = dotenvy::dotenv() {
if is_not_found_error(&e) {
} else {
let msg = format!("Warning: failed to load .env file: {e}");
eprintln!("{}", redaction::redact_text(&msg));
}
}
let cli = cli::Cli::parse_from(args);
cli::color::init_color(cli.color, cli.no_color);
let mut builder = env_logger::Builder::from_default_env();
if cli.verbose {
builder.filter_level(log::LevelFilter::Debug);
} else if std::env::var("RUST_LOG").is_err() {
builder.filter_level(log::LevelFilter::Info);
}
let logger = builder.build();
let max_level = logger.filter();
redaction::RedactedLogger::init(Box::new(logger), max_level)
.context("initialize redacted logger")?;
if let Err(err) =
ralph::fsutil::cleanup_default_temp_dirs(ralph::constants::timeouts::TEMP_RETENTION)
{
log::debug!("startup temp cleanup: {:#}", err);
}
let should_run_sanity = sanity::should_run_sanity_checks(&cli.command);
let should_refresh_readme = sanity::should_refresh_readme_for_command(&cli.command);
if should_refresh_readme && (cli.no_sanity_checks || !should_run_sanity) {
let resolved = ralph::config::resolve_from_cwd_for_doctor()?;
if let Some(msg) = sanity::refresh_readme_if_needed(&resolved)? {
log::info!("{}", msg);
}
}
if should_run_sanity && !cli.no_sanity_checks {
let resolved = ralph::config::resolve_from_cwd_for_doctor()?;
let non_interactive = match &cli.command {
cli::Command::Run(run_args) => match &run_args.command {
cli::run::RunCommand::One(one_args) => one_args.non_interactive,
cli::run::RunCommand::Loop(loop_args) => loop_args.non_interactive,
cli::run::RunCommand::Resume(resume_args) => resume_args.non_interactive,
cli::run::RunCommand::Parallel(_) => true, },
_ => false,
};
let options = sanity::SanityOptions {
auto_fix: cli.auto_fix,
skip: false,
non_interactive,
};
let sanity_result = sanity::run_sanity_checks(&resolved, &options)?;
if !sanity::report_sanity_results(&sanity_result, cli.auto_fix) {
anyhow::bail!(
"Sanity checks failed. Please resolve the issues above or run with --auto-fix."
);
}
}
match cli.command {
cli::Command::Queue(args) => cli::queue::handle_queue(args.command, cli.force),
cli::Command::Config(args) => cli::config::handle_config(args.command),
cli::Command::HelpAll => {
cli::handle_help_all();
Ok(())
}
cli::Command::Machine(args) => cli::machine::handle_machine(*args, cli.force),
cli::Command::Run(args) => cli::run::handle_run(args.command, cli.force),
cli::Command::Task(args) => cli::task::handle_task(*args, cli.force),
cli::Command::Scan(args) => cli::scan::handle_scan(args, cli.force),
cli::Command::Init(args) => cli::init::handle_init(args, cli.force),
cli::Command::App(args) => cli::app::handle_app(args.command),
cli::Command::Prompt(args) => cli::prompt::handle_prompt(args),
cli::Command::Doctor(args) => cli::doctor::handle_doctor(args),
cli::Command::Context(args) => cli::context::handle_context(args),
cli::Command::Prd(args) => cli::prd::handle_prd(args, cli.force),
cli::Command::Completions(args) => cli::completions::handle_completions(args),
cli::Command::Migrate(args) => cli::migrate::handle_migrate(args),
cli::Command::Cleanup(args) => cli::cleanup::handle_cleanup(args),
cli::Command::Version(args) => cli::version::handle_version(args),
cli::Command::Watch(args) => cli::watch::handle_watch(args, cli.force),
cli::Command::Webhook(args) => {
let resolved = ralph::config::resolve_from_cwd()?;
cli::webhook::handle_webhook(&args, &resolved)
}
cli::Command::Productivity(args) => cli::productivity::handle(args),
cli::Command::Plugin(args) => {
let resolved = ralph::config::resolve_from_cwd()?;
ralph::commands::plugin::run(&args, &resolved)
}
cli::Command::Runner(args) => match args.command {
cli::runner::RunnerCommand::Capabilities(cap_args) => {
cli::runner::handle_runner_capabilities(cap_args)
}
cli::runner::RunnerCommand::List(list_args) => {
cli::runner::handle_runner_list(list_args)
}
},
cli::Command::Daemon(args) => cli::daemon::handle_daemon(args.command),
cli::Command::Tutorial(args) => cli::tutorial::handle_tutorial(args),
cli::Command::Undo(args) => cli::undo::handle(args, cli.force),
cli::Command::CliSpec(args) => cli::handle_cli_spec(args),
}
}
fn is_not_found_error(e: &dotenvy::Error) -> bool {
use std::io;
match e {
dotenvy::Error::Io(io_err) if io_err.kind() == io::ErrorKind::NotFound => true,
_ => {
let err_str = e.to_string().to_lowercase();
err_str.contains("not found") || err_str.contains("no such file")
}
}
}
fn is_machine_command_args(args: &[OsString]) -> bool {
let mut iter = args.iter().skip(1);
while let Some(arg) = iter.next() {
let Some(value) = arg.to_str() else {
return false;
};
match value {
"--force" | "-f" | "--verbose" | "-v" | "--no-color" | "--auto-fix"
| "--no-sanity-checks" => continue,
"--color" => {
let _ = iter.next();
continue;
}
_ if value.starts_with("--color=") => continue,
_ => return value == "machine",
}
}
false
}
fn normalize_repo_prompt_args<I>(args: I) -> Vec<OsString>
where
I: IntoIterator<Item = OsString>,
{
let mut normalized = Vec::new();
let mut passthrough = false;
for arg in args {
if passthrough {
normalized.push(arg);
continue;
}
if arg == std::ffi::OsStr::new("--") {
passthrough = true;
normalized.push(arg);
continue;
}
let as_str = arg.to_str();
if as_str == Some("-rp") {
normalized.push(OsString::from("--repo-prompt"));
continue;
}
if let Some(value) = as_str.and_then(|s| s.strip_prefix("-rp=")) {
let mut rewritten = OsString::from("--repo-prompt=");
rewritten.push(value);
normalized.push(rewritten);
continue;
}
normalized.push(arg);
}
normalized
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_repo_prompt_args_rewrites_short_flag() {
let args = vec![
OsString::from("ralph"),
OsString::from("-rp"),
OsString::from("plan"),
];
let normalized = normalize_repo_prompt_args(args);
assert_eq!(
normalized,
vec![
OsString::from("ralph"),
OsString::from("--repo-prompt"),
OsString::from("plan")
]
);
}
#[test]
fn normalize_repo_prompt_args_rewrites_equals_form() {
let args = vec![OsString::from("ralph"), OsString::from("-rp=tools")];
let normalized = normalize_repo_prompt_args(args);
assert_eq!(
normalized,
vec![
OsString::from("ralph"),
OsString::from("--repo-prompt=tools")
]
);
}
#[test]
fn normalize_repo_prompt_args_respects_double_dash() {
let args = vec![
OsString::from("ralph"),
OsString::from("--"),
OsString::from("-rp"),
OsString::from("plan"),
];
let normalized = normalize_repo_prompt_args(args);
assert_eq!(
normalized,
vec![
OsString::from("ralph"),
OsString::from("--"),
OsString::from("-rp"),
OsString::from("plan")
]
);
}
#[test]
fn is_machine_command_args_detects_machine_after_globals() {
let args = vec![
OsString::from("ralph"),
OsString::from("--no-color"),
OsString::from("--color=never"),
OsString::from("machine"),
OsString::from("queue"),
OsString::from("read"),
];
assert!(is_machine_command_args(&args));
}
#[test]
fn is_machine_command_args_rejects_non_machine_commands() {
let args = vec![
OsString::from("ralph"),
OsString::from("--verbose"),
OsString::from("queue"),
OsString::from("read"),
];
assert!(!is_machine_command_args(&args));
}
}