fn checkpoint_path() -> Option<std::path::PathBuf> {
std::env::var("HOME").ok().map(|h| {
std::path::PathBuf::from(h)
.join(".config")
.join("vibestats")
.join("checkpoint.toml")
})
}
pub fn run() {
let new_token = match std::process::Command::new("gh")
.args(["auth", "token"])
.output()
{
Err(e) => {
println!("vibestats: auth failed — could not run 'gh': {e}");
println!("Ensure 'gh' CLI is installed and accessible in PATH.");
return;
}
Ok(out) if !out.status.success() => {
let stderr = String::from_utf8_lossy(&out.stderr);
println!(
"vibestats: auth failed — 'gh auth token' returned non-zero: {}",
stderr.trim()
);
println!("Run 'gh auth login' first, then retry 'vibestats auth'.");
return;
}
Ok(out) => {
let token = String::from_utf8_lossy(&out.stdout).trim().to_string();
if token.is_empty() {
println!("vibestats: auth failed — 'gh auth token' returned empty token.");
println!("Run 'gh auth login' first, then retry 'vibestats auth'.");
return;
}
token
}
};
let mut config = match crate::config::Config::load() {
Ok(c) => c,
Err(e) => {
println!("vibestats: auth failed — could not load config: {e}");
return;
}
};
config.oauth_token = new_token;
if let Err(e) = config.save() {
println!("vibestats: auth failed — could not save config: {e}");
return;
}
let secret_result = (|| -> std::io::Result<std::process::Output> {
use std::io::Write;
let mut child = std::process::Command::new("gh")
.args([
"secret",
"set",
"VIBESTATS_TOKEN",
"--repo",
&config.vibestats_data_repo,
"--body-file",
"-",
])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
child
.stdin
.as_mut()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "stdin not piped"))?
.write_all(config.oauth_token.as_bytes())?;
drop(child.stdin.take());
child.wait_with_output()
})();
match secret_result {
Err(e) => {
println!(
"vibestats: token saved locally but could not update VIBESTATS_TOKEN secret: {e}"
);
println!("Run manually (feeds token via stdin to avoid leaking it in argv):");
println!(
" gh auth token | gh secret set VIBESTATS_TOKEN --repo {} --body-file -",
config.vibestats_data_repo
);
}
Ok(out) if !out.status.success() => {
let stderr = String::from_utf8_lossy(&out.stderr);
println!("vibestats: token saved locally but 'gh secret set' failed: {stderr}");
println!("Run manually (feeds token via stdin to avoid leaking it in argv):");
println!(
" gh auth token | gh secret set VIBESTATS_TOKEN --repo {} --body-file -",
config.vibestats_data_repo
);
}
Ok(_) => {} }
if let Some(cp_path) = checkpoint_path() {
let mut cp = crate::checkpoint::Checkpoint::load(&cp_path);
cp.clear_auth_error();
if let Err(e) = cp.save(&cp_path) {
println!("vibestats: auth complete (note: could not clear auth_error flag: {e})");
return;
}
}
println!("vibestats: auth complete");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn checkpoint_path_returns_some_when_home_is_set() {
let result = checkpoint_path();
assert!(
result.is_some(),
"checkpoint_path() should return Some when HOME is set"
);
let path = result.unwrap();
let path_str = path.to_string_lossy();
assert!(
path_str.ends_with(".config/vibestats/checkpoint.toml"),
"path should end with .config/vibestats/checkpoint.toml, got: {path_str}"
);
}
#[test]
fn checkpoint_path_returns_none_when_home_unset() {
let original_home = std::env::var("HOME").ok();
unsafe {
std::env::remove_var("HOME");
}
let result = checkpoint_path();
if let Some(home) = original_home {
unsafe {
std::env::set_var("HOME", home);
}
}
assert!(
result.is_none(),
"checkpoint_path() should return None when HOME is unset"
);
}
#[test]
fn nonexistent_gh_binary_causes_error() {
let result = std::process::Command::new("/nonexistent/gh")
.args(["auth", "token"])
.output();
assert!(
result.is_err(),
"Expected Err for non-existent binary, got Ok"
);
}
}