mod catalog;
mod check;
mod config;
mod crypt;
mod export;
mod init;
mod manager;
mod managers;
mod mcp;
mod plugins;
mod progress;
mod run;
mod schedule;
mod self_update;
mod snapshot;
mod upgrade;
use std::io::IsTerminal;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Args, Parser, Subcommand};
use serde::Serialize;
const EXIT_UPDATES: u8 = 100;
const EXIT_USAGE: u8 = 2;
#[derive(Parser)]
#[command(
version,
about = "Finds the package managers on this system and keeps them up to date"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
List {
#[arg(long)]
json: bool,
},
Check {
#[arg(long)]
json: bool,
#[command(flatten)]
selection: Selection,
},
Upgrade(UpgradeArgs),
Export(ExportArgs),
Init(InitArgs),
SelfUpdate(SelfUpdateArgs),
Mcp,
Plugin {
#[command(subcommand)]
action: PluginAction,
},
Schedule {
#[command(subcommand)]
action: ScheduleAction,
},
}
#[derive(Subcommand)]
enum ScheduleAction {
Install(ScheduleInstallArgs),
Status {
#[arg(long)]
json: bool,
},
Remove {
#[arg(long)]
dry_run: bool,
},
Run,
}
#[derive(Subcommand)]
enum PluginAction {
List {
#[arg(long)]
json: bool,
},
New(PluginNewArgs),
Search {
#[arg(long)]
json: bool,
},
Install(PluginInstallArgs),
Remove {
id: String,
},
}
#[derive(Args)]
struct PluginInstallArgs {
id: String,
#[arg(long, short)]
yes: bool,
#[arg(long)]
force: bool,
}
#[derive(Args)]
struct PluginNewArgs {
id: String,
#[arg(long)]
exec: bool,
#[arg(long, short)]
output: Option<PathBuf>,
}
#[derive(Args)]
struct SelfUpdateArgs {
#[arg(long)]
check: bool,
#[arg(long, short)]
yes: bool,
#[arg(long)]
json: bool,
}
#[derive(Args)]
struct ScheduleInstallArgs {
#[command(flatten)]
managers: Managers,
#[arg(long, value_enum, default_value = "daily")]
every: schedule::Every,
#[arg(long, default_value = "03:00")]
at: String,
#[arg(long)]
check_only: bool,
#[arg(long)]
dry_run: bool,
}
#[derive(Args)]
struct Managers {
#[arg(long, value_delimiter = ',')]
only: Vec<String>,
#[arg(long, value_delimiter = ',')]
exclude: Vec<String>,
}
#[derive(Args)]
struct Selection {
#[command(flatten)]
managers: Managers,
#[arg(long, value_delimiter = ',')]
hold: Vec<String>,
#[arg(long)]
refresh: bool,
}
#[derive(Args)]
struct UpgradeArgs {
#[command(flatten)]
selection: Selection,
#[arg(long, short)]
yes: bool,
#[arg(long)]
dry_run: bool,
#[arg(long)]
no_sudo: bool,
#[arg(long = "package", value_delimiter = ',')]
packages: Vec<String>,
#[arg(long)]
json: bool,
}
#[derive(Args)]
struct ExportArgs {
#[command(flatten)]
managers: Managers,
#[arg(long, short)]
output: Option<PathBuf>,
#[arg(long, conflicts_with = "no_config")]
config: bool,
#[arg(long)]
no_config: bool,
#[arg(long, short)]
yes: bool,
#[arg(long, conflicts_with = "no_config")]
encrypt: bool,
#[arg(long)]
force: bool,
#[arg(long)]
json: bool,
}
#[derive(Args)]
struct InitArgs {
#[command(flatten)]
managers: Managers,
#[arg(long)]
from: Option<PathBuf>,
#[arg(long, value_delimiter = ',', conflicts_with = "from")]
tools: Vec<String>,
#[arg(long, short)]
yes: bool,
#[arg(long)]
dry_run: bool,
#[arg(long)]
no_sudo: bool,
#[arg(long)]
no_config: bool,
#[arg(long)]
overwrite: bool,
#[arg(long)]
json: bool,
}
#[derive(Serialize)]
struct Detected {
id: &'static str,
name: &'static str,
installed: bool,
path: Option<PathBuf>,
version: Option<String>,
}
fn main() -> ExitCode {
if std::env::var_os("NO_COLOR").is_some() {
console::set_colors_enabled(false);
console::set_colors_enabled_stderr(false);
}
let command = Cli::parse().command;
let code = dispatch(&command);
if notices_updates(&command) {
self_update::notice();
}
code
}
fn notices_updates(command: &Command) -> bool {
let json = match command {
Command::List { json } | Command::Check { json, .. } => *json,
Command::Upgrade(args) => args.json,
Command::Export(args) => args.json,
Command::Init(args) => args.json,
Command::SelfUpdate(_)
| Command::Schedule { .. }
| Command::Mcp
| Command::Plugin { .. } => true,
};
!json && std::io::stdout().is_terminal() && std::io::stderr().is_terminal()
}
fn dispatch(command: &Command) -> ExitCode {
let scheduled = match command {
Command::List { json } => return list(*json),
Command::Mcp => Some(mcp::serve()),
Command::Plugin { action } => Some(match action {
PluginAction::List { json } => plugins::list(*json),
PluginAction::New(args) => plugins::new(args),
PluginAction::Search { json } => plugins::registry::search(*json),
PluginAction::Install(args) => plugins::registry::install(args),
PluginAction::Remove { id } => plugins::registry::remove(id),
}),
Command::Schedule { action } => Some(match action {
ScheduleAction::Install(args) => schedule::install(args),
ScheduleAction::Status { json } => schedule::status(*json),
ScheduleAction::Remove { dry_run } => schedule::remove(*dry_run),
ScheduleAction::Run => schedule::run_job(),
}),
_ => None,
};
if let Some(outcome) = scheduled {
return outcome.unwrap_or_else(|e| {
eprintln!("repoke: {e}");
ExitCode::from(EXIT_USAGE)
});
}
let outcome = config::load().and_then(|config| match &command {
Command::Check { json, selection } => check::run(selection, &config, *json),
Command::Upgrade(args) => upgrade::run(args, &config),
Command::Export(args) => export::run(args, &config),
Command::Init(args) => init::run(args, &config),
Command::SelfUpdate(args) => self_update::run(args),
Command::List { .. } | Command::Schedule { .. } | Command::Mcp | Command::Plugin { .. } => {
unreachable!("handled above")
}
});
outcome.unwrap_or_else(|e| {
eprintln!("repoke: {e}");
ExitCode::from(EXIT_USAGE)
})
}
fn list(json: bool) -> ExitCode {
let progress = progress::Progress::new(json);
let bar = progress.spinner("", "looking for package managers");
let detected: Vec<Detected> = check::parallel(managers::all(), |m| {
let path = m.locate();
Detected {
id: m.id(),
name: m.name(),
installed: path.is_some(),
version: path.as_deref().and_then(|p| m.version(p)),
path,
}
});
bar.finish_and_clear();
if json {
print_json(&detected);
} else {
print_list(&detected);
}
ExitCode::SUCCESS
}
fn print_list(detected: &[Detected]) {
use console::style;
let id_width = detected.iter().map(|d| d.id.len()).max().unwrap_or(0);
let name_width = detected.iter().map(|d| d.name.len()).max().unwrap_or(0);
let plugins: Vec<&str> = plugins::loaded().managers.iter().map(|m| m.id()).collect();
let (found, missing): (Vec<&Detected>, Vec<&Detected>) =
detected.iter().partition(|d| d.installed);
println!(
"{} {}",
style("Found").bold(),
style(format!("({} of {})", found.len(), detected.len())).dim()
);
for d in &found {
let version = d.version.as_deref().unwrap_or("installed");
let plugin = if plugins.contains(&d.id) {
style(" plugin").dim().to_string()
} else {
String::new()
};
println!(
" {} {} {:<name_width$} {}{plugin}",
style("✓").green(),
style(format!("{:<id_width$}", d.id)).bold(),
d.name,
style(version).cyan()
);
}
if !missing.is_empty() {
println!("\n{}", style("Not found").bold());
for d in &missing {
let line = format!("{:<id_width$} {}", d.id, d.name);
println!(" {} {}", style("·").dim(), style(line).dim());
}
}
}
fn print_json<T: Serialize>(value: &T) {
println!(
"{}",
serde_json::to_string_pretty(value).expect("serializable output")
);
}