robin/scripts/
script_runner.rs1use std::process::Command;
2use std::path::PathBuf;
3use anyhow::{Result, Context, anyhow};
4use colored::*;
5use inquire::Select;
6use serde_json;
7
8use crate::config::RobinConfig;
9use crate::utils::send_notification;
10
11pub fn run_script(script: &serde_json::Value, notify: bool) -> Result<()> {
12 let start_time = std::time::Instant::now();
13
14 match script {
15 serde_json::Value::String(cmd) => {
16 let status = if cfg!(target_os = "windows") {
17 Command::new("cmd")
18 .args(["/C", cmd])
19 .status()
20 } else {
21 Command::new("sh")
22 .arg("-c")
23 .arg(cmd)
24 .status()
25 }.with_context(|| format!("Failed to execute script: {}", cmd))?;
26
27 if notify {
28 let duration = start_time.elapsed();
29 let success = status.success();
30 let message = if success {
31 format!("Completed in {:.1}s", duration.as_secs_f32())
32 } else {
33 "Failed".to_string()
34 };
35
36 send_notification(
37 "Robin",
38 &format!("Command '{}' {}", cmd.split_whitespace().next().unwrap_or(cmd), message),
39 success,
40 )?;
41 }
42
43 if !status.success() {
44 println!("{}", "Script failed!".red());
45 return Err(anyhow!("Script failed: {}", cmd));
46 }
47 Ok(())
48 },
49 serde_json::Value::Array(commands) => {
50 for cmd in commands {
51 if let Some(cmd_str) = cmd.as_str() {
52 let status = if cfg!(target_os = "windows") {
53 Command::new("cmd")
54 .args(["/C", cmd_str])
55 .status()
56 } else {
57 Command::new("sh")
58 .arg("-c")
59 .arg(cmd_str)
60 .status()
61 }.with_context(|| format!("Failed to execute script: {}", cmd_str))?;
62
63 if !status.success() {
64 println!("{}", format!("Script failed: {}", cmd_str).red());
65 return Err(anyhow!("Script failed: {}", cmd_str));
66 }
67 }
68 }
69
70 if notify {
71 let duration = start_time.elapsed();
72 send_notification(
73 "Robin",
74 &format!("Command sequence completed in {:.1}s", duration.as_secs_f32()),
75 true,
76 )?;
77 }
78 Ok(())
79 },
80 _ => Err(anyhow!("Invalid script type: must be string or array of strings")),
81 }
82}
83
84pub fn list_commands(config_path: &PathBuf) -> Result<()> {
85 let config = RobinConfig::load(config_path)
86 .with_context(|| "No .robin.json found. Run 'robin init' first")?;
87
88 let max_len = config.scripts.keys()
90 .map(|name| name.len())
91 .max()
92 .unwrap_or(0);
93
94 let mut commands: Vec<_> = config.scripts.iter().collect();
96 commands.sort_by(|a, b| a.0.cmp(b.0));
97
98 for (name, script) in commands {
99 match script {
100 serde_json::Value::String(cmd) => {
101 println!("==> {:<width$} # {}", name.blue(), cmd, width = max_len);
102 },
103 serde_json::Value::Array(commands) => {
104 println!("==> {:<width$} # [", name.blue(), width = max_len);
105 for cmd in commands {
106 if let Some(cmd_str) = cmd.as_str() {
107 println!(" {}", cmd_str);
108 }
109 }
110 println!(" ]");
111 },
112 _ => println!("==> {:<width$} # <invalid script type>", name.blue(), width = max_len),
113 }
114 }
115
116 Ok(())
117}
118
119pub fn interactive_mode(config_path: &PathBuf) -> Result<()> {
120 let config = RobinConfig::load(config_path)
121 .with_context(|| "No .robin.json found. Run 'robin init' first")?;
122
123 let commands: Vec<String> = config.scripts.keys().cloned().collect();
124 if commands.is_empty() {
125 println!("{}", "No commands available".red());
126 return Ok(());
127 }
128
129 let selection = Select::new("Select a command to run:", commands).prompt()?;
130 if let Some(script) = config.scripts.get(&selection) {
131 run_script(script, false)?;
132 }
133
134 Ok(())
135}