A lightweight CLI tool that connects to a remote server over SSH and executes PM2 process manager commands.
at main 148 lines 4.4 kB view raw
1use std::{io::Read, net::TcpStream, path::Path}; 2 3use anyhow::Error; 4use base64::{Engine, engine::general_purpose}; 5use log::info; 6use owo_colors::OwoColorize; 7use regex::Regex; 8use ssh2::Session; 9 10pub fn run_pm2_command( 11 host: &str, 12 port: u32, 13 key: &str, 14 cmd: &str, 15 args: Vec<&String>, 16) -> Result<(), Error> { 17 let parts: Vec<&str> = host.split('@').collect(); 18 if parts.len() != 2 { 19 return Err(Error::msg("Host must be in the format user@host")); 20 } 21 let username = parts[0]; 22 23 let host = parts[1]; 24 25 let ssh_url = format!("{}@{}:{}", username, host, port); 26 info!( 27 "Connecting to {} using key {} ...", 28 ssh_url.bright_cyan(), 29 key.bright_cyan() 30 ); 31 32 let tcp = TcpStream::connect(format!("{}:{}", host, port))?; 33 let mut session = Session::new()?; 34 session.set_tcp_stream(tcp); 35 session.handshake()?; 36 37 let private_key_path = shellexpand::tilde(key).to_string(); 38 let private_key = Path::new(&private_key_path); 39 session.userauth_pubkey_file(username, None, private_key, None)?; 40 if !session.authenticated() { 41 return Err(Error::msg("SSH authentication failed")); 42 } 43 44 info!("Connected to {} successfully!", ssh_url.bright_cyan()); 45 46 let mut channel = session.channel_session()?; 47 let command = format!( 48 "NO_NEOFETCH=1 bash -lic 'pm2 {} {}'", 49 cmd, 50 args.iter() 51 .map(|s| s.as_str()) 52 .collect::<Vec<&str>>() 53 .join(" ") 54 ); 55 56 info!("Executing command: {}", command.bright_yellow()); 57 58 channel.request_pty("xterm", None, None)?; 59 60 setup_pm2(&session)?; 61 62 channel.exec(&command)?; 63 64 let mut buffer = [0; 1024]; 65 66 // Stream stdout in real-time 67 loop { 68 match channel.read(&mut buffer) { 69 Ok(0) => break, // EOF, command finished 70 Ok(n) => { 71 let output = String::from_utf8_lossy(&buffer[..n]); 72 let clean_output = clean_terminal_noises(&output); 73 print!("{}", clean_output); // Real-time output 74 } 75 Err(e) => { 76 eprintln!("Error reading from channel: {}", e); 77 break; 78 } 79 } 80 } 81 82 channel.wait_close()?; 83 info!("Exit status: {}", channel.exit_status()?); 84 85 Ok(()) 86} 87 88pub fn setup_pm2(session: &Session) -> Result<(), Error> { 89 let mut channel = session.channel_session()?; 90 channel.request_pty("xterm", None, None)?; 91 92 info!("Setting up PM2 on the remote server..."); 93 94 let install_script = r#"#!/bin/bash 95 set -e 96 97 if ! command -v pm2 &> /dev/null; then 98 echo "PM2 not found, installing..." 99 curl https://mise.run | sh 100 export PATH=$HOME/.local/bin:$PATH 101 export PATH=$HOME/.local/share/mise/shims:$PATH 102 echo 'export PATH=$HOME/.local/bin:$PATH' >> ~/.bashrc 103 echo 'export PATH=$HOME/.local/share/mise/shims:$PATH' >> ~/.bashrc 104 mise install node 105 mise use -g node 106 npm install -g pm2 107 fi 108 "#; 109 110 let encoded_script = general_purpose::STANDARD.encode(install_script); 111 112 let remote_command = format!( 113 "echo {} | base64 -d > /tmp/install-pm2.sh && chmod +x /tmp/install-pm2.sh && /tmp/install-pm2.sh", 114 encoded_script 115 ); 116 117 channel.exec(&format!("NO_NEOFETCH=1 bash -lic '{}'", remote_command))?; 118 119 let mut buffer = [0; 1024]; 120 loop { 121 match channel.read(&mut buffer) { 122 Ok(0) => break, // EOF, script finished 123 Ok(n) => { 124 let output = String::from_utf8_lossy(&buffer[..n]); 125 let clean_output = clean_terminal_noises(&output); 126 print!("{}", clean_output); // Real-time output 127 } 128 Err(e) => { 129 eprintln!("Error reading from channel: {}", e); 130 break; 131 } 132 } 133 } 134 135 info!("PM2 setup completed successfully!"); 136 137 Ok(()) 138} 139 140fn clean_terminal_noises(s: &str) -> String { 141 let osc_re = Regex::new(r"\x1b\][^\x07\x1b]*(\x07|\x1b\\)").unwrap(); 142 let csi_dsr_re = Regex::new(r"\x1b\[\d+;\d+R").unwrap(); 143 let malformed_csi_re = Regex::new(r"\[{1,2}\d{1,3}(;\d{1,3})?R").unwrap(); 144 let cleaned = osc_re.replace_all(s, ""); 145 let cleaned = csi_dsr_re.replace_all(&cleaned, ""); 146 let cleaned = malformed_csi_re.replace_all(&cleaned, ""); 147 cleaned.into_owned() 148}