mod commands;
mod output;
mod prompt;
mod selfupdate;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use is_terminal::IsTerminal;
use repograph_core::{Config, RepographError};
use tracing_subscriber::EnvFilter;
#[derive(Debug, Parser)]
#[command(name = "repograph", version, about)]
pub(crate) struct Cli {
#[arg(long, global = true, env = "REPOGRAPH_CONFIG_DIR", value_name = "PATH")]
config_dir: Option<PathBuf>,
#[arg(long, global = true, env = "REPOGRAPH_DATA_DIR", value_name = "PATH")]
data_dir: Option<PathBuf>,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Add(commands::add::Args),
Completions(commands::completions::Args),
Context(commands::context::Args),
Doctor(commands::doctor::Args),
Edit(commands::edit::Args),
Find(commands::find::Args),
Index(commands::index::Args),
Init(commands::init::Args),
List(commands::list::Args),
Remove(commands::remove::Args),
Status(commands::status::Args),
Switch(commands::switch::Args),
Update(commands::update::Args),
Workspace(commands::workspace::Args),
}
fn main() -> ExitCode {
init_tracing();
let cli = Cli::parse();
let config_dir = match resolve_config_dir(cli.config_dir, Config::default_dir()) {
Ok(p) => p,
Err(e) => return report(&e),
};
let command_is_update = matches!(cli.command, Command::Update(_));
let data_dir = || resolve_data_dir(cli.data_dir.clone(), default_data_dir());
let result = match cli.command {
Command::Add(args) => commands::add::run(args, &config_dir),
Command::Completions(args) => commands::completions::run(&args),
Command::Context(args) => commands::context::run(&args, &config_dir),
Command::Doctor(args) => {
data_dir().and_then(|d| commands::doctor::run(&args, &config_dir, &d))
}
Command::Edit(args) => commands::edit::run(args, &config_dir),
Command::Find(args) => data_dir().and_then(|d| commands::find::run(&args, &config_dir, &d)),
Command::Index(args) => {
data_dir().and_then(|d| commands::index::run(&args, &config_dir, &d))
}
Command::Init(args) => commands::init::run(&args, &config_dir),
Command::List(args) => commands::list::run(&args, &config_dir),
Command::Remove(args) => commands::remove::run(&args, &config_dir),
Command::Status(args) => commands::status::run(&args, &config_dir),
Command::Switch(args) => commands::switch::run(&args, &config_dir),
Command::Update(args) => commands::update::run(&args),
Command::Workspace(args) => commands::workspace::run(args, &config_dir),
};
let succeeded = result.is_ok();
let exit = match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => report(&e),
};
if succeeded {
selfupdate::notify(command_is_update);
}
exit
}
fn init_tracing() {
let default_level = if std::io::stderr().is_terminal() {
"warn"
} else {
"info"
};
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level));
let _ = tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(filter)
.with_target(false)
.without_time()
.try_init();
}
fn resolve_config_dir(
override_path: Option<PathBuf>,
default: Option<PathBuf>,
) -> Result<PathBuf, RepographError> {
if let Some(p) = override_path {
return Ok(p);
}
default.ok_or_else(|| {
RepographError::UsageError(
"no config directory available; pass --config-dir or set REPOGRAPH_CONFIG_DIR"
.to_string(),
)
})
}
fn default_data_dir() -> Option<PathBuf> {
dirs::data_dir().map(|d| d.join("repograph"))
}
fn resolve_data_dir(
override_path: Option<PathBuf>,
default: Option<PathBuf>,
) -> Result<PathBuf, RepographError> {
if let Some(p) = override_path {
return Ok(p);
}
default.ok_or_else(|| {
RepographError::UsageError(
"no data directory available; pass --data-dir or set REPOGRAPH_DATA_DIR".to_string(),
)
})
}
fn report(err: &RepographError) -> ExitCode {
if !matches!(err, RepographError::DoctorErrorsFound { .. }) {
tracing::error!(error = %err, "repograph failed");
}
ExitCode::from(err.exit_code())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn override_wins_over_default() {
let p = resolve_config_dir(
Some(PathBuf::from("/tmp/override")),
Some(PathBuf::from("/tmp/default")),
)
.unwrap();
assert_eq!(p, PathBuf::from("/tmp/override"));
}
#[test]
fn default_used_when_no_override() {
let p = resolve_config_dir(None, Some(PathBuf::from("/tmp/default"))).unwrap();
assert_eq!(p, PathBuf::from("/tmp/default"));
}
#[test]
fn no_override_no_default_returns_usage_error_exit_2() {
let err = resolve_config_dir(None, None).unwrap_err();
assert_eq!(err.exit_code(), 2);
let msg = err.to_string();
assert!(
msg.contains("--config-dir"),
"message guides user to --config-dir, got: {msg}"
);
}
}