1use anyhow::Result;
2use clap::{Parser, Subcommand};
3
4pub mod config;
5pub mod db;
6pub mod dto;
7pub mod inference;
8pub mod installer;
9pub mod os_detector;
10pub mod runner;
11pub mod tokenizer;
12pub mod updater;
13
14#[derive(Parser, Debug)]
15#[command(
16 name = "cmdh",
17 about = "cmdh — the CmdHub CLI client for offline command search and execution",
18 version
19)]
20pub struct Cli {
21 #[arg(short, long, global = true, help = "Custom configuration file path")]
23 pub config: Option<std::path::PathBuf>,
24
25 #[command(subcommand)]
26 pub command: Commands,
27}
28
29#[derive(Subcommand, Debug, Clone)]
30pub enum Commands {
31 Search {
33 query: String,
35 #[arg(long, default_value_t = 1)]
37 limit: usize,
38 #[arg(short, long, group = "output_format")]
40 full: bool,
41 #[arg(short, long, group = "output_format")]
43 usage_only: bool,
44 #[arg(short, long, group = "output_format")]
46 minimal: bool,
47 },
48 Update {
50 #[arg(long)]
52 force: bool,
53 },
54 Run {
56 cmd_path: String,
58 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
60 args: Vec<String>,
61 #[arg(short, long)]
63 yes: bool,
64 },
65 Install {
67 #[command(subcommand)]
68 sub: InstallAction,
69 },
70 Init {
72 #[arg(long)]
74 force: bool,
75 },
76}
77
78#[derive(Subcommand, Debug, Clone)]
79pub enum InstallAction {
80 Vector {
82 #[arg(long, value_name = "FILE")]
84 from_file: Option<std::path::PathBuf>,
85 #[arg(long)]
87 force: bool,
88 },
89}
90
91pub async fn run() -> Result<()> {
92 let cli = Cli::parse();
94
95 if let Commands::Init { force } = cli.command {
97 let config_dir = config::get_config_dir();
98 let config_path = config_dir.join("config.toml");
99
100 if config_path.exists() && !force {
101 eprintln!(
102 "Warning: Configuration file already exists at {:?}",
103 config_path
104 );
105 return Ok(());
106 }
107
108 std::fs::create_dir_all(&config_dir)?;
109
110 let detected = os_detector::detect_os().unwrap_or_else(|| "unknown".to_string());
111 let config_content = format!(
112 r#"# CmdHub configuration file
113api_url = "https://api.cmdhub.xyz"
114
115[output]
116# Set the format of the search results output to stdout.
117# Supported modes:
118# - "full" : Returns the full command contract including descriptions, risks, and install commands.
119# - "usage" : Returns a slim template format focusing purely on path and execution usage structure.
120# - "minimal": Returns only the command pathway (e.g. [{{"cmd_path":"git"}}]).
121mode = "full"
122
123[install]
124# Host operating system override.
125# Detected on your platform as: "{detected}"
126# To override manually, uncomment the line below:
127# os = "{detected}"
128
129# Priority sequence when searching for package manager installer instructions.
130# The resolver checks system installers first (matching your OS release),
131# then traverses these developer packages in order.
132package_managers = ["uv", "npm", "cargo", "go"]
133"#
134 );
135
136 std::fs::write(&config_path, config_content)?;
137 println!(
138 "Configuration initialized successfully at {:?}",
139 config_path
140 );
141 return Ok(());
142 }
143
144 let config = config::load_or_create_config(cli.config.clone())?;
146
147 let conn = db::open_db()?;
149 db::init_db(&conn)?;
150
151 match cli.command {
152 Commands::Search {
153 query,
154 limit,
155 full,
156 usage_only,
157 minimal,
158 } => {
159 let default_path = config::get_data_dir().join("models/bge-micro-v2.onnx");
160 let model_path = config
161 .vector
162 .model_path
163 .as_ref()
164 .map(std::path::PathBuf::from)
165 .unwrap_or(default_path);
166
167 let mut query_vector = None;
168 if model_path.exists() {
169 if let Ok(model) = inference::EmbeddingModel::load(&model_path) {
170 let tokenizer = tokenizer::Tokenizer::new();
171 let (ids, mask) = tokenizer.tokenize_query(&query);
172 if let Ok(vec) = model.generate_embedding(&ids, &mask) {
173 query_vector = Some(vec);
174 }
175 }
176 } else {
177 eprintln!(
178 "Tip: Semantic search is inactive. Run 'cmdh install vector' to activate."
179 );
180 }
181
182 let results = db::search_all(&conn, &query, query_vector.as_deref(), limit)?;
183
184 let mode = if full {
185 "full"
186 } else if usage_only {
187 "usage"
188 } else if minimal {
189 "minimal"
190 } else {
191 &config.output.mode
192 };
193
194 let json_output = dto::format_results(results, mode, &config);
195 println!("{}", serde_json::to_string(&json_output)?);
197 }
198 Commands::Update { force } => {
199 updater::update_database(&config, force).await?;
200 }
201 Commands::Run {
202 cmd_path,
203 args,
204 yes,
205 } => {
206 runner::run_command(&conn, &cmd_path, &args, yes)?;
207 }
208 Commands::Install { sub } => match sub {
209 InstallAction::Vector { from_file, force } => {
210 installer::install_vector(&config, from_file, force).await?;
211 }
212 },
213 Commands::Init { .. } => unreachable!(),
214 }
215
216 Ok(())
217}