use actor::Actor;
use colored::Colorize;
use config::Config;
use openai::Message;
use std::{env, process, time::Duration};
mod actor;
mod cli;
mod config;
mod debug_log;
mod git;
mod jj;
mod model;
mod openai;
mod spinner;
mod util;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args: Vec<String> = env::args().collect();
if args
.iter()
.any(|arg| matches!(arg.as_str(), "-h" | "--help" | "--version"))
{
cli::Options::new(args.into_iter(), &Config::default());
unreachable!();
}
let requested_config = args.windows(2).find_map(|pair| match pair[0].as_str() {
"-c" | "--config" => Some(pair[1].clone()),
_ => None,
});
let config = if let Some(config_path) = requested_config {
Config::load_from_path(std::path::Path::new(&config_path))?
} else {
Config::load()?
};
let options = cli::Options::new(args.into_iter(), &config);
if options.check_version_only {
util::check_version().await;
return Ok(());
}
let api_key = match &options.api_key {
Some(ref key) => key.clone(),
None => {
let env_var = &config.api_key_env_var;
if env_var.trim().is_empty() {
String::new()
} else {
match env::var(env_var) {
Ok(key) => key,
Err(_) => {
println!("{}", format!("No API key found. Either:").red());
println!(" 1. Set the {} environment variable", env_var.purple());
println!(" 2. Use the {} option", "--api-key <key>".purple());
println!("\n{}", "For API key safety best practices, see: https://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety".bright_black());
process::exit(1);
}
}
}
}
};
let vcs_type = jj::detect_vcs()?;
match vcs_type {
jj::VcsType::Git => println!("{}", "Using Git repository".bright_black()),
jj::VcsType::Jujutsu => println!("{}", "Using Jujutsu repository".bright_black()),
}
let mut actor = Actor::new(
options.clone(),
api_key,
options.api_endpoint.clone(),
vcs_type.clone(),
);
let system_len =
openai::count_token(options.system_msg.as_ref().unwrap_or(&config.system_msg)).unwrap_or(0);
let extra_len = openai::count_token(&options.msg).unwrap_or(0);
actor.add_message(Message::developer(
options.system_msg.unwrap_or(config.system_msg.clone()),
));
match vcs_type {
jj::VcsType::Git => {
let repo = git::get_repo()?;
if options.amend {
if git::has_staged_changes(&repo)? {
println!("{}", "Error: You have staged changes.".red());
println!(
"{}",
"When using --amend, you should not have any staged changes."
.bright_black()
);
println!(
"{}",
"The --amend option only changes the commit message of the last commit."
.bright_black()
);
println!(
"{}",
"If you want to include new changes, either:".bright_black()
);
println!(
"{}",
"1. Commit them first normally, then amend that commit".bright_black()
);
println!(
"{}",
"2. Or use git commit --amend manually to include them".bright_black()
);
process::exit(1);
}
let diff = git::get_last_commit_diff(&repo)?;
if diff.is_empty() {
println!(
"{}",
"Error: Could not get changes from the last commit.".red()
);
println!(
"{}",
"Make sure you have at least one commit in your repository.".bright_black()
);
process::exit(1);
}
actor.add_message(Message::user(diff));
actor.used_tokens = system_len + extra_len;
} else {
let (diff, diff_tokens) = util::decide_diff(
&repo,
system_len + extra_len,
options.model.context_size(),
options.always_select_files,
)?;
actor.add_message(Message::user(diff));
actor.used_tokens = system_len + extra_len + diff_tokens;
}
}
jj::VcsType::Jujutsu => {
if !jj::has_jj_changes_for_revision(options.jj_revision.as_deref())? {
let revision_msg = if let Some(ref rev) = options.jj_revision {
format!("No changes detected in Jujutsu revision '{}'.", rev)
} else {
"No changes detected in Jujutsu working directory.".to_string()
};
println!("{}", revision_msg.red());
println!(
"{}",
"Please make some changes before running turbocommit.".bright_black()
);
process::exit(1);
}
if let Some(ref rev) = options.jj_revision {
jj::validate_revision_id(rev)?;
}
let (diff, diff_tokens) = util::decide_diff_jj(
system_len + extra_len,
options.model.context_size(),
options.always_select_files,
options.jj_revision.as_deref(),
)?;
if options.jj_rewrite {
if let Some(current_desc) = jj::get_jj_description(options.jj_revision.as_deref())?
{
let hint_msg = format!("Current description: {}", current_desc);
actor.add_message(Message::user(hint_msg));
}
}
actor.add_message(Message::user(diff));
actor.used_tokens = system_len + extra_len + diff_tokens;
}
}
if !options.msg.is_empty() {
actor.add_message(Message::user(options.msg));
}
if options.auto_commmit {
let _ = actor.auto_commit().await?;
} else {
actor.start().await?;
}
if !options.disable_auto_update_check {
util::check_version().await;
}
if util::check_config_age(Duration::from_secs(60 * 60 * 24 * 30 * 6)) {
if !util::is_system_prompt_same_as_default(&config.system_msg) {
println!(
"\n{}\n{}\n{}",
"Your developer instruction seems to be old.".yellow(),
"There is a new recommended default. To apply it, delete the `system_msg` field in your config file.".bright_black(),
"To get rid of this message, simply save your config file to change the last modified date.".bright_black()
);
}
}
Ok(())
}