use ai_usagebar::widget::cli::{AuthProvider, Cli, Command, NousAuthAction};
use ai_usagebar::widget::run::run;
use clap::Parser;
fn main() {
let (argv, config_path) = match split_config_arg(std::env::args_os()) {
Ok(split) => split,
Err(message) => {
eprintln!("ai-usagebar: {message}");
std::process::exit(2);
}
};
let cli = Cli::parse_from(argv);
if let Some(path) = &config_path {
if !path.is_file() {
eprintln!(
"ai-usagebar: config file not found: {} (create it first, or point --config at an existing file)",
path.display()
);
std::process::exit(2);
}
ai_usagebar::config::set_override_path(path);
}
if let Some(Command::Account { action }) = &cli.command {
std::process::exit(ai_usagebar::account::run(action));
}
if let Some(Command::Settings { action }) = &cli.command {
std::process::exit(ai_usagebar::tui::settings::run_cli(action));
}
if let Some(Command::Detect { all, json }) = &cli.command {
std::process::exit(ai_usagebar::detect::run_cli(*all, *json));
}
if let Some(Command::Vendors { json }) = &cli.command {
std::process::exit(ai_usagebar::catalog::run(*json));
}
if let Some(Command::Auth { provider }) = &cli.command {
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(_) => std::process::exit(1),
};
let code = match provider {
AuthProvider::Nous { action } => rt.block_on(run_nous_auth(action)),
};
std::process::exit(code);
}
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(_) => {
println!(
r#"{{"text":"⚠","tooltip":"failed to create tokio runtime","class":"critical"}}"#
);
std::process::exit(0);
}
};
if let Some(Command::Usage { json }) = &cli.command {
std::process::exit(rt.block_on(ai_usagebar::report::run(*json)));
}
let code = rt.block_on(run(cli));
std::process::exit(code);
}
fn split_config_arg<I>(
argv: I,
) -> Result<(Vec<std::ffi::OsString>, Option<std::path::PathBuf>), String>
where
I: IntoIterator<Item = std::ffi::OsString>,
{
let mut argv = argv.into_iter();
let mut rest = Vec::new();
let mut config: Option<std::path::PathBuf> = None;
if let Some(program) = argv.next() {
rest.push(program);
}
while let Some(arg) = argv.next() {
if arg == "--" {
rest.push(arg);
rest.extend(argv);
break;
}
let value = if arg == "--config" {
match argv.next() {
Some(value) => Some(std::path::PathBuf::from(value)),
None => return Err("--config requires a path".into()),
}
} else {
ai_usagebar::config::config_flag_value(&arg)
};
if let Some(value) = value {
if config.is_some() {
return Err("--config given more than once".into());
}
config = Some(value);
} else {
rest.push(arg);
}
}
Ok((rest, config))
}
async fn run_nous_auth(action: &NousAuthAction) -> i32 {
let store = ai_usagebar::nous::credentials::CredentialStore::default();
match action {
NousAuthAction::Logout => match store.logout() {
Ok(()) => {
println!("Nous Research logout complete");
0
}
Err(error) => {
eprintln!("Nous Research logout failed: {error}");
1
}
},
NousAuthAction::Login => {
let client = match reqwest::Client::builder()
.timeout(ai_usagebar::vendor::HTTP_CLIENT_TIMEOUT)
.redirect(ai_usagebar::vendor::same_origin_redirect_policy())
.build()
{
Ok(client) => client,
Err(_) => {
eprintln!("Nous Research login failed: could not initialize HTTP client");
return 1;
}
};
let endpoints = ai_usagebar::nous::oauth::Endpoints::default();
let device =
match ai_usagebar::nous::oauth::request_device_code(&client, &endpoints).await {
Ok(device) => device,
Err(error) => {
eprintln!("Nous Research login failed: {error}");
return 1;
}
};
println!("Open {}", device.verification_uri);
println!("Verification code: {}", device.user_code);
let opener = ai_usagebar::nous::oauth::SystemBrowserOpener;
if !ai_usagebar::nous::oauth::open_verification_url(
&device.verification_uri_complete,
&opener,
) {
eprintln!("Browser opener unavailable; open the URL manually.");
}
let token =
match ai_usagebar::nous::oauth::poll_for_token(&client, &endpoints.token, &device)
.await
{
Ok(token) => token,
Err(error) => {
eprintln!("Nous Research login failed: {error}");
return 1;
}
};
let credential =
match ai_usagebar::nous::oauth::credential_from_token(token, chrono::Utc::now()) {
Ok(credential) => credential,
Err(error) => {
eprintln!("Nous Research login failed: {error}");
return 1;
}
};
match ai_usagebar::nous::oauth::persist_credential(&store, credential) {
Ok(()) => {
println!("Nous Research login complete");
0
}
Err(error) => {
eprintln!("Nous Research login failed: {error}");
1
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(items: &[&str]) -> Vec<std::ffi::OsString> {
items.iter().map(std::ffi::OsString::from).collect()
}
#[test]
fn flag_is_extracted_before_the_subcommand() {
let (rest, config) = split_config_arg(argv(&[
"ai-usagebar",
"--config",
"a.toml",
"account",
"add",
"work",
]))
.unwrap();
assert_eq!(config.as_deref(), Some(std::path::Path::new("a.toml")));
assert_eq!(rest, argv(&["ai-usagebar", "account", "add", "work"]));
}
#[test]
fn equals_form_is_extracted_after_the_subcommand() {
let (rest, config) =
split_config_arg(argv(&["ai-usagebar", "usage", "--config=a.toml", "--json"])).unwrap();
assert_eq!(config.as_deref(), Some(std::path::Path::new("a.toml")));
assert_eq!(rest, argv(&["ai-usagebar", "usage", "--json"]));
}
#[test]
fn tokens_after_double_dash_pass_through_untouched() {
let (rest, config) = split_config_arg(argv(&[
"ai-usagebar",
"account",
"add",
"--",
"--config",
"work",
]))
.unwrap();
assert!(
config.is_none(),
"`--config` behind `--` is a positional, not the flag"
);
assert_eq!(
rest,
argv(&["ai-usagebar", "account", "add", "--", "--config", "work"])
);
}
#[test]
fn duplicate_flag_is_rejected_in_every_form() {
for repeated in [
&["ai-usagebar", "--config", "a.toml", "--config", "b.toml"][..],
&["ai-usagebar", "--config=a.toml", "--config=b.toml"][..],
&["ai-usagebar", "--config", "a.toml", "--config=b.toml"][..],
] {
let err = split_config_arg(argv(repeated)).unwrap_err();
assert_eq!(err, "--config given more than once");
}
}
#[test]
fn dangling_flag_is_rejected() {
let err = split_config_arg(argv(&["ai-usagebar", "usage", "--config"])).unwrap_err();
assert_eq!(err, "--config requires a path");
}
#[cfg(unix)]
#[test]
fn both_forms_keep_undecodable_bytes_intact() {
use std::os::unix::ffi::{OsStrExt, OsStringExt};
let equals = std::ffi::OsString::from_vec(b"--config=caf\xe9.toml".to_vec());
let (_rest, config) =
split_config_arg(vec![std::ffi::OsString::from("ai-usagebar"), equals]).unwrap();
assert_eq!(
config.expect("equals form parsed").as_os_str().as_bytes(),
b"caf\xe9.toml"
);
let spaced = std::ffi::OsString::from_vec(b"caf\xe9.toml".to_vec());
let (_rest, config) = split_config_arg(vec![
std::ffi::OsString::from("ai-usagebar"),
std::ffi::OsString::from("--config"),
spaced,
])
.unwrap();
assert_eq!(
config.expect("spaced form parsed").as_os_str().as_bytes(),
b"caf\xe9.toml"
);
}
#[cfg(windows)]
#[test]
fn equals_form_keeps_lone_surrogates_intact() {
use std::os::windows::ffi::{OsStrExt, OsStringExt};
let mut wide: Vec<u16> = "--config=".encode_utf16().collect();
wide.push(0xDC00); let arg = std::ffi::OsString::from_wide(&wide);
let (_rest, config) =
split_config_arg(vec![std::ffi::OsString::from("ai-usagebar"), arg]).unwrap();
assert_eq!(
config
.expect("equals form parsed")
.as_os_str()
.encode_wide()
.collect::<Vec<_>>(),
vec![0xDC00u16]
);
}
}