magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::{AppError, AppResult, CliArgs, CliCommand, SessionsCommand};
use std::io::{self, IsTerminal};

fn usage_error(message: impl Into<String>) -> AppError {
    AppError::usage(anyhow::anyhow!(message.into()))
}

pub(super) fn validate_update_launch(args: &CliArgs) -> AppResult<()> {
    if args.app.is_some()
        || args.command.is_some()
        || args.initial_prompt.is_some()
        || args.provider.is_some()
        || args.model.is_some()
        || args.api_key.is_some()
        || args.theme.is_some()
        || args.no_session
        || args.continue_session
        || args.resume.is_some()
    {
        return Err(usage_error("--update must be used alone"));
    }
    Ok(())
}

pub(super) fn validate_app_launch(args: &CliArgs) -> AppResult<()> {
    if args.app.is_none() {
        return Ok(());
    }
    if args.initial_prompt.is_some() {
        return Err(usage_error(
            "--app cannot be combined with --prompt; service mode does not accept an initial prompt",
        ));
    }
    if args.model.is_some() {
        return Err(usage_error(
            "--app cannot be combined with --model; service mode does not select a model",
        ));
    }
    if args.provider.is_some() {
        return Err(usage_error(
            "--app cannot be combined with --provider; service mode does not select a provider",
        ));
    }
    if args.api_key.is_some() {
        return Err(usage_error(
            "--app cannot be combined with --api-key; service mode does not accept credentials",
        ));
    }
    if args.theme.is_some() {
        return Err(usage_error(
            "--app cannot be combined with --theme; service mode does not render terminal output",
        ));
    }
    if args.no_session {
        return Err(usage_error(
            "--app cannot be combined with --no-session; service mode does not attach sessions",
        ));
    }
    if args.continue_session {
        return Err(usage_error(
            "--app cannot be combined with --continue; service mode does not attach sessions",
        ));
    }
    if args.resume.is_some() {
        return Err(usage_error(
            "--app cannot be combined with --resume; service mode does not attach sessions",
        ));
    }
    if args.command.is_some() {
        return Err(usage_error(
            "--app cannot be combined with subcommands; use one mode per invocation",
        ));
    }
    Ok(())
}

pub(super) fn validate_subcommand_launch(args: &CliArgs) -> AppResult<()> {
    if args.initial_prompt.is_some() {
        return Err(usage_error(
            "subcommands cannot be combined with --prompt; use one mode per invocation",
        ));
    }
    if args.theme.is_some() {
        return Err(usage_error(
            "subcommands cannot be combined with --theme; appearance is only used by normal startup",
        ));
    }
    if args.continue_session {
        return Err(usage_error(
            "subcommands cannot be combined with --continue; diagnostics do not attach to sessions",
        ));
    }
    if args.resume.is_some() {
        return Err(usage_error(
            "subcommands cannot be combined with --resume; diagnostics do not attach to sessions",
        ));
    }
    if args.no_session {
        return Err(usage_error(
            "subcommands cannot be combined with --no-session; diagnostics never create sessions",
        ));
    }
    if args.provider.is_some() {
        return Err(usage_error(
            "subcommands cannot be combined with --provider; diagnostics do not use provider auth",
        ));
    }
    if args.model.is_some() {
        return Err(usage_error(
            "subcommands cannot be combined with --model; diagnostics do not use provider models",
        ));
    }
    if args.api_key.is_some() {
        return Err(usage_error(
            "subcommands cannot be combined with --api-key; diagnostics do not authenticate",
        ));
    }
    if let Some(CliCommand::Sessions {
        command:
            SessionsCommand::MeasureCompression {
                model,
                max_files,
                max_bytes,
            },
    }) = args.command.as_ref()
    {
        if !crate::tool_output_measurement::is_valid_measurement_model(model) {
            return Err(usage_error(
                "measure-compression --model must name a supported openai-codex text model",
            ));
        }
        if !crate::tool_output_measurement::MeasurementLimits::is_valid_file_count(*max_files) {
            return Err(usage_error(
                "measure-compression --max-files must be between 1 and 10000",
            ));
        }
        if !crate::tool_output_measurement::MeasurementLimits::is_valid_byte_limit(*max_bytes) {
            return Err(usage_error(
                "measure-compression --max-bytes must be between 1048576 and 1073741824",
            ));
        }
    }

    Ok(())
}

pub(super) fn validate_session_launch(args: &CliArgs) -> AppResult<()> {
    if args.no_session && (args.continue_session || args.resume.is_some()) {
        return Err(usage_error(
            "--no-session cannot be combined with --continue or --resume",
        ));
    }
    if args.continue_session && args.resume.is_some() {
        return Err(usage_error(
            "--continue cannot be combined with --resume; choose one session target",
        ));
    }
    Ok(())
}

pub(super) fn validate_initial_prompt_launch(args: &CliArgs) -> AppResult<()> {
    if args
        .initial_prompt
        .as_deref()
        .is_some_and(|prompt| prompt.trim().is_empty())
    {
        return Err(usage_error("--prompt requires non-whitespace text"));
    }
    Ok(())
}

pub(super) fn validate_tui_launch() -> AppResult<()> {
    let stdin_is_tty = io::stdin().is_terminal();
    let stdout_is_tty = io::stdout().is_terminal();
    if !stdin_is_tty || !stdout_is_tty {
        return Err(usage_error(
            "Mission Control requires both stdin and stdout to be TTYs; use --app service for non-interactive clients",
        ));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::Parser;

    #[test]
    fn update_requires_standalone_maintenance_invocation() {
        assert!(validate_update_launch(&CliArgs::parse_from(["magi-code", "--update"])).is_ok());
        for flags in [
            vec!["--app", "service"],
            vec!["--prompt", "hello"],
            vec!["--resume", "session"],
            vec!["--continue"],
            vec!["--no-session"],
            vec!["--model", "model"],
            vec!["--provider", "anthropic"],
            vec!["--api-key", "unused-fixture"],
            vec!["--theme", "matrix"],
            vec!["sessions", "repair-permissions", "--dry-run"],
        ] {
            let args = CliArgs::parse_from(["magi-code", "--update"].into_iter().chain(flags));
            assert!(matches!(
                validate_update_launch(&args),
                Err(AppError::Usage(_))
            ));
        }
    }

    #[test]
    fn initial_prompt_accepts_text_and_rejects_blank_text() {
        let args = CliArgs::parse_from(["magi-code", "--prompt", "hello"]);
        assert!(validate_initial_prompt_launch(&args).is_ok());
        let args = CliArgs::parse_from(["magi-code", "--prompt", " \t\n"]);
        assert_eq!(
            validate_initial_prompt_launch(&args)
                .unwrap_err()
                .to_string(),
            "--prompt requires non-whitespace text"
        );
    }

    #[test]
    fn session_selection_rejects_conflicting_targets() {
        for flags in [
            vec!["--no-session", "--continue"],
            vec!["--no-session", "--resume", "existing"],
            vec!["--continue", "--resume", "existing"],
        ] {
            let args = CliArgs::parse_from(std::iter::once("magi-code").chain(flags));
            assert!(matches!(
                validate_session_launch(&args),
                Err(AppError::Usage(_))
            ));
        }
    }

    #[test]
    fn maintenance_commands_reject_initial_prompt() {
        let args = CliArgs::parse_from(["magi-code", "--prompt", "hello", "mcp", "list"]);
        assert!(
            validate_subcommand_launch(&args)
                .unwrap_err()
                .to_string()
                .contains("--prompt")
        );
    }
}