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