use anyhow::Result;
use clap::{Parser, Subcommand};
use std::sync::Arc;
use redb::Database;
use std::path::PathBuf;
use stargaze::{
cmd_list, cmd_readmes, cmd_search, cmd_show, cmd_stats, cmd_sync, count_repos, default_db_path,
fetch_readmes_parallel, load_all, load_one, open_db, read_meta,
resolve_token, retain_repos, run_api_server, run_mcp_stdio, truncate, upsert_repos,
regenerate_embeddings, GhClient, Repo, RepoIndex, SearchHit,
};
use async_std::task::spawn_blocking;
#[derive(Parser)]
#[command(
name = "stargaze",
version,
about = "Cache and search your GitHub stars"
)]
struct Cli {
#[arg(long, global = true)]
db: Option<PathBuf>,
#[arg(long, global = true)]
token: Option<String>,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Sync {
#[arg(short, long)]
user: Option<String>,
#[arg(long, default_value_t = false)]
prune: bool,
#[arg(long, default_value_t = false)]
with_readmes: bool,
#[arg(long, default_value_t = 8)]
concurrency: usize,
},
Readmes {
#[arg(short, long, default_value_t = 8)]
concurrency: usize,
#[arg(long, default_value_t = false)]
force: bool,
},
Search {
query: String,
#[arg(short, long, default_value_t = 30)]
limit: usize,
#[arg(long)]
lang: Option<String>,
#[arg(long)]
topic: Option<String>,
#[arg(long, default_value_t = false)]
fuzzy: bool,
#[arg(long, default_value_t = false)]
or_mode: bool,
#[arg(long, default_value_t = false)]
topic_boost: bool,
#[arg(long, default_value_t = false)]
semantic: bool,
},
Show {
full_name: String,
},
Stats,
List {
#[arg(short, long, default_value_t = 50)]
limit: usize,
},
Serve,
Api {
#[arg(long, default_value = "127.0.0.1:7879")]
bind: String,
#[arg(long)]
api_key: Option<String>,
#[arg(long, default_value_t = 4)]
threads: usize,
},
Embed {
#[arg(short, long, default_value_t = 4)]
concurrency: usize,
},
}
#[async_std::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let db_path = match cli.db {
Some(p) => p,
None => default_db_path()?,
};
let db = Arc::new(open_db(&db_path).expect("failed to open db"));
match cli.cmd {
Cmd::Sync {
user,
prune,
with_readmes,
concurrency,
} => {
let token = resolve_token(cli.token)?;
stargaze::cmd_sync(Arc::clone(&db), token, user, prune, with_readmes, concurrency).await
}
Cmd::Readmes { concurrency, force } => {
let token = resolve_token(cli.token)?;
stargaze::cmd_readmes(Arc::clone(&db), token, concurrency, force).await
}
Cmd::Search {
query,
limit,
lang,
topic,
fuzzy,
or_mode,
topic_boost,
semantic,
} => stargaze::cmd_search(Arc::clone(&db), &query, limit, lang, topic, fuzzy, or_mode, topic_boost, semantic).await,
Cmd::Show { full_name } => stargaze::cmd_show(Arc::clone(&db), &full_name).await,
Cmd::Stats => stargaze::cmd_stats(Arc::clone(&db)).await,
Cmd::List { limit } => stargaze::cmd_list(Arc::clone(&db), limit).await,
Cmd::Serve => { spawn_blocking(move || run_mcp_stdio(Arc::try_unwrap(db).unwrap_or_else(|_| panic!("Failed to unwrap Arc")))).await?; Ok(()) },
Cmd::Api {
bind,
api_key,
threads,
} => {
let addr: std::net::SocketAddr = bind
.parse()
.map_err(|e| anyhow::anyhow!("invalid --bind {}: {}", bind, e))?;
spawn_blocking(move || run_api_server(Arc::try_unwrap(db).unwrap_or_else(|_| panic!("Failed to unwrap Arc")), addr, api_key, threads)).await?; Ok(())
}
Cmd::Embed { concurrency: _ } => {
eprintln!("Regenerating embeddings for repos missing them...");
let (updated, skipped, errors) = regenerate_embeddings(&db)
.map_err(|e| anyhow::anyhow!("Failed to regenerate embeddings: {}", e))?;
eprintln!("Done: {} updated, {} skipped (already had embeddings), {} errors", updated, skipped, errors);
Ok(())
}
}
}