malvin 0.2.4

Non-interactive research and coding agent
use super::{
    DefaultRouteDispatch, Exit, command_accepts_session_name, dispatch_command,
    dispatch_default_route, dispatch_do_workflow, dispatch_gates_only_route, finish_entrypoint,
    prepare_cli_output, print_command_error, unsupported_name_error,
};
use crate::cli::args::Cli;
use crate::cli::config_defaults::is_gates_only_route;
use crate::cli::entrypoint_checks::{
    ensure_malvin_checks_for_command, ensure_malvin_checks_for_default_route,
    ensure_malvin_checks_for_do_workflow, ensure_malvin_checks_for_gates_only_route,
};
use crate::do_flow::DoArgs;

fn parse_cli_args_or_exit(
    args: impl IntoIterator<Item = impl Into<std::ffi::OsString> + Clone>,
) -> Result<(Cli, clap::ArgMatches), Exit> {
    match crate::cli::config_defaults::parse_cli_with_config_defaults(args) {
        Ok(parsed) => Ok(parsed),
        Err(e) => {
            use clap::error::ErrorKind;
            let exit = match e.kind() {
                ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => Exit::Success,
                _ => Exit::Failure,
            };
            let _ = e.print();
            Err(exit)
        }
    }
}

fn entrypoint_doc_exit(cli: &Cli) -> Exit {
    match crate::cli::command_docs::print_doc_for_cli(cli) {
        Ok(()) => Exit::Success,
        Err(e) => {
            print_command_error(&e);
            Exit::Failure
        }
    }
}

fn entrypoint_before_dispatch(cli: &Cli) -> Option<Exit> {
    if cli.do_workflow && cli.command.is_some() {
        print_command_error("`--do` cannot be combined with a subcommand");
        return Some(Exit::Failure);
    }
    if cli.command.is_none()
        && cli.request.is_none()
        && !cli.shared.doc
        && !cli.do_workflow
        && !is_gates_only_route(cli)
    {
        let _ = crate::cli::commands_help::print_commands_only_help();
        return Some(Exit::Success);
    }
    if let Some(exit) = super::entrypoint_short_help::entrypoint_request_missing_short_help(cli) {
        return Some(exit);
    }
    if cli.shared.doc {
        return Some(entrypoint_doc_exit(cli));
    }
    None
}

fn entrypoint_preflight(cli: &Cli) -> Option<Exit> {
    if cli.do_workflow {
        return ensure_malvin_checks_for_do_workflow().err().map(|e| {
            print_command_error(&e);
            Exit::Failure
        });
    }
    if let Some(command) = cli.command.as_ref() {
        ensure_malvin_checks_for_command(command);
    }
    if is_gates_only_route(cli) {
        return ensure_malvin_checks_for_gates_only_route().err().map(|e| {
            print_command_error(&e);
            Exit::Failure
        });
    }
    if cli.request.is_some() {
        return ensure_malvin_checks_for_default_route().err().map(|e| {
            print_command_error(&e);
            Exit::Failure
        });
    }
    None
}

fn entrypoint_acquire_session(
    opt_name: Option<&str>,
) -> Result<(String, crate::SessionNameGuard), Exit> {
    crate::acquire_session_name(opt_name).map_err(|e| {
        print_command_error(&e);
        Exit::Failure
    })
}

fn entrypoint_validate_name(cli: &Cli) -> Option<Exit> {
    cli.shared.name.as_ref()?;
    if cli.do_workflow || default_route_accepts_session_name(cli) || is_gates_only_route(cli) {
        return None;
    }
    cli
        .command
        .as_ref()
        .expect("command or default route request");
    print_command_error(unsupported_name_error());
    Some(Exit::Failure)
}

const fn default_route_accepts_session_name(cli: &Cli) -> bool {
    cli.command.is_none() && cli.request.is_some() && !cli.do_workflow
}

fn entrypoint_sweep_stale_acp_spawn_locks() {
    let Ok(cwd) = std::env::current_dir() else {
        return;
    };
    let chamber = crate::malvin_acp_spawn_chamber_dir(&cwd);
    if !chamber.is_dir() {
        return;
    }
    if let Err(e) = crate::acp_spawn_sweep::sweep_stale_acp_spawn_locks(&cwd) {
        tracing::warn!(
            target: "malvin::entrypoint",
            error = %e,
            "stale ACP spawn lock sweep failed; continuing"
        );
    }
}

fn run_entrypoint(cli: Cli, matches: clap::ArgMatches) -> Exit {
    prepare_cli_output(&cli.global);
    if let Some(exit) = entrypoint_before_dispatch(&cli) {
        return exit;
    }
    entrypoint_sweep_stale_acp_spawn_locks();
    if let Some(exit) = entrypoint_validate_name(&cli) {
        return exit;
    }
    if let Some(exit) = entrypoint_preflight(&cli) {
        return exit;
    }
    let accepts_name = cli.do_workflow
        || is_gates_only_route(&cli)
        || cli
            .command
            .as_ref()
            .is_some_and(command_accepts_session_name)
        || default_route_accepts_session_name(&cli);
    if accepts_name {
        let _session_name_guard = match entrypoint_acquire_session(cli.shared.name.as_deref()) {
            Ok((session_name, guard)) => {
                crate::set_active_acp_lock_slot(session_name);
                guard
            }
            Err(exit) => return exit,
        };
        return dispatch_after_session(cli, matches);
    }
    dispatch_after_session(cli, matches)
}

fn dispatch_after_session(cli: Cli, matches: clap::ArgMatches) -> Exit {
    if cli.do_workflow {
        return finish_entrypoint(dispatch_do_workflow(
            DoArgs {
                request: cli.request,
            },
            &cli.shared,
        ));
    }
    if let Some(command) = cli.command {
        finish_entrypoint(dispatch_command(command, &cli.shared, &matches))
    } else if let Some(request) = cli.request {
        let mut shared = cli.shared;
        finish_entrypoint(dispatch_default_route(DefaultRouteDispatch {
            request,
            max_loops: cli.max_loops,
            max_hypotheses: cli.max_hypotheses,
            shared: &mut shared,
            matches: &matches,
        }))
    } else if is_gates_only_route(&cli) {
        let mut shared = cli.shared;
        finish_entrypoint(dispatch_gates_only_route(
            cli.max_loops,
            cli.max_hypotheses,
            &mut shared,
            &matches,
        ))
    } else {
        Exit::Success
    }
}

pub fn entrypoint_from(
    args: impl IntoIterator<Item = impl Into<std::ffi::OsString> + Clone>,
) -> Exit {
    crate::init_from_env();
    let args: Vec<std::ffi::OsString> = args.into_iter().map(Into::into).collect();
    match parse_cli_args_or_exit(args) {
        Ok((cli, matches)) => run_entrypoint(cli, matches),
        Err(exit) => exit,
    }
}