1use anyhow::{Context, Result};
2use cmdhub_shared::{AciCommandContract, CmdHubError, DbAciRecord, RiskLevel};
3use rusqlite::Connection;
4use std::io::{self, Write};
5use std::process::{Command, Stdio};
6
7pub fn get_command_by_path(conn: &Connection, cmd_path: &str) -> Result<AciCommandContract> {
8 let mut stmt = conn.prepare(
9 "SELECT \
10 arg.app_id, \
11 app.name, \
12 arg.cmd_path, \
13 arg.node_type, \
14 arg.description, \
15 arg.risk_level, \
16 arg.example_template, \
17 app.install_instructions \
18 FROM arguments arg \
19 JOIN apps app ON arg.app_id = app.app_id \
20 WHERE arg.cmd_path = ?1",
21 )?;
22
23 let record = stmt
24 .query_row([cmd_path], |row| {
25 Ok(DbAciRecord {
26 app_id: row.get(0)?,
27 name: row.get(1)?,
28 cmd_path: row.get(2)?,
29 node_type: row.get(3)?,
30 description: row.get(4)?,
31 risk_level: row.get(5)?,
32 example_template: row.get(6)?,
33 install_instructions: row.get(7)?,
34 })
35 })
36 .context("Command path not found in database")?;
37
38 AciCommandContract::try_from(record).map_err(|e| anyhow::anyhow!(e))
39}
40
41pub fn run_command(
42 conn: &Connection,
43 cmd_path: &str,
44 args: &[String],
45 skip_gating: bool,
46) -> Result<()> {
47 let contract = get_command_by_path(conn, cmd_path)?;
48
49 if contract.risk_level == RiskLevel::Dangerous && !skip_gating {
51 use std::io::IsTerminal;
52 if std::env::var("CMD_TEST").is_ok() || !std::io::stdin().is_terminal() {
53 return Err(anyhow::anyhow!(CmdHubError::ExecutionBlocked {
54 risk_level: "dangerous".to_string(),
55 command: contract.cmd_path.clone(),
56 }));
57 }
58
59 eprintln!("\x1b[31;1m[WARNING] RISK LEVEL IS DANGEROUS!\x1b[0m");
60 eprintln!("\x1b[31;1mThis command may have destructive side effects, file deletions, or privilege escalations.\x1b[0m");
61 eprintln!("\x1b[31;1mCommand Path: {}\x1b[0m", contract.cmd_path);
62 eprintln!("\x1b[31;1mDescription: {}\x1b[0m", contract.description);
63 eprint!("Are you sure you want to execute this command? (y/yes to confirm): ");
64 io::stderr().flush()?;
65
66 let mut input = String::new();
67 io::stdin()
68 .read_line(&mut input)
69 .context("Failed to read user confirmation from standard input")?;
70 let trimmed = input.trim().to_lowercase();
71 if trimmed != "y" && trimmed != "yes" {
72 return Err(anyhow::anyhow!(CmdHubError::ExecutionBlocked {
73 risk_level: "dangerous".to_string(),
74 command: contract.cmd_path.clone(),
75 }));
76 }
77 }
78
79 let executable = &contract.name;
81
82 let mut child = Command::new(executable)
84 .args(args)
85 .stdin(Stdio::inherit())
86 .stdout(Stdio::inherit())
87 .stderr(Stdio::inherit())
88 .spawn()
89 .with_context(|| format!("Failed to spawn command '{}'", executable))?;
90
91 let exit_status = child
92 .wait()
93 .context("Failed to wait for child process execution")?;
94
95 if !exit_status.success() {
96 if let Some(code) = exit_status.code() {
97 return Err(anyhow::anyhow!("Process exited with status code: {}", code));
98 } else {
99 return Err(anyhow::anyhow!("Process terminated by signal"));
100 }
101 }
102
103 Ok(())
104}