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