use std::io::{IsTerminal, Read};
use std::path::Path;
use rpi_ai::types::ImageContent;
use crate::args::{parse_args, print_help, print_version, resolve_mode, Args, RunMode};
use crate::provider::{resolve, ResolveError};
use crate::session::{build, BuildError};
pub const EXIT_USAGE: i32 = 2;
pub const EXIT_RUNTIME: i32 = 1;
pub async fn run() -> i32 {
let argv: Vec<String> = std::env::args().skip(1).collect();
if argv.first().map(|s| s.as_str()) == Some("auth") {
return crate::auth::run(&argv[1..]).await;
}
let parsed = parse_args(&argv);
if parsed.help {
print_help();
return 0;
}
if parsed.version {
print_version();
return 0;
}
if !parsed.errors.is_empty() {
for err in &parsed.errors {
eprintln!("error: {err}");
}
eprintln!();
print_help();
return EXIT_USAGE;
}
if parsed.verbose {
for warn in &parsed.ignored {
eprintln!("warning: {warn}");
}
}
let cwd = match std::env::current_dir() {
Ok(c) => c,
Err(e) => {
eprintln!("error: could not determine the current directory: {e}");
return EXIT_USAGE;
}
};
let stdin_text = read_piped_stdin();
let (file_text, _file_images) = match process_file_args(&parsed.file_args, &cwd) {
Ok(t) => t,
Err(msg) => {
eprintln!("error: {msg}");
return EXIT_USAGE;
}
};
let file_text_opt = if file_text.is_empty() { None } else { Some(file_text.as_str()) };
let (initial, extra) =
build_initial_message(&parsed, stdin_text.as_deref(), file_text_opt);
let resolved = match resolve(
parsed.provider.as_deref(),
parsed.model.as_deref(),
parsed.thinking,
parsed.api_key.as_deref(),
parsed.base_url.as_deref(),
) {
Ok(r) => r,
Err(e) => {
print_resolve_error(&e);
return match e {
ResolveError::NoApiKey { .. } | ResolveError::Config(_) => EXIT_USAGE,
_ => EXIT_RUNTIME,
};
}
};
let harness = match build(&resolved, &parsed, &cwd).await {
Ok(h) => h,
Err(e) => {
print_build_error(&e);
return EXIT_RUNTIME;
}
};
let stdin_is_tty = std::io::stdin().is_terminal();
let stdout_is_tty = std::io::stdout().is_terminal();
let mode = resolve_mode(&parsed, stdin_is_tty, stdout_is_tty);
let mode = if matches!(mode, RunMode::Interactive) && stdin_text.is_some() {
RunMode::Print
} else {
mode
};
match mode {
RunMode::Print => crate::modes::print(&harness, &parsed, initial.clone(), &extra).await,
RunMode::Json => crate::modes::json(&harness, &parsed, initial.clone(), &extra).await,
RunMode::Interactive => {
crate::modes::interactive(&harness, &parsed, initial.clone(), &extra).await
}
RunMode::Rpc => {
eprintln!("error: rpc mode is not implemented in v1 (use --mode text or --mode json)");
EXIT_USAGE
}
}
}
fn read_piped_stdin() -> Option<String> {
if std::io::stdin().is_terminal() {
return None;
}
let mut buf = String::new();
match std::io::stdin().read_to_string(&mut buf) {
Ok(_) => {
let trimmed = buf.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
Err(_) => None,
}
}
fn process_file_args(
file_args: &[std::path::PathBuf],
cwd: &Path,
) -> Result<(String, Vec<ImageContent>), String> {
let mut text = String::new();
for rel in file_args {
let abs = if rel.is_absolute() {
rel.clone()
} else {
cwd.join(rel)
};
if !abs.exists() {
return Err(format!("file not found: {}", abs.display()));
}
if is_likely_image(&abs) {
return Err(format!(
"image attachments are not supported in v1: {}",
abs.display()
));
}
match std::fs::read_to_string(&abs) {
Ok(content) => {
text.push_str(&format!(
"<file name=\"{}\">\n{}\n</file>\n",
abs.display(),
content
));
}
Err(e) => {
return Err(format!(
"could not read file {}: {e}",
abs.display()
));
}
}
}
Ok((text, Vec::new()))
}
fn is_likely_image(path: &Path) -> bool {
matches!(
path.extension().and_then(|e| e.to_str()).map(|e| e.to_ascii_lowercase()).as_deref(),
Some("png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp")
)
}
fn build_initial_message(
parsed: &Args,
stdin: Option<&str>,
file_text: Option<&str>,
) -> (Option<String>, Vec<String>) {
let mut extra = parsed.messages.clone();
let mut parts: Vec<String> = Vec::new();
if let Some(s) = stdin {
parts.push(s.to_string());
}
if let Some(t) = file_text {
parts.push(t.to_string());
}
if !extra.is_empty() {
parts.push(extra.remove(0));
}
let initial = if parts.is_empty() { None } else { Some(parts.join("")) };
(initial, extra)
}
fn print_resolve_error(e: &ResolveError) {
match e {
ResolveError::NoApiKey { hint } => {
eprintln!("error: {e}");
eprintln!();
eprintln!("Provide credentials via one of: {hint}.");
}
ResolveError::Config(_) => {
eprintln!("error: {e}");
eprintln!();
eprintln!("Check ~/.rpi/auth.json / ~/.rpi/models.json (set RPI_CODING_AGENT_DIR to relocate).");
}
_ => eprintln!("error: {e}"),
}
}
fn print_build_error(e: &BuildError) {
match e {
BuildError::RestoreNotImplemented { requested: _, flag } => {
eprintln!("error: {e}");
eprintln!();
eprintln!(
"To start a fresh session instead, drop {flag} (and any --session argument)."
);
}
_ => eprintln!("error: {e}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::args::Args;
#[test]
fn build_initial_combines_stdin_file_and_first_message() {
let mut args = Args::default();
args.messages = vec!["first".into(), "second".into(), "third".into()];
let (initial, extra) =
build_initial_message(&args, Some("stdin-text"), Some("<file>...</file>"));
assert_eq!(initial.as_deref(), Some("stdin-text<file>...</file>first"));
assert_eq!(extra, vec!["second".to_string(), "third".to_string()]);
}
#[test]
fn build_initial_with_no_messages_uses_stdin_and_file_only() {
let args = Args::default();
let (initial, extra) =
build_initial_message(&args, Some("only-stdin"), Some("<file>x</file>"));
assert_eq!(initial.as_deref(), Some("only-stdin<file>x</file>"));
assert!(extra.is_empty());
}
#[test]
fn build_initial_none_when_all_empty() {
let args = Args::default();
let (initial, extra) = build_initial_message(&args, None, None);
assert!(initial.is_none());
assert!(extra.is_empty());
}
#[test]
fn build_initial_shifts_only_first_message() {
let mut args = Args::default();
args.messages = vec!["a".into(), "b".into()];
let (initial, extra) = build_initial_message(&args, None, None);
assert_eq!(initial.as_deref(), Some("a"));
assert_eq!(extra, vec!["b".to_string()]);
}
#[test]
fn is_likely_image_detects_extensions() {
assert!(is_likely_image(Path::new("foo.png")));
assert!(is_likely_image(Path::new("foo.JPG")));
assert!(!is_likely_image(Path::new("foo.rs")));
assert!(!is_likely_image(Path::new("foo")));
}
}