A lightweight CLI tool that connects to a remote server over SSH and executes PM2 process manager commands.

Merge pull request #1 from tsirysndr/pm2-auto-install

feat: auto install node.js and pm2 if not installed

authored by tsiry-sandratraina.com and committed by

GitHub d1954a38 10522d31

+82 -3
+9 -1
Cargo.lock
··· 74 74 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 75 75 76 76 [[package]] 77 + name = "base64" 78 + version = "0.22.1" 79 + source = "registry+https://github.com/rust-lang/crates.io-index" 80 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 81 + 82 + [[package]] 77 83 name = "bitflags" 78 84 version = "2.9.1" 79 85 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 337 343 338 344 [[package]] 339 345 name = "pm22" 340 - version = "0.1.0" 346 + version = "0.2.0" 341 347 dependencies = [ 342 348 "anyhow", 349 + "base64", 343 350 "clap", 344 351 "env_logger", 345 352 "log", 346 353 "owo-colors", 354 + "regex", 347 355 "shell-escape", 348 356 "shellexpand", 349 357 "ssh2",
+3 -1
Cargo.toml
··· 1 1 [package] 2 2 name = "pm22" 3 - version = "0.1.0" 3 + version = "0.2.0" 4 4 edition = "2024" 5 5 description = "A lightweight CLI tool that connects to a remote server over SSH and executes PM2 process manager commands." 6 6 license = "MIT" ··· 12 12 13 13 [dependencies] 14 14 anyhow = "1.0.98" 15 + base64 = "0.22.1" 15 16 clap = "4.5.40" 16 17 env_logger = "0.11.8" 17 18 log = "0.4.27" 18 19 owo-colors = "4.2.1" 20 + regex = "1.11.1" 19 21 shell-escape = "0.1.5" 20 22 shellexpand = "3.1.1" 21 23 ssh2 = "0.9.5"
+1
README.md
··· 15 15 ## ✨ Features 16 16 17 17 - SSH into any server with your private key 18 + - Auto install Node.js and PM2 if not found 18 19 - Execute any PM2 command remotely (`start`, `stop`, `restart`, `delete`, `logs`, etc.) 19 20 - Supports custom ports and SSH keys 20 21 - Optional verbose output with `--verbose`
+69 -1
src/pm2.rs
··· 1 1 use std::{io::Read, net::TcpStream, path::Path}; 2 2 3 3 use anyhow::Error; 4 + use base64::{engine::general_purpose, Engine}; 4 5 use log::info; 5 6 use owo_colors::OwoColorize; 7 + use regex::Regex; 6 8 use ssh2::Session; 7 9 8 10 pub fn run_pm2_command( ··· 54 56 info!("Executing command: {}", command.bright_yellow()); 55 57 56 58 channel.request_pty("xterm", None, None)?; 59 + 60 + setup_pm2(&session)?; 61 + 57 62 channel.exec(&command)?; 58 63 59 64 let mut buffer = [0; 1024]; ··· 64 69 Ok(0) => break, // EOF, command finished 65 70 Ok(n) => { 66 71 let output = String::from_utf8_lossy(&buffer[..n]); 67 - print!("{}", output); // Real-time output 72 + let clean_output = clean_terminal_noises(&output); 73 + print!("{}", clean_output); // Real-time output 68 74 } 69 75 Err(e) => { 70 76 eprintln!("Error reading from channel: {}", e); ··· 78 84 79 85 Ok(()) 80 86 } 87 + 88 + pub 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 + 140 + fn 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 + }