1use crate::config::Config;
2use anyhow::{Context, Result};
3use cmdhub_shared::{AciCommandContract, CmdHubError, DbAciRecord, RiskLevel};
4use rusqlite::Connection;
5use std::io::{self, Write};
6use std::process::{Command, Stdio};
7
8pub fn get_command_by_path(conn: &Connection, cmd_path: &str) -> Result<AciCommandContract> {
9 let prov = crate::db::provenance_expr(conn);
10 let mut stmt = conn.prepare(&format!(
11 "SELECT \
12 arg.app_id, \
13 app.name, \
14 arg.cmd_path, \
15 arg.node_type, \
16 arg.description, \
17 arg.risk_level, \
18 arg.example_template, \
19 app.os_aliases, \
20 app.install_instructions, \
21 app.popularity, \
22 arg.docker_image, \
23 arg.script_url, \
24 arg.source_url, \
25 {prov} \
26 FROM arguments arg \
27 JOIN apps app ON arg.app_id = app.app_id \
28 WHERE arg.cmd_path = ?1"
29 ))?;
30
31 let record = stmt
32 .query_row([cmd_path], |row| {
33 Ok(DbAciRecord {
34 app_id: row.get(0)?,
35 name: row.get(1)?,
36 cmd_path: row.get(2)?,
37 node_type: row.get(3)?,
38 description: row.get(4)?,
39 risk_level: row.get(5)?,
40 example_template: row.get(6)?,
41 os_aliases: row.get(7)?,
42 install_instructions: row.get(8)?,
43 popularity: row.get(9)?,
44 docker_image: row.get(10)?,
45 script_url: row.get(11)?,
46 source_url: row.get(12)?,
47 provenance: row.get(13)?,
48 })
49 })
50 .context("Command path not found in database")?;
51
52 AciCommandContract::try_from(record).map_err(|e| anyhow::anyhow!(e))
53}
54
55pub fn run_command(
56 config: &Config,
57 conn: &Connection,
58 cmd_path: &str,
59 args: &[String],
60 skip_gating: bool,
61) -> Result<()> {
62 let contract = get_command_by_path(conn, cmd_path)?;
63
64 if contract.risk_level == RiskLevel::Dangerous && !skip_gating {
66 match config.risk_guard_level.as_str() {
67 "allow" => {
68 }
70 "block" => {
71 return Err(anyhow::anyhow!(CmdHubError::ExecutionBlocked {
72 risk_level: "dangerous".to_string(),
73 command: contract.cmd_path.clone(),
74 }));
75 }
76 _ => {
77 use std::io::IsTerminal;
79 if std::env::var("CMD_TEST").is_ok() || !std::io::stdin().is_terminal() {
80 return Err(anyhow::anyhow!(CmdHubError::ExecutionBlocked {
81 risk_level: "dangerous".to_string(),
82 command: contract.cmd_path.clone(),
83 }));
84 }
85
86 eprintln!("\x1b[31;1m[WARNING] RISK LEVEL IS DANGEROUS!\x1b[0m");
87 eprintln!("\x1b[31;1mThis command may have destructive side effects, file deletions, or privilege escalations.\x1b[0m");
88 eprintln!("\x1b[31;1mCommand Path: {}\x1b[0m", contract.cmd_path);
89 eprintln!("\x1b[31;1mDescription: {}\x1b[0m", contract.description);
90 eprint!("Are you sure you want to execute this command? (y/yes to confirm): ");
91 io::stderr().flush()?;
92
93 let mut input = String::new();
94 io::stdin()
95 .read_line(&mut input)
96 .context("Failed to read user confirmation from standard input")?;
97 let trimmed = input.trim().to_lowercase();
98 if trimmed != "y" && trimmed != "yes" {
99 return Err(anyhow::anyhow!(CmdHubError::ExecutionBlocked {
100 risk_level: "dangerous".to_string(),
101 command: contract.cmd_path.clone(),
102 }));
103 }
104 }
105 }
106 }
107
108 let executable = crate::dto::resolve_binary_name(&contract, config);
110
111 if !crate::dto::check_is_installed(&contract, config) {
113 eprintln!(
114 "Warning: command '{}' is not installed locally.",
115 executable
116 );
117 if let Some(install_cmd) = crate::dto::resolve_install_command(&contract, config) {
118 eprintln!("To install, run: {}", install_cmd);
119 }
120 }
121
122 let mut child = Command::new(&executable)
124 .args(args)
125 .stdin(Stdio::inherit())
126 .stdout(Stdio::inherit())
127 .stderr(Stdio::inherit())
128 .spawn()
129 .with_context(|| format!("Failed to spawn command '{}'", executable))?;
130
131 let exit_status = child
132 .wait()
133 .context("Failed to wait for child process execution")?;
134
135 if !exit_status.success() {
136 if let Some(code) = exit_status.code() {
137 return Err(anyhow::anyhow!("Process exited with status code: {}", code));
138 } else {
139 return Err(anyhow::anyhow!("Process terminated by signal"));
140 }
141 }
142
143 Ok(())
144}