Skip to main content

cmdhub_cli/
runner.rs

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    // Safety Gate
50    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    // Prepare executable
80    let executable = &contract.name;
81
82    // Spawn the subprocess
83    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}