use clap::{Parser, Subcommand};
use doiget_cli::commands::output::{self, FlagInput, OutputMode};
#[derive(Subcommand, Debug)]
enum ProvenanceAction {
Migrate {
#[arg(long)]
dry_run: bool,
},
}
#[derive(Parser, Debug)]
#[command(
name = "doiget",
version,
about = "Fetch academic papers via official Open Access APIs.",
long_about = "doiget is an OA-first paper fetcher and stdio MCP server.\n\
\n\
Subcommands:\n\
\x20 fetch Fetch a single paper PDF by DOI or arXiv id\n\
\x20 batch Fetch many refs from a newline-separated file\n\
\x20 bib Export a stored entry as BibTeX\n\
\x20 csl Export a stored entry as CSL JSON\n\
\x20 info Show metadata for a stored entry\n\
\x20 search Search the local store by title / authors / venue\n\
\x20 list-recent List the most recently fetched entries\n\
\x20 audit-log Inspect or verify the provenance log\n\
\x20 provenance Provenance-log lifecycle ops (migrate v1 -> v2)\n\
\x20 config Show or doctor the resolved configuration\n\
\x20 serve Run as an MCP server over stdio\n\
\x20 graph Expand a DOI's citation neighborhood via OpenAlex\n\
\x20 (requires --features citation + DOIGET_ENABLE_OPENALEX)\n\
\x20 capabilities Emit a JSON inventory of the binary's full surface\n\
\x20 (for LLM cold-boot; #214)\n\
\n\
See README.md and docs/ for the full specification."
)]
struct Cli {
#[arg(
long,
global = true,
value_enum,
conflicts_with_all = ["json", "quiet"],
)]
mode: Option<OutputMode>,
#[arg(long, global = true, conflicts_with_all = ["mode", "quiet"])]
json: bool,
#[arg(short = 'q', long, global = true, conflicts_with_all = ["mode", "json"])]
quiet: bool,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand, Debug)]
enum Command {
Fetch {
ref_: String,
#[arg(long)]
dry_run: bool,
},
Batch {
path: String,
#[arg(long)]
dry_run: bool,
},
Info {
ref_: String,
},
ListRecent {
#[arg(default_value_t = 10)]
limit: usize,
},
Search {
query: String,
},
Bib {
ref_: String,
},
Csl {
ref_: String,
},
AuditLog {
#[arg(long)]
verify: bool,
},
Provenance {
#[command(subcommand)]
action: ProvenanceAction,
},
Serve,
Capabilities,
Config {
action: String,
},
#[cfg(feature = "citation")]
Graph {
ref_: String,
#[arg(long)]
depth: Option<u32>,
#[arg(long)]
total: Option<u32>,
#[arg(long)]
per_paper: Option<u32>,
},
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let cli = Cli::parse();
let result: anyhow::Result<()> = run_dispatch(cli).await;
match result {
Ok(()) => Ok(()),
Err(err) => match err.downcast_ref::<doiget_cli::commands::fetch::CliExit>() {
Some(doiget_cli::commands::fetch::CliExit(code)) => {
std::process::exit(*code);
}
None => Err(err),
},
}
}
fn flag_input_from(cli: &Cli) -> FlagInput {
if let Some(m) = cli.mode {
FlagInput::Explicit(m)
} else if cli.json {
FlagInput::JsonShort
} else if cli.quiet {
FlagInput::QuietShort
} else {
FlagInput::None
}
}
fn forced_implicit_for(command: &Option<Command>) -> Option<OutputMode> {
match command {
Some(Command::Serve) => Some(OutputMode::Mcp),
_ => None,
}
}
async fn run_dispatch(cli: Cli) -> anyhow::Result<()> {
let mode = output::resolve(
forced_implicit_for(&cli.command),
flag_input_from(&cli),
std::env::var("DOIGET_MODE").ok().as_deref(),
output::stdout_is_tty(),
);
match cli.command {
None => {
anyhow::bail!("no subcommand. Run `doiget --help` for available commands.");
}
Some(Command::AuditLog { verify }) => doiget_cli::commands::audit_log::run(verify, mode),
Some(Command::Provenance { action }) => match action {
ProvenanceAction::Migrate { dry_run } => {
doiget_cli::commands::provenance::migrate(dry_run, mode)
}
},
Some(Command::Config { action }) => doiget_cli::commands::config::run(action, mode),
Some(Command::Info { ref_ }) => doiget_cli::commands::info::run(ref_, mode),
Some(Command::ListRecent { limit }) => doiget_cli::commands::list_recent::run(limit, mode),
Some(Command::Search { query }) => doiget_cli::commands::search::run(query, mode),
Some(Command::Fetch { ref_, dry_run }) => {
doiget_cli::commands::fetch::run_with_options(ref_, dry_run, mode).await
}
Some(Command::Batch { path, dry_run }) => {
doiget_cli::commands::batch::run_with_options(path, dry_run, mode).await
}
Some(Command::Bib { ref_ }) => doiget_cli::commands::bib::run(ref_, mode),
Some(Command::Csl { ref_ }) => doiget_cli::commands::csl::run(ref_, mode),
Some(Command::Serve) => {
debug_assert_eq!(mode, OutputMode::Mcp, "serve must resolve to Mcp");
let profile = doiget_core::CapabilityProfile::from_env()?;
doiget_mcp::Server::new(profile).run().await
}
Some(Command::Capabilities) => {
let cli_cmd = <Cli as clap::CommandFactory>::command();
doiget_cli::commands::capabilities::run(&cli_cmd, mode)
}
#[cfg(feature = "citation")]
Some(Command::Graph {
ref_,
depth,
total,
per_paper,
}) => doiget_cli::commands::graph::run(ref_, depth, total, per_paper, mode).await,
}
}