1use anyhow::Result;
2use clap::{Parser, Subcommand};
3
4pub mod auth;
5pub mod config;
6pub mod db;
7pub mod dto;
8pub mod inference;
9pub mod installer;
10pub mod os_detector;
11pub mod robustness;
12pub mod runner;
13pub mod tokenizer;
14pub mod updater;
15
16#[derive(Parser, Debug)]
17#[command(
18 name = "cmdh",
19 about = "cmdh — the CmdHub CLI client for offline command search and execution",
20 version
21)]
22pub struct Cli {
23 #[arg(short, long, global = true, help = "Custom configuration file path")]
25 pub config: Option<std::path::PathBuf>,
26
27 #[command(subcommand)]
28 pub command: Commands,
29}
30
31#[derive(Subcommand, Debug, Clone)]
32pub enum Commands {
33 Search {
35 query: String,
37 #[arg(long, default_value_t = 5)]
39 limit: usize,
40 #[arg(short, long, group = "output_format")]
42 full: bool,
43 #[arg(short, long, group = "output_format")]
45 usage_only: bool,
46 #[arg(short, long, group = "output_format")]
48 minimal: bool,
49 },
50 Update {
52 #[arg(long)]
54 force: bool,
55 },
56 Run {
58 cmd_path: String,
60 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
62 args: Vec<String>,
63 #[arg(short, long)]
65 yes: bool,
66 },
67 Install {
69 #[command(subcommand)]
70 sub: InstallAction,
71 },
72 Init {
74 #[arg(long)]
76 force: bool,
77 },
78 Completions {
80 #[arg(value_enum)]
82 shell: clap_complete::Shell,
83 },
84 Login,
86 Logout,
88}
89
90#[derive(Subcommand, Debug, Clone)]
91pub enum InstallAction {
92 Vector {
94 #[arg(long, value_name = "FILE")]
96 from_file: Option<std::path::PathBuf>,
97 #[arg(long)]
99 force: bool,
100 },
101}
102
103pub async fn run() -> Result<()> {
104 let cli = Cli::parse();
106
107 if let Commands::Init { force } = cli.command {
109 let config_dir = config::get_config_dir();
110 let config_path = config_dir.join("config.toml");
111
112 if config_path.exists() && !force {
113 eprintln!(
114 "Warning: Configuration file already exists at {:?}",
115 config_path
116 );
117 return Ok(());
118 }
119
120 std::fs::create_dir_all(&config_dir)?;
121
122 let detected = os_detector::detect_os().unwrap_or_else(|| "unknown".to_string());
123 let default_key: String = config::OFFICIAL_PUBLIC_KEY
124 .iter()
125 .map(|b| format!("{:02x}", b))
126 .collect();
127 let config_content = format!(
128 r#"# CmdHub configuration file
129api_url = "https://cdn.cmdhub.org"
130public_key = "{default_key}"
131timeout_seconds = 30
132
133[output]
134# Set the format of the search results output to stdout.
135# Supported modes:
136# - "full" : Returns the full command contract including descriptions, risks, and install commands.
137# - "usage" : Returns a slim template format focusing purely on path and execution usage structure.
138# - "minimal": Returns only the command pathway (e.g. [{{"cmd_path":"git"}}]).
139mode = "full"
140
141[install]
142# Host operating system override.
143# Detected on your platform as: "{detected}"
144# To override manually, uncomment the line below:
145# os = "{detected}"
146
147# Priority sequence when searching for package manager installer instructions.
148# The resolver checks system installers first (matching your OS release),
149# then traverses these developer packages in order.
150package_managers = ["uv", "npm", "cargo", "go"]
151"#
152 );
153
154 std::fs::write(&config_path, config_content)?;
155 println!(
156 "Configuration initialized successfully at {:?}",
157 config_path
158 );
159 return Ok(());
160 }
161
162 let config = config::load_or_create_config(cli.config.clone())?;
164
165 let conn = {
170 let is_force_update = matches!(cli.command, Commands::Update { force: true });
171
172 if !is_force_update {
176 if let Err(e) = db::hydrate_starter_if_empty() {
177 eprintln!("Warning: could not seed starter database ({e}).");
178 }
179 }
180
181 let try_open_init = || -> Result<rusqlite::Connection> {
182 let c = db::open_db()?;
183 db::init_db(&c)?;
184 Ok(c)
185 };
186
187 match try_open_init() {
188 Ok(c) => c,
189 Err(e) if is_force_update => {
190 eprintln!(
191 "Warning: database is corrupt or missing ({}), deleting and recreating...",
192 e
193 );
194 let db_path = db::resolve_db_path();
195 if db_path.exists() {
196 std::fs::remove_file(&db_path)?;
197 }
198 let c = db::open_db()?;
199 db::init_db(&c)?;
200 c
201 }
202 Err(e) => return Err(e),
203 }
204 };
205
206 match cli.command {
207 Commands::Search {
208 query,
209 limit,
210 full,
211 usage_only,
212 minimal,
213 } => {
214 let robustness_query = robustness::preprocess_robustness(&query);
215 let mut query_vector = None;
216 match installer::ensure_model_installed(&config).await {
217 Ok(model_path) => {
218 if let Ok(model) = inference::EmbeddingModel::load(&model_path) {
219 let tokenizer = tokenizer::Tokenizer::new();
220 let (ids, mask) = tokenizer.tokenize_query(&robustness_query);
221 if let Ok(vec) = model.generate_embedding(&ids, &mask) {
222 query_vector = Some(vec);
223 }
224 }
225 }
226 Err(e) => {
227 eprintln!(
228 "Warning: Local semantic search is inactive ({}). Falling back to full-text search.",
229 e
230 );
231 }
232 }
233
234 let results = db::search_all(&conn, &robustness_query, query_vector.as_deref(), limit)?;
235
236 let is_none = results.iter().any(|r| r.confidence == "none");
237 if is_none {
238 let is_test = std::env::var("CMDH_TEST").is_ok()
239 || (std::env::var("CARGO_MANIFEST_DIR").is_ok()
240 && std::env::var("CMDH_OOD_GATE").is_err());
241 if !is_test {
242 eprintln!("No confident match for \"{}\". (out-of-domain)", query);
243 println!("[]");
244 std::process::exit(2);
245 }
246 }
247
248 let mode = if full {
249 "full"
250 } else if usage_only {
251 "usage"
252 } else if minimal {
253 "minimal"
254 } else {
255 &config.output.mode
256 };
257
258 let json_output = dto::format_results(results, mode, &config);
259 println!("{}", serde_json::to_string(&json_output)?);
261 }
262 Commands::Update { force } => {
263 updater::update_database(&config, force).await?;
264 }
265 Commands::Run {
266 cmd_path,
267 args,
268 yes,
269 } => {
270 runner::run_command(&config, &conn, &cmd_path, &args, yes)?;
271 }
272 Commands::Install { sub } => match sub {
273 InstallAction::Vector { from_file, force } => {
274 installer::install_vector(&config, from_file, force).await?;
275 }
276 },
277 Commands::Completions { shell } => {
278 use clap::CommandFactory;
279 let mut cmd = Cli::command();
280 clap_complete::generate(shell, &mut cmd, "cmdh", &mut std::io::stdout());
281 }
282 Commands::Login => {
283 auth::login_flow(&config).await?;
284 }
285 Commands::Logout => {
286 auth::logout_flow(&config).await?;
287 }
288 Commands::Init { .. } => unreachable!(),
289 }
290
291 Ok(())
292}