use anyhow::Result;
use clap::{Parser, Subcommand};
pub mod config;
pub mod db;
pub mod dto;
pub mod inference;
pub mod installer;
pub mod os_detector;
pub mod runner;
pub mod tokenizer;
pub mod updater;
#[derive(Parser, Debug)]
#[command(
name = "cmdh",
about = "cmdh — the CmdHub CLI client for offline command search and execution",
version
)]
pub struct Cli {
#[arg(short, long, global = true, help = "Custom configuration file path")]
pub config: Option<std::path::PathBuf>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug, Clone)]
pub enum Commands {
Search {
query: String,
#[arg(long, default_value_t = 1)]
limit: usize,
#[arg(short, long, group = "output_format")]
full: bool,
#[arg(short, long, group = "output_format")]
usage_only: bool,
#[arg(short, long, group = "output_format")]
minimal: bool,
},
Update {
#[arg(long)]
force: bool,
},
Run {
cmd_path: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
#[arg(short, long)]
yes: bool,
},
Install {
#[command(subcommand)]
sub: InstallAction,
},
Init {
#[arg(long)]
force: bool,
},
}
#[derive(Subcommand, Debug, Clone)]
pub enum InstallAction {
Vector {
#[arg(long, value_name = "FILE")]
from_file: Option<std::path::PathBuf>,
#[arg(long)]
force: bool,
},
}
pub async fn run() -> Result<()> {
let cli = Cli::parse();
if let Commands::Init { force } = cli.command {
let config_dir = config::get_config_dir();
let config_path = config_dir.join("config.toml");
if config_path.exists() && !force {
eprintln!(
"Warning: Configuration file already exists at {:?}",
config_path
);
return Ok(());
}
std::fs::create_dir_all(&config_dir)?;
let detected = os_detector::detect_os().unwrap_or_else(|| "unknown".to_string());
let config_content = format!(
r#"# CmdHub configuration file
api_url = "https://api.cmdhub.xyz"
[output]
# Set the format of the search results output to stdout.
# Supported modes:
# - "full" : Returns the full command contract including descriptions, risks, and install commands.
# - "usage" : Returns a slim template format focusing purely on path and execution usage structure.
# - "minimal": Returns only the command pathway (e.g. [{{"cmd_path":"git"}}]).
mode = "full"
[install]
# Host operating system override.
# Detected on your platform as: "{detected}"
# To override manually, uncomment the line below:
# os = "{detected}"
# Priority sequence when searching for package manager installer instructions.
# The resolver checks system installers first (matching your OS release),
# then traverses these developer packages in order.
package_managers = ["uv", "npm", "cargo", "go"]
"#
);
std::fs::write(&config_path, config_content)?;
println!(
"Configuration initialized successfully at {:?}",
config_path
);
return Ok(());
}
let config = config::load_or_create_config(cli.config.clone())?;
let conn = db::open_db()?;
db::init_db(&conn)?;
match cli.command {
Commands::Search {
query,
limit,
full,
usage_only,
minimal,
} => {
let default_path = config::get_data_dir().join("models/bge-micro-v2.onnx");
let model_path = config
.vector
.model_path
.as_ref()
.map(std::path::PathBuf::from)
.unwrap_or(default_path);
let mut query_vector = None;
if model_path.exists() {
if let Ok(model) = inference::EmbeddingModel::load(&model_path) {
let tokenizer = tokenizer::Tokenizer::new();
let (ids, mask) = tokenizer.tokenize_query(&query);
if let Ok(vec) = model.generate_embedding(&ids, &mask) {
query_vector = Some(vec);
}
}
} else {
eprintln!(
"Tip: Semantic search is inactive. Run 'cmdh install vector' to activate."
);
}
let results = db::search_all(&conn, &query, query_vector.as_deref(), limit)?;
let mode = if full {
"full"
} else if usage_only {
"usage"
} else if minimal {
"minimal"
} else {
&config.output.mode
};
let json_output = dto::format_results(results, mode, &config);
println!("{}", serde_json::to_string(&json_output)?);
}
Commands::Update { force } => {
updater::update_database(&config, force).await?;
}
Commands::Run {
cmd_path,
args,
yes,
} => {
runner::run_command(&conn, &cmd_path, &args, yes)?;
}
Commands::Install { sub } => match sub {
InstallAction::Vector { from_file, force } => {
installer::install_vector(&config, from_file, force).await?;
}
},
Commands::Init { .. } => unreachable!(),
}
Ok(())
}