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")
);
}
}