From dbcef96bc24480a9d723ab7bab11b9decf70605b Mon Sep 17 00:00:00 2001
From: Your Name <j@842.be>
Date: Sat, 10 May 2025 18:19:02 +1200
Subject: [PATCH] EXPERIMENTAL

---
 src/server_env_manager.cpp |  18 +-
 src/service_runner.cpp     |  99 ++++---
 src/utils/runner.cpp       | 573 ++++++++++++++++---------------------
 src/utils/runner.hpp       |  77 +++--
 4 files changed, 378 insertions(+), 389 deletions(-)

diff --git a/src/server_env_manager.cpp b/src/server_env_manager.cpp
index dce24d5..38bd9d3 100644
--- a/src/server_env_manager.cpp
+++ b/src/server_env_manager.cpp
@@ -134,11 +134,13 @@ std::string server_env_manager::get_variable(const std::string& name) const {
 
 bool server_env_manager::check_remote_dir_exists(const std::string &dir_path) const
 {
-    return 0==runner::execute_cmd("test",{"-d", quote(dir_path)}, {}, {}, true, false, get_SSH_INFO());
+    runner::runner_ssh test_runner(get_SSH_INFO(),"test",{"-d", quote(dir_path)});
+    return test_runner.execute();
 }
 
 bool server_env_manager::check_remote_file_exists(const std::string& file_path) const {
-    return 0==runner::execute_cmd("test",{"-f",quote(file_path)}, {}, {}, true, false, get_SSH_INFO());
+    runner::runner_ssh test_runner(get_SSH_INFO(),"test",{"-f",quote(file_path)});
+    return test_runner.execute();
 }
 
 bool server_env_manager::check_remote_items_exist(const std::vector<std::string> &file_paths) const
@@ -149,8 +151,8 @@ bool server_env_manager::check_remote_items_exist(const std::vector<std::string>
         file_paths_str += quote(file_path) + " ";
     }
     // check if all items in the vector exist on the remote server, in a single command.
-
-    return 0==runner::execute_cmd("bash",{"-c","for item in " + file_paths_str + "; do test -f $item; done"}, {}, {}, true, false, get_SSH_INFO());
+    runner::runner_ssh test_runner(get_SSH_INFO(),"bash",{"-c","for item in " + file_paths_str + "; do test -f $item; done"});
+    return test_runner.execute();
 }
 
 bool server_env_manager::run_remote_template_command(const std::string &service_name, const std::string &command, std::vector<std::string> args, bool silent, std::map<std::string, std::string> extra_env_vars, std::string * output) const
@@ -177,7 +179,13 @@ bool server_env_manager::run_remote_template_command(const std::string &service_
     ASSERT(!output || !silent);  // if output is captured, silent must be false
     ASSERT(!interactive || !silent);  // if command is ssh, silent must be false
 
-    return 0==runner::execute_cmd("bash",{"-c", quote(script_path) + argstr}, working_dir, env_vars, silent, interactive, get_SSH_INFO(), output);
+    if (interactive) {
+        runner::runner_ssh_interactive bash_runner(get_SSH_INFO(),"bash",{"-c", quote(script_path) + argstr}, working_dir, env_vars);  
+        return bash_runner.execute();
+    } else {
+        runner::runner_ssh bash_runner(get_SSH_INFO(),"bash",{"-c", quote(script_path) + argstr}, working_dir, env_vars, silent);  
+        return bash_runner.execute();
+    }
 }
 
 // base64 <<< "FOO=BAR WHEE=YAY bash ./test.sh"
diff --git a/src/service_runner.cpp b/src/service_runner.cpp
index e78ca7b..caed78a 100644
--- a/src/service_runner.cpp
+++ b/src/service_runner.cpp
@@ -52,18 +52,23 @@ bool service_runner::install(bool silent) {
     if (!tinfo.is_set())
         return false;
         
-    // Create service directory
-    if (0!=runner::execute_cmd("mkdir",{"-p",remotepath::service(mServer, mService)},"",{},true,false,mServerEnv.get_SSH_INFO()))
-    {
-        std::cerr << "Failed to create service directory " << remotepath::service(mServer, mService) << std::endl;
-        return false;
+
+    { // Create service directory
+        runner::runner_ssh mkdir_runner(mServerEnv.get_SSH_INFO(),"mkdir",{"-p",remotepath::service(mServer, mService)});
+        if (!mkdir_runner.execute())
+        {
+            std::cerr << "Failed to create service directory " << remotepath::service(mServer, mService) << std::endl;
+            return false;
+        }
     }
-    
-    // Check if rsync is installed on remote host
-    if (0!=runner::execute_cmd("which",{"rsync"},"",{},true,false,mServerEnv.get_SSH_INFO()))
-    {
-        std::cerr << "rsync is not installed on the remote host" << std::endl;
-        return false;
+
+    { // Check if rsync is installed on remote host
+        runner::runner_ssh which_runner(mServerEnv.get_SSH_INFO(),"which",{"rsync"});
+        if (!which_runner.execute())
+        {
+            std::cerr << "rsync is not installed on the remote host" << std::endl;
+            return false;
+        }
     }
 
     // make sure all shell files are executable
@@ -125,8 +130,11 @@ bool service_runner::uninstall(bool silent) {
     }
     
     // 4. Remove the service directory from the server
-    if (0!=runner::execute_cmd("rm -rf " + quote(remotepath::service(mServer, mService)), {}, "", {}, true, false, mServerEnv.get_SSH_INFO()))
-        std::cerr << "Failed to remove remote service directory at " << remotepath::service(mServer, mService) << std::endl;
+    {
+        runner::runner_ssh rm_runner(mServerEnv.get_SSH_INFO(),"rm",{"-rf",remotepath::service(mServer, mService)});
+        if (!rm_runner.execute())
+            std::cerr << "Failed to remove remote service directory at " << remotepath::service(mServer, mService) << std::endl;
+    }
 
     std::cout << "Service " << mService << " successfully uninstalled from " << mServer << std::endl;
     return true;
@@ -170,8 +178,11 @@ bool service_runner::fullnuke()
         return false;
     }  
     
-    if (0!=runner::execute_cmd("rm -rf " + quote(local_service_path), {}, "", {}, true, false, mServerEnv.get_SSH_INFO()))
-        std::cerr << "Failed to remove local service directory at " << local_service_path << std::endl;
+    {
+        runner::runner_local rm_runner("rm",{"-rf",quote(local_service_path)});
+        if (!rm_runner.execute())
+            std::cerr << "Failed to remove local service directory at " << local_service_path << std::endl;
+    }
 
     std::cout << "Service " << mService << " successfully fully nuked from " << mServer << std::endl;
     return true;
@@ -265,7 +276,6 @@ std::map<std::string, ServiceStatus> service_runner::get_all_services_status(std
 {
     std::map<std::string, ServiceStatus> status;
 
-    std::string command = "_allservicesstatus";
     std::string service_name = "dropshell-agent";
 
 
@@ -275,12 +285,15 @@ std::map<std::string, ServiceStatus> service_runner::get_all_services_status(std
         return status;
     }
 
-    std::string cmd_path = remotepath::service_template(server_name,service_name) + "/shared/";
+    std::string cmd_path = remotepath::service_template(server_name,service_name) + "/shared/_allservicesstatus.sh";
     std::string output;
-    if (0!=runner::execute_cmd("bash",{cmd_path+command+".sh"}, cmd_path, {}, true, false, env.get_SSH_INFO(), &output))
     {
-        std::cerr << "Error: Failed to run command script at " << cmd_path << std::endl;
-        return status;
+        runner::runner_ssh_capture bash_runner(env.get_SSH_INFO(),output,cmd_path);
+        if (!bash_runner.execute())
+        {
+            std::cerr << "Error: Failed to run command script at " << cmd_path << std::endl;
+            return status;
+        }
     }
 
     std::stringstream ss(output);
@@ -390,7 +403,8 @@ bool service_runner::interactive_ssh(const std::string & server_name, const std:
         std::cerr << "Error: Invalid server environment file: " << server_name << std::endl;
         return false;
     }
-    return 0==runner::execute_cmd("", {}, "", {}, false, true, env.get_SSH_INFO());
+    runner::runner_ssh_interactive ssh_runner(env.get_SSH_INFO(),command);
+    return ssh_runner.execute();
 }
 
 void service_runner::edit_server(const std::string &server_name)
@@ -438,7 +452,8 @@ bool service_runner::edit_file(const std::string &file_path)
     }
 
     std::cout << "Editing file: " << file_path << std::endl;
-    return 0==runner::execute_cmd(editor_cmd, {file_path}, "", {}, false, true, nullptr);
+    runner::runner_local_interactive editor_runner(editor_cmd,{file_path});
+    return editor_runner.execute();
 }
 
 bool service_runner::interactive_ssh_service()
@@ -544,7 +559,8 @@ bool service_runner::restore(std::string backup_file, bool silent)
         std::string remote_backup_file_path = remote_backups_dir + "/" + backup_file;
 
         // Copy backup file from local to server
-        if (0!=runner::execute_cmd("scp", {quote(local_backup_file_path), mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_backup_file_path)}, "", {}, false, false, mServerEnv.get_SSH_INFO()))
+        runner::runner_local scp_runner("scp",{quote(local_backup_file_path), mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_backup_file_path)});
+        if (!scp_runner.execute())
         {
             std::cerr << "Failed to copy backup file from server" << std::endl;
             return false;
@@ -621,11 +637,14 @@ bool service_runner::backup(bool silent) {
 
     // Create backups directory on server if it doesn't exist
     std::string remote_backups_dir = remotepath::backups(mServer);
-    if (!silent) std::cout << "Remote backups directory on "<< mServer <<": " << remote_backups_dir << std::endl;   
-    if (0!=runner::execute_cmd("mkdir", {"-p",quote(remote_backups_dir)}, "", {}, true, false, mServerEnv.get_SSH_INFO()))
+    if (!silent) std::cout << "Remote backups directory on "<< mServer <<": " << remote_backups_dir << std::endl;  
     {
-        std::cerr << "Failed to create backups directory on server" << std::endl;
-        return false;
+        runner::runner_ssh mkdir_runner(mServerEnv.get_SSH_INFO(),"mkdir",{"-p",quote(remote_backups_dir)});
+        if (!mkdir_runner.execute())
+        {
+            std::cerr << "Failed to create backups directory on server" << std::endl;
+            return false;
+        }
     }
 
     // Create backups directory locally if it doesn't exist
@@ -664,7 +683,8 @@ bool service_runner::backup(bool silent) {
         }
 
         // Copy backup file from server to local
-        if (0!=runner::execute_cmd("scp", {quote(remote_backup_file_path), quote(local_backup_file_path)}, "", {}, silent, false, mServerEnv.get_SSH_INFO()))
+        runner::runner_local scp_runner("scp",{"-P", mServerEnv.get_SSH_PORT(), mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_backup_file_path), quote(local_backup_file_path)});
+        if (!scp_runner.execute())
         {
             std::cerr << "Failed to copy backup file from server" << std::endl;
             return false;
@@ -681,15 +701,19 @@ bool service_runner::backup(bool silent) {
 cRemoteTempFolder::cRemoteTempFolder(const server_env_manager &server_env) : mServerEnv(server_env)
 {
     std::string p = remotepath::temp_files(server_env.get_server_name()) + "/" + random_alphanumeric_string(10);
-    if (0!=runner::execute_cmd("mkdir", {"-p",quote(p)}, "", {}, true, false, server_env.get_SSH_INFO()))
-        std::cerr << "Failed to create temp directory on server" << std::endl;
-    else 
-        mPath = p;
+    {
+        runner::runner_ssh mkdir_runner(server_env.get_SSH_INFO(),"mkdir",{"-p",quote(p)});
+        if (!mkdir_runner.execute())
+            std::cerr << "Failed to create temp directory on server" << std::endl;
+        else 
+            mPath = p;
+    }
 }
 
 cRemoteTempFolder::~cRemoteTempFolder()
 {
-    if (0!=runner::execute_cmd("rm", {"-rf",quote(mPath)}, "", {}, true, false, mServerEnv.get_SSH_INFO()))
+    runner::runner_ssh rm_runner(mServerEnv.get_SSH_INFO(),"rm",{"-rf",quote(mPath)});
+    if (!rm_runner.execute())
         std::cerr << "Failed to remove temp directory on server" << std::endl;
 }
 
@@ -748,11 +772,10 @@ std::string service_runner::get_latest_backup_file(const std::string& server, co
 bool service_runner::rsync_copy(const std::string& local_path, const std::string& remote_path, bool silent) {
     std::cout << "Copying: [LOCAL] " << local_path << std::endl << std::string(8,' ')<<"[REMOTE] " << remote_path << std::endl;
     
-    if (0 != runner::execute_cmd(
-        "rsync",
-        {"--delete","--mkpath","-zrpc","-e",quote("ssh -p " + mServerEnv.get_SSH_PORT()),local_path,
-        mServerEnv.get_SSH_USER()+"@"+mServerEnv.get_SSH_HOST()+":"+remote_path},
-        "", {}, true, false)) {
+    runner::runner_local rsync_runner("rsync", {"--delete","--mkpath","-zrpc","-e",quote("ssh -p " + mServerEnv.get_SSH_PORT()),local_path,
+        mServerEnv.get_SSH_USER()+"@"+mServerEnv.get_SSH_HOST()+":"+remote_path}, "", {}, true);
+
+    if (!rsync_runner.execute()) {
         std::cerr << "Failed to copy files using rsync" << std::endl;
         return false;
     }
diff --git a/src/utils/runner.cpp b/src/utils/runner.cpp
index 1761460..ff7f33e 100644
--- a/src/utils/runner.cpp
+++ b/src/utils/runner.cpp
@@ -77,364 +77,281 @@ ssh_session ssh_connect_and_auth(const sSSHInfo* sshinfo, const std::map<std::st
     return session;
 }
 
-std::string ssh_build_remote_command(const std::string& command, const std::vector<std::string>& args, const std::string& working_dir, const std::map<std::string, std::string>& env) {
-    std::ostringstream remote_cmd;
-    for (const auto& kv : env) {
-        if (kv.first == "SSHPASS") continue;
-        remote_cmd << kv.first << "='" << kv.second << "' ";
-    }
-    if (!working_dir.empty()) {
-        remote_cmd << "cd '" << working_dir << "' && ";
-    }
-    remote_cmd << command;
-    for (const auto& arg : args) {
-        remote_cmd << " " << arg;
-    }
-    return remote_cmd.str();
-}
+} // namespace runner
 
-// // Utility function to escape special shell characters
-// std::string escape_shell_arg(const std::string& arg) {
-//     std::ostringstream escaped;
-//     escaped << '"';
-//     for (char c : arg) {
-//         if (c == '"' || c == '\\' || c == '$' || c == '`') {
-//             escaped << '\\';
-//         }
-//         escaped << c;
-//     }
-//     escaped << '"';
-//     return escaped.str();
-// }
-
-// // For non-interactive SSH, just build the command with args
-// std::string ssh_build_command_only(const std::string& command, const std::vector<std::string>& args) {
-//     std::ostringstream remote_cmd;
-//     remote_cmd << command;
-//     for (const auto& arg : args) {
-//         remote_cmd << " " << arg;
-//     }
-//     return remote_cmd.str();
-// }
-
-int ssh_interactive_shell_session(ssh_session session, ssh_channel channel, const std::string& remote_cmd_str, const std::string& command, std::string* output) {
-    // Request a PTY with xterm-256color type for color support
-    // First try using default terminal settings - should work with libssh 0.9.0+
-    int rc = ssh_channel_request_pty(channel);
-    if (rc != SSH_OK) {
-        if (output) *output = std::string("Failed to request pty: ") + ssh_get_error(session);
-        return -1;
-    }
-    
-    // Try to explicitly tell the server we want colors through additional means
-    // 1. Set TERM environment variable
-    rc = ssh_channel_request_env(channel, "TERM", "xterm-256color");
-    // Ignore errors - this is optional
-    
-    // 2. Request a shell
-    rc = ssh_channel_request_shell(channel);
-    if (rc != SSH_OK) {
-        if (output) *output = std::string("Failed to request shell: ") + ssh_get_error(session);
-        return -1;
-    }
-    
-    struct termios orig_termios, raw_termios;
-    tcgetattr(STDIN_FILENO, &orig_termios);
-    raw_termios = orig_termios;
-    cfmakeraw(&raw_termios);
-    tcsetattr(STDIN_FILENO, TCSANOW, &raw_termios);
-    
-    if (!command.empty()) {
-        ssh_channel_write(channel, remote_cmd_str.c_str(), remote_cmd_str.size());
-        ssh_channel_write(channel, "\n", 1);
-    } else {
-        // Initialize bash with color support if no specific command
-        std::string init_cmd = "export TERM=xterm-256color && if [ -f ~/.bashrc ]; then source ~/.bashrc; fi";
-        ssh_channel_write(channel, init_cmd.c_str(), init_cmd.size());
-        ssh_channel_write(channel, "\n", 1);
-    }
-    
-    int maxfd = STDIN_FILENO > STDOUT_FILENO ? STDIN_FILENO : STDOUT_FILENO;
-    maxfd = maxfd > ssh_get_fd(session) ? maxfd : ssh_get_fd(session);
-    char buffer[4096];
-    bool done = false;
-    
-    while (!done) {
-        fd_set fds_read;
-        FD_ZERO(&fds_read);
-        FD_SET(STDIN_FILENO, &fds_read);
-        FD_SET(ssh_get_fd(session), &fds_read);
-        
-        int ret = select(maxfd + 1, &fds_read, nullptr, nullptr, nullptr);
-        if (ret < 0) break;
-        
-        if (FD_ISSET(STDIN_FILENO, &fds_read)) {
-            ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer));
-            if (n > 0) {
-                ssh_channel_write(channel, buffer, n);
-            } else {
-                ssh_channel_send_eof(channel);
-                done = true;
-            }
-        }
-        
-        if (FD_ISSET(ssh_get_fd(session), &fds_read)) {
-            int n = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
-            if (n > 0) {
-                write(STDOUT_FILENO, buffer, n);
-            } else if (n == 0) {
-                done = true;
-            }
-        }
-        
-        if (ssh_channel_is_closed(channel) || ssh_channel_is_eof(channel)) {
-            done = true;
-        }
-    }
-    
-    tcsetattr(STDIN_FILENO, TCSANOW, &orig_termios);
-    return 0;
-}
-
-int ssh_exec_command(ssh_session session, ssh_channel channel, const std::string& remote_cmd_str, bool silent, std::string* output, const std::map<std::string, std::string>& env, const std::string& working_dir) {
-    // Build complete command with env, working_dir, and the command itself
-    std::ostringstream cmd_with_env;
-    
-    // Create a simple, flat command that will work reliably
-    // Format: env VAR=value bash -c 'cd /path && command args'
-    
-    // Start with env variables
-    if (!env.empty()) {
-        cmd_with_env << "env ";
-        for (const auto& kv : env) {
-            if (kv.first == "SSHPASS") continue;
-            cmd_with_env << kv.first << "=\"" << kv.second << "\" ";
-        }
-    }
-    
-    // Use a single bash -c with the entire command inside single quotes
-    cmd_with_env << "bash -c '";
-    
-    // Add cd if working directory specified
-    if (!working_dir.empty()) {
-        cmd_with_env << "cd " << working_dir << " && ";
-    }
-    cmd_with_env << remote_cmd_str;
-    
-    // Close the single quote
-    cmd_with_env << "'";
-    
-    std::string final_cmd = cmd_with_env.str();
-
-    std::cout << "Final remote command: " << final_cmd << std::endl;
-    
-    int rc = ssh_channel_request_exec(channel, final_cmd.c_str());
-    if (rc != SSH_OK) {
-        std::string error = std::string("Failed to exec remote command: ") + ssh_get_error(session);
-        std::cerr << "SSH exec error: " << error << std::endl;
-        if (output) *output = error;
-        return -1;
-    }
-    
-    if (output) {
-        std::ostringstream oss;
-        char buffer[4096];
-        int nbytes;
-        
-        // Read from stdout
-        while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) {
-            oss.write(buffer, nbytes);
-        }
-        if (nbytes < 0) {
-            std::cerr << "Error reading from stdout" << std::endl;
-        }
-        
-        // Read from stderr
-        while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 1)) > 0) {
-            oss.write(buffer, nbytes);
-        }
-        if (nbytes < 0) {
-            std::cerr << "Error reading from stderr" << std::endl;
-        }
-        
-        *output = oss.str();
-    } else if (!silent) {
-        char buffer[4096];
-        int nbytes;
-        
-        // Read from stdout
-        while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) {
-            write(1, buffer, nbytes);
-        }
-        if (nbytes < 0) {
-            std::cerr << "Error reading from stdout" << std::endl;
-        }
-        
-        // Read from stderr
-        while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 1)) > 0) {
-            write(2, buffer, nbytes);
-        }
-        if (nbytes < 0) {
-            std::cerr << "Error reading from stderr" << std::endl;
-        }
-    }
-    
-    return 0;
-}
-
-int local_execute_cmd(
-    const std::string& command,
-    const std::vector<std::string>& args,
-    const std::string& working_dir,
-    const std::map<std::string, std::string>& env,
-    bool silent,
-    bool interactive,
-    std::string* output
-) {
-    std::cerr << "Local command: " << command << std::endl;
-
-    int pipefd[2];
-    bool use_pipe = output && !interactive;
-    if (use_pipe && pipe(pipefd) == -1) {
-        perror("pipe");
-        std::cerr << "Pipe error: " << strerror(errno) << std::endl;
-        return -1;
-    }
+// runner_local: non-interactive local command execution
+bool runner_local::execute() {
     pid_t pid = fork();
     if (pid == -1) {
-        perror("fork");
-        std::cerr << "Fork error: " << strerror(errno) << std::endl;
-        return -1;
+        std::cerr << "Failed to fork" << std::endl;
+        mReturnCode = -1;
+        return false;
     }
     if (pid == 0) {
-        if (!working_dir.empty()) {
-            if (chdir(working_dir.c_str()) != 0) {
+        // Child process
+        if (!mWorkingDir.empty()) {
+            if (chdir(mWorkingDir.c_str()) != 0) {
                 perror("chdir");
-                std::cerr << "Chdir error: " << strerror(errno) << std::endl;
-                exit(-1);
+                exit(127);
             }
         }
-
-        for (const auto& kv : env) {
-            setenv(kv.first.c_str(), kv.second.c_str(), 1);
-        }
-        
-        if (use_pipe) {
-            close(pipefd[0]);
-            dup2(pipefd[1], STDOUT_FILENO);
-            dup2(pipefd[1], STDERR_FILENO);
-            close(pipefd[1]);
-        } else if (silent && !interactive) {
-            int devnull = open("/dev/null", O_WRONLY);
-            dup2(devnull, STDOUT_FILENO);
-            dup2(devnull, STDERR_FILENO);
-            close(devnull);
-        }
-        if (!interactive) {
-            setsid();
+        // Set environment variables
+        for (const auto& [k, v] : mEnv) {
+            setenv(k.c_str(), v.c_str(), 1);
         }
+        // Build argv
         std::vector<char*> argv;
-        argv.push_back(const_cast<char*>(command.c_str()));
-        for (const auto& arg : args) {
-            argv.push_back(const_cast<char*>(arg.c_str()));
-        }
+        argv.push_back(const_cast<char*>(mCommand.c_str()));
+        for (auto& arg : mArgs) argv.push_back(const_cast<char*>(arg.c_str()));
         argv.push_back(nullptr);
-
-        std::cerr << "Local command: " << command << std::endl;
-        for (const auto& arg : args) {
-            std::cerr << "Local arg: " << arg << std::endl;
-        }
-
-        execvp(command.c_str(), argv.data());
-        perror("execvp");
-        exit(-1);
-    } else {
-        if (use_pipe) {
-            close(pipefd[1]);
-            std::ostringstream oss;
-            char buf[4096];
-            ssize_t n;
-            while ((n = read(pipefd[0], buf, sizeof(buf))) > 0) {
-                oss.write(buf, n);
+        if (mSilent) {
+            int fd = open("/dev/null", O_WRONLY);
+            if (fd != -1) {
+                dup2(fd, STDOUT_FILENO);
+                dup2(fd, STDERR_FILENO);
+                close(fd);
             }
-            close(pipefd[0]);
-            *output = oss.str();
         }
+        execvp(mCommand.c_str(), argv.data());
+        perror("execvp");
+        exit(127);
+    } else {
         int status = 0;
         waitpid(pid, &status, 0);
-        std::cerr << "Waitpid status: " << status << std::endl;
         if (WIFEXITED(status)) {
-            return WEXITSTATUS(status);
+            mReturnCode = WEXITSTATUS(status);
         } else {
-            return -1;
+            mReturnCode = -1;
         }
+        return mReturnCode == 0;
     }
 }
 
-} // anonymous namespace
-
-int execute_cmd(
-    const std::string& command,
-    const std::vector<std::string>& args,
-    const std::string& working_dir,
-    const std::map<std::string, std::string>& env,
-    const bool silent,
-    const bool interactive,
-    const copySSHPtr& sshinfo,
-    std::string* output
-) {
-    if (sshinfo.valid()) {
-        std::string error;
-        ssh_session session = ssh_connect_and_auth(&sshinfo, env, &error);
-        if (!session) {
-            if (output) *output = error;
-            std::cerr << "SSH connection error: " << error << std::endl;
-            return -1;
+// runner_local_interactive: interactive local command execution
+bool runner_local_interactive::execute() {
+    pid_t pid = fork();
+    if (pid == -1) {
+        std::cerr << "Failed to fork" << std::endl;
+        mReturnCode = -1;
+        return false;
+    }
+    if (pid == 0) {
+        if (!mWorkingDir.empty()) {
+            if (chdir(mWorkingDir.c_str()) != 0) {
+                perror("chdir");
+                exit(127);
+            }
         }
-        ssh_channel channel = ssh_channel_new(session);
-        if (!channel) {
-            if (output) *output = "Failed to create SSH channel.";
-            ssh_disconnect(session);
-            ssh_free(session);
-            std::cerr << "SSH channel error: " << output->c_str() << std::endl;
-            return -1;
+        for (const auto& [k, v] : mEnv) {
+            setenv(k.c_str(), v.c_str(), 1);
         }
-        int rc = ssh_channel_open_session(channel);
-        if (rc != SSH_OK) {
-            if (output) *output = std::string("Failed to open SSH channel: ") + ssh_get_error(session);
-            ssh_channel_free(channel);
-            ssh_disconnect(session);
-            ssh_free(session);
-            std::cerr << "SSH channel open error: " << output->c_str() << std::endl;
-            return -1;
-        }
-        
-        int ret = 0;
-        std::string remote_cmd_str = ssh_build_remote_command(command, args, working_dir, {});
-        if (interactive) {
-            ret = ssh_interactive_shell_session(session, channel, remote_cmd_str, command, output);
+        std::vector<char*> argv;
+        argv.push_back(const_cast<char*>(mCommand.c_str()));
+        for (auto& arg : mArgs) argv.push_back(const_cast<char*>(arg.c_str()));
+        argv.push_back(nullptr);
+        // Attach to terminal (no redirection)
+        execvp(mCommand.c_str(), argv.data());
+        perror("execvp");
+        exit(127);
+    } else {
+        int status = 0;
+        waitpid(pid, &status, 0);
+        if (WIFEXITED(status)) {
+            mReturnCode = WEXITSTATUS(status);
         } else {
-            // For non-interactive, handle working directory in ssh_exec_command
-            ret = ssh_exec_command(session, channel, remote_cmd_str, silent, output, env, working_dir);
+            mReturnCode = -1;
         }
-        
-        ssh_channel_send_eof(channel);
+        return mReturnCode == 0;
+    }
+}
+
+// runner_ssh: non-interactive SSH command execution
+bool runner_ssh::execute() {
+    std::string error;
+    ssh_session session = ssh_connect_and_auth(&mSSHInfo, mEnv, &error);
+    if (!session) {
+        std::cerr << error << std::endl;
+        mReturnCode = -1;
+        return false;
+    }
+    ssh_channel channel = ssh_channel_new(session);
+    if (!channel) {
+        std::cerr << "Failed to create SSH channel." << std::endl;
+        ssh_disconnect(session);
+        ssh_free(session);
+        mReturnCode = -1;
+        return false;
+    }
+    if (ssh_channel_open_session(channel) != SSH_OK) {
+        std::cerr << "Failed to open SSH channel: " << ssh_get_error(session) << std::endl;
+        ssh_channel_free(channel);
+        ssh_disconnect(session);
+        ssh_free(session);
+        mReturnCode = -1;
+        return false;
+    }
+    std::stringstream cmd;
+    cmd << mCommand;
+    for (const auto& arg : mArgs) {
+        cmd << " '" << arg << "'";
+    }
+    int rc = ssh_channel_request_exec(channel, cmd.str().c_str());
+    if (rc != SSH_OK) {
+        std::cerr << "SSH exec failed: " << ssh_get_error(session) << std::endl;
         ssh_channel_close(channel);
         ssh_channel_free(channel);
         ssh_disconnect(session);
         ssh_free(session);
-
-        if (output) trim(output);
-
-        std::cerr << "SSH command execution result: " << ret << std::endl;
-        return ret;
-    } else {
-        int ret=local_execute_cmd(command, args, working_dir, env, silent, interactive, output);
-        if (output) trim(output);
-
-        std::cerr << "Local command execution result: " << ret << std::endl;
-        return ret;
+        mReturnCode = -1;
+        return false;
     }
+    char buffer[256];
+    int nbytes;
+    while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) {
+        if (!mSilent) std::cout.write(buffer, nbytes);
+    }
+    mReturnCode = ssh_channel_get_exit_status(channel);
+    ssh_channel_send_eof(channel);
+    ssh_channel_close(channel);
+    ssh_channel_free(channel);
+    ssh_disconnect(session);
+    ssh_free(session);
+    return mReturnCode == 0;
 }
 
+// runner_ssh_interactive: interactive SSH command execution
+bool runner_ssh_interactive::execute() {
+    std::string error;
+    ssh_session session = ssh_connect_and_auth(&mSSHInfo, mEnv, &error);
+    if (!session) {
+        std::cerr << error << std::endl;
+        mReturnCode = -1;
+        return false;
+    }
+    ssh_channel channel = ssh_channel_new(session);
+    if (!channel) {
+        std::cerr << "Failed to create SSH channel." << std::endl;
+        ssh_disconnect(session);
+        ssh_free(session);
+        mReturnCode = -1;
+        return false;
+    }
+    if (ssh_channel_open_session(channel) != SSH_OK) {
+        std::cerr << "Failed to open SSH channel: " << ssh_get_error(session) << std::endl;
+        ssh_channel_free(channel);
+        ssh_disconnect(session);
+        ssh_free(session);
+        mReturnCode = -1;
+        return false;
+    }
+    std::stringstream cmd;
+    cmd << mCommand;
+    for (const auto& arg : mArgs) {
+        cmd << " '" << arg << "'";
+    }
+    int rc = ssh_channel_request_pty(channel);
+    if (rc != SSH_OK) {
+        std::cerr << "Failed to request PTY: " << ssh_get_error(session) << std::endl;
+        ssh_channel_close(channel);
+        ssh_channel_free(channel);
+        ssh_disconnect(session);
+        ssh_free(session);
+        mReturnCode = -1;
+        return false;
+    }
+    rc = ssh_channel_request_shell(channel);
+    if (rc != SSH_OK) {
+        std::cerr << "Failed to request shell: " << ssh_get_error(session) << std::endl;
+        ssh_channel_close(channel);
+        ssh_channel_free(channel);
+        ssh_disconnect(session);
+        ssh_free(session);
+        mReturnCode = -1;
+        return false;
+    }
+    // Forward input/output between user and SSH channel
+    fd_set fds;
+    char buffer[256];
+    while (true) {
+        FD_ZERO(&fds);
+        FD_SET(0, &fds); // stdin
+        int ssh_fd = ssh_get_fd(session);
+        FD_SET(ssh_fd, &fds);
+        int maxfd = std::max(0, ssh_fd) + 1;
+        int ret = select(maxfd, &fds, nullptr, nullptr, nullptr);
+        if (ret < 0) break;
+        if (FD_ISSET(0, &fds)) {
+            int n = read(0, buffer, sizeof(buffer));
+            if (n > 0) ssh_channel_write(channel, buffer, n);
+        }
+        if (FD_ISSET(ssh_fd, &fds)) {
+            int n = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
+            if (n > 0) write(1, buffer, n);
+            else if (n == 0) break;
+        }
+        if (ssh_channel_is_closed(channel)) break;
+    }
+    mReturnCode = ssh_channel_get_exit_status(channel);
+    ssh_channel_send_eof(channel);
+    ssh_channel_close(channel);
+    ssh_channel_free(channel);
+    ssh_disconnect(session);
+    ssh_free(session);
+    return mReturnCode == 0;
+}
+
+// runner_ssh_capture: SSH command execution with output capture
+bool runner_ssh_capture::execute() {
+    std::string error;
+    ssh_session session = ssh_connect_and_auth(&mSSHInfo, mEnv, &error);
+    if (!session) {
+        std::cerr << error << std::endl;
+        mReturnCode = -1;
+        return false;
+    }
+    ssh_channel channel = ssh_channel_new(session);
+    if (!channel) {
+        std::cerr << "Failed to create SSH channel." << std::endl;
+        ssh_disconnect(session);
+        ssh_free(session);
+        mReturnCode = -1;
+        return false;
+    }
+    if (ssh_channel_open_session(channel) != SSH_OK) {
+        std::cerr << "Failed to open SSH channel: " << ssh_get_error(session) << std::endl;
+        ssh_channel_free(channel);
+        ssh_disconnect(session);
+        ssh_free(session);
+        mReturnCode = -1;
+        return false;
+    }
+    std::stringstream cmd;
+    cmd << mCommand;
+    for (const auto& arg : mArgs) {
+        cmd << " '" << arg << "'";
+    }
+    int rc = ssh_channel_request_exec(channel, cmd.str().c_str());
+    if (rc != SSH_OK) {
+        std::cerr << "SSH exec failed: " << ssh_get_error(session) << std::endl;
+        ssh_channel_close(channel);
+        ssh_channel_free(channel);
+        ssh_disconnect(session);
+        ssh_free(session);
+        mReturnCode = -1;
+        return false;
+    }
+    char buffer[256];
+    int nbytes;
+    mOutput.clear();
+    while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) {
+        mOutput.append(buffer, nbytes);
+    }
+    mReturnCode = ssh_channel_get_exit_status(channel);
+    ssh_channel_send_eof(channel);
+    ssh_channel_close(channel);
+    ssh_channel_free(channel);
+    ssh_disconnect(session);
+    ssh_free(session);
+    return mReturnCode == 0;
+}
 } // namespace runner
\ No newline at end of file
diff --git a/src/utils/runner.hpp b/src/utils/runner.hpp
index ce908bb..c3effc6 100644
--- a/src/utils/runner.hpp
+++ b/src/utils/runner.hpp
@@ -14,26 +14,67 @@ struct sSSHInfo {
     std::string port;
 };
 
-class copySSHPtr {
+class runner {
     public:
-        copySSHPtr(const sSSHInfo* sshinfo) : mSSHInfo(sshinfo ? *sshinfo : sSSHInfo()) {}
-        copySSHPtr(const sSSHInfo& sshinfo) : mSSHInfo(sshinfo) {}
-        bool valid() const { return !mSSHInfo.host.empty(); }
-        const sSSHInfo * operator&() const { return (valid() ? &mSSHInfo : nullptr); }
-    private:
-        sSSHInfo mSSHInfo;
-};  
+        runner(std::string command, std::vector<std::string> args={}, std::string working_dir="", std::map<std::string, std::string> env={}, bool silent=false) :
+            mCommand(command),
+            mArgs(args),
+            mWorkingDir(working_dir),
+            mEnv(env),
+            mSilent(silent),
+            mReturnCode(0) {}
+        virtual ~runner() {}
 
-int execute_cmd(
-    const std::string& command,
-    const std::vector<std::string>& args,
-    const std::string& working_dir,
-    const std::map<std::string, std::string>& env,
-    const bool silent,
-    const bool interactive,
-    const copySSHPtr& sshinfo = nullptr,
-    std::string* output = nullptr
-); 
+        virtual bool execute() = 0;
+
+        int return_code() const { return mReturnCode; }
+    protected:
+        std::string mCommand;
+        std::vector<std::string> mArgs;
+        std::string mWorkingDir;
+        std::map<std::string, std::string> mEnv;
+        bool mSilent;
+        int mReturnCode;
+};
+
+class runner_local : public runner {
+    public:
+        runner_local(std::string command, std::vector<std::string> args={}, std::string working_dir="", std::map<std::string, std::string> env={}, bool silent=false) : 
+            runner(command, args, working_dir, env, silent) {}
+        bool execute() override;
+};
+
+class runner_local_interactive : public runner {
+    public:
+        runner_local_interactive(std::string command, std::vector<std::string> args={}, std::string working_dir="", std::map<std::string, std::string> env={}) : 
+            runner(command, args, working_dir, env, false) {}
+        bool execute() override;
+};
+
+class runner_ssh : public runner {
+    public:
+        runner_ssh(const sSSHInfo sshinfo, std::string command, std::vector<std::string> args={}, std::string working_dir="", std::map<std::string, std::string> env={}, bool silent=false) : 
+            runner(command, args, working_dir, env, silent), mSSHInfo(sshinfo) {}   
+        bool execute() override;
+    protected:
+        const sSSHInfo mSSHInfo;
+};
+
+class runner_ssh_interactive : public runner_ssh {
+    public:
+        runner_ssh_interactive(const sSSHInfo sshinfo, std::string command, std::vector<std::string> args={}, std::string working_dir="", std::map<std::string, std::string> env={}, bool silent=true) : 
+            runner_ssh(sshinfo, command, args, working_dir, env, silent) {}
+        bool execute() override;
+};
+
+class runner_ssh_capture : public runner_ssh {
+    public:
+        runner_ssh_capture(const sSSHInfo sshinfo, std::string output, std::string command, std::vector<std::string> args={}, std::string working_dir="", std::map<std::string, std::string> env={}) : 
+            runner_ssh(sshinfo, command, args, working_dir, env, true), mOutput(output) {}
+        bool execute() override;
+    protected:
+        std::string & mOutput;
+};
 
 } // namespace runner