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 runner;
12pub mod tokenizer;
13pub mod updater;
14
15#[derive(Parser, Debug)]
16#[command(
17 name = "cmdh",
18 about = "cmdh — the CmdHub CLI client for offline command search and execution",
19 version
20)]
21pub struct Cli {
22 #[arg(short, long, global = true, help = "Custom configuration file path")]
24 pub config: Option<std::path::PathBuf>,
25
26 #[command(subcommand)]
27 pub command: Commands,
28}
29
30#[derive(Subcommand, Debug, Clone)]
31pub enum Commands {
32 Search {
34 query: String,
36 #[arg(long, default_value_t = 1)]
38 limit: usize,
39 #[arg(short, long, group = "output_format")]
41 full: bool,
42 #[arg(short, long, group = "output_format")]
44 usage_only: bool,
45 #[arg(short, long, group = "output_format")]
47 minimal: bool,
48 },
49 Update {
51 #[arg(long)]
53 force: bool,
54 },
55 Run {
57 cmd_path: String,
59 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
61 args: Vec<String>,
62 #[arg(short, long)]
64 yes: bool,
65 },
66 Install {
68 #[command(subcommand)]
69 sub: InstallAction,
70 },
71 Init {
73 #[arg(long)]
75 force: bool,
76 },
77 Completions {
79 #[arg(value_enum)]
81 shell: clap_complete::Shell,
82 },
83 Login,
85 Logout,
87}
88
89#[derive(Subcommand, Debug, Clone)]
90pub enum InstallAction {
91 Vector {
93 #[arg(long, value_name = "FILE")]
95 from_file: Option<std::path::PathBuf>,
96 #[arg(long)]
98 force: bool,
99 },
100}
101
102pub async fn run() -> Result<()> {
103 let cli = Cli::parse();
105
106 if let Commands::Init { force } = cli.command {
108 let config_dir = config::get_config_dir();
109 let config_path = config_dir.join("config.toml");
110
111 if config_path.exists() && !force {
112 eprintln!(
113 "Warning: Configuration file already exists at {:?}",
114 config_path
115 );
116 return Ok(());
117 }
118
119 std::fs::create_dir_all(&config_dir)?;
120
121 let detected = os_detector::detect_os().unwrap_or_else(|| "unknown".to_string());
122 let default_key: String = config::OFFICIAL_PUBLIC_KEY
123 .iter()
124 .map(|b| format!("{:02x}", b))
125 .collect();
126 let config_content = format!(
127 r#"# CmdHub configuration file
128api_url = "https://api.cmdhub.xyz"
129public_key = "{default_key}"
130timeout_seconds = 30
131
132[output]
133# Set the format of the search results output to stdout.
134# Supported modes:
135# - "full" : Returns the full command contract including descriptions, risks, and install commands.
136# - "usage" : Returns a slim template format focusing purely on path and execution usage structure.
137# - "minimal": Returns only the command pathway (e.g. [{{"cmd_path":"git"}}]).
138mode = "full"
139
140[install]
141# Host operating system override.
142# Detected on your platform as: "{detected}"
143# To override manually, uncomment the line below:
144# os = "{detected}"
145
146# Priority sequence when searching for package manager installer instructions.
147# The resolver checks system installers first (matching your OS release),
148# then traverses these developer packages in order.
149package_managers = ["uv", "npm", "cargo", "go"]
150"#
151 );
152
153 std::fs::write(&config_path, config_content)?;
154 println!(
155 "Configuration initialized successfully at {:?}",
156 config_path
157 );
158 return Ok(());
159 }
160
161 let config = config::load_or_create_config(cli.config.clone())?;
163
164 let conn = db::open_db()?;
166 db::init_db(&conn)?;
167
168 match cli.command {
169 Commands::Search {
170 query,
171 limit,
172 full,
173 usage_only,
174 minimal,
175 } => {
176 let mut query_vector = None;
177 match installer::ensure_model_installed(&config).await {
178 Ok(model_path) => {
179 if let Ok(model) = inference::EmbeddingModel::load(&model_path) {
180 let tokenizer = tokenizer::Tokenizer::new();
181 let (ids, mask) = tokenizer.tokenize_query(&query);
182 if let Ok(vec) = model.generate_embedding(&ids, &mask) {
183 query_vector = Some(vec);
184 }
185 }
186 }
187 Err(e) => {
188 eprintln!(
189 "Warning: Local semantic search is inactive ({}). Falling back to full-text search.",
190 e
191 );
192 }
193 }
194
195 let results = db::search_all(&conn, &query, query_vector.as_deref(), limit)?;
196
197 let mode = if full {
198 "full"
199 } else if usage_only {
200 "usage"
201 } else if minimal {
202 "minimal"
203 } else {
204 &config.output.mode
205 };
206
207 let json_output = dto::format_results(results, mode, &config);
208 println!("{}", serde_json::to_string(&json_output)?);
210 }
211 Commands::Update { force } => {
212 updater::update_database(&config, force).await?;
213 }
214 Commands::Run {
215 cmd_path,
216 args,
217 yes,
218 } => {
219 runner::run_command(&conn, &cmd_path, &args, yes)?;
220 }
221 Commands::Install { sub } => match sub {
222 InstallAction::Vector { from_file, force } => {
223 installer::install_vector(&config, from_file, force).await?;
224 }
225 },
226 Commands::Completions { shell } => {
227 use clap::CommandFactory;
228 let mut cmd = Cli::command();
229 clap_complete::generate(shell, &mut cmd, "cmdh", &mut std::io::stdout());
230 }
231 Commands::Login => {
232 auth::login_flow(&config).await?;
233 }
234 Commands::Logout => {
235 auth::logout_flow(&config).await?;
236 }
237 Commands::Init { .. } => unreachable!(),
238 }
239
240 Ok(())
241}