From 78dbf4aff35abe1862674e1a6fd9ed56a02708d0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 11 May 2025 12:22:36 +1200 Subject: [PATCH] Implementing commands --- CMakeLists.txt | 1 + src/command_registry.cpp | 45 +++++++ src/command_registry.hpp | 46 +++++++ src/commands/edit.cpp | 169 +++++++++++++++++++++++++ src/commands/standard_autocomplete.cpp | 27 ++++ src/commands/standard_autocomplete.hpp | 15 +++ src/main.cpp | 56 +++----- src/service_runner.cpp | 64 ---------- src/service_runner.hpp | 7 +- src/utils/utils.cpp | 18 +++ src/utils/utils.hpp | 4 + 11 files changed, 341 insertions(+), 111 deletions(-) create mode 100644 src/command_registry.cpp create mode 100644 src/command_registry.hpp create mode 100644 src/commands/edit.cpp create mode 100644 src/commands/standard_autocomplete.cpp create mode 100644 src/commands/standard_autocomplete.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 17c7d2f..f0c9e47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ target_include_directories(dropshell PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/utils ${CMAKE_CURRENT_SOURCE_DIR}/src/contrib + ${CMAKE_CURRENT_SOURCE_DIR}/src/commands $ ) diff --git a/src/command_registry.cpp b/src/command_registry.cpp new file mode 100644 index 0000000..9f5c5bc --- /dev/null +++ b/src/command_registry.cpp @@ -0,0 +1,45 @@ +#include "command_registry.hpp" + +CommandRegistry& CommandRegistry::instance() { + static CommandRegistry reg; + return reg; +} + +void CommandRegistry::register_command(const CommandInfo& info) { + auto ptr = std::make_shared(info); + for (const auto& name : info.names) { + command_map_[name] = ptr; + } + all_commands_.push_back(ptr); +} + +const CommandInfo* CommandRegistry::find_command(const std::string& name) const { + auto it = command_map_.find(name); + if (it != command_map_.end()) return it->second.get(); + return nullptr; +} + +std::vector CommandRegistry::list_commands(bool include_hidden) const { + std::set out; + for (const auto& cmd : all_commands_) { + if (!cmd->hidden || include_hidden) { + for (const auto& name : cmd->names) out.insert(name); + } + } + return std::vector(out.begin(), out.end()); +} + +void CommandRegistry::autocomplete(const std::vector& args) const { + if (args.size() < 3) { + for (const auto& name : list_commands(false)) { + std::cout << name << std::endl; + } + return; + } + std::string cmd = args[2]; + auto* info = find_command(cmd); + if (info && info->autocomplete) { + CommandContext ctx{args}; + info->autocomplete(ctx); + } +} \ No newline at end of file diff --git a/src/command_registry.hpp b/src/command_registry.hpp new file mode 100644 index 0000000..faf2d76 --- /dev/null +++ b/src/command_registry.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +struct CommandContext { + std::vector args; + // Add more fields as needed (e.g., config pointer, output stream, etc.) +}; + +struct CommandInfo { + std::vector names; + std::function handler; + std::function autocomplete; // optional + bool hidden = false; + bool requires_config = false; + int min_args = 0; + int max_args = -1; // -1 = unlimited + std::string help_usage; // install SERVER [SERVICE] + std::string help_description; // Install/reinstall/update service(s). Safe/non-destructive. +}; + +class CommandRegistry { +public: + static CommandRegistry& instance(); + + void register_command(const CommandInfo& info); + + // Returns nullptr if not found + const CommandInfo* find_command(const std::string& name) const; + + // List all commands (optionally including hidden) + std::vector list_commands(bool include_hidden = false) const; + + // For autocomplete + void autocomplete(const std::vector& args) const; + +private: + CommandRegistry() = default; + std::map> command_map_; + std::vector> all_commands_; +}; diff --git a/src/commands/edit.cpp b/src/commands/edit.cpp new file mode 100644 index 0000000..4a9c885 --- /dev/null +++ b/src/commands/edit.cpp @@ -0,0 +1,169 @@ +#include "command_registry.hpp" +#include "config.hpp" +#include "utils/utils.hpp" +#include "service_runner.hpp" +#include "utils/directories.hpp" +#include "standard_autocomplete.hpp" + +#include +#include +#include +#include +#include +#include + +namespace dropshell { + +void edit_autocomplete(const CommandContext& ctx); +int edit_handler(const CommandContext& ctx); + +static std::vector edit_name_list={"edit"}; + +// Static registration +struct EditCommandRegister { + EditCommandRegister() { + CommandRegistry::instance().register_command({ + edit_name_list, + edit_handler, + edit_autocomplete, + false, // hidden + false, // requires_config + 0, // min_args (after command) + 2 // max_args (after command) + }); + } +} edit_command_register; + +// ------------------------------------------------------------------------------------------------ +// edit command implementation +// ------------------------------------------------------------------------------------------------ + + + +// ------------------------------------------------------------------------------------------------ +// utility function to edit a file +// ------------------------------------------------------------------------------------------------ +bool edit_file(const std::string &file_path) +{ + // make sure parent directory exists. + std::string parent_dir = get_parent(file_path); + std::filesystem::create_directories(parent_dir); + + std::string editor_cmd; + const char* editor_env = std::getenv("EDITOR"); + + if (editor_env && std::strlen(editor_env) > 0) { + editor_cmd = std::string(editor_env) + " " + quote(file_path); + } else if (isatty(STDIN_FILENO)) { + // Check if stdin is connected to a terminal if EDITOR is not set + editor_cmd = "nano -w " + quote(file_path); + } else { + std::cerr << "Error: Standard input is not a terminal and EDITOR environment variable is not set." << std::endl; + std::cerr << "Try setting the EDITOR environment variable (e.g., export EDITOR=nano) or run in an interactive terminal." << std::endl; + std::cerr << "You can manually edit the file at: " << file_path << std::endl; + return false; + } + + std::cout << "Editing file: " << file_path << std::endl; + return execute_local_command(editor_cmd, nullptr, cMode::Interactive | cMode::RawCommand); +} + +// ------------------------------------------------------------------------------------------------ +// edit config +// ------------------------------------------------------------------------------------------------ +int edit_config() +{ + if (!gConfig().is_config_set()) + gConfig().save_config(false); // save defaults. + + std::string config_file = localfile::dropshell_json(); + if (!edit_file(config_file) || !std::filesystem::exists(config_file)) + return die("Error: Failed to edit config file."); + + gConfig().load_config(); + if (!gConfig().is_config_set()) + return die("Error: Failed to load and parse edited config file!"); + + gConfig().save_config(true); + std::cout << "Successfully edited config file at " << config_file << std::endl; + return 0; +} + +// ------------------------------------------------------------------------------------------------ +// edit server +// ------------------------------------------------------------------------------------------------ +int edit_server(const std::string &server_name) +{ + std::string serverpath = localpath::server(server_name); + if (serverpath.empty()) { + std::cerr << "Error: Server not found: " << server_name << std::endl; + return -1; + } + + std::ostringstream aftertext; + aftertext << "If you have changed DROPSHELL_DIR, you should manually move the files to the new location NOW.\n" + << "You can ssh in to the remote server with: dropshell ssh "< &name_list, const CommandContext &ctx) +{ + if (ctx.args.size() == 1) { + // edit command + for (const auto& name : name_list) { + std::cout << name << std::endl; + } + } + else if (ctx.args.size() == 2) { + // list servers + std::vector servers = get_configured_servers(); + for (const auto& server : servers) { + std::cout << server.name << std::endl; + } + } + else if (ctx.args.size() == 3) { + // list services + std::vector services = get_server_services_info(ctx.args[2]); + for (const auto& service : services) { + std::cout << service.service_name << std::endl; + } + } +} \ No newline at end of file diff --git a/src/commands/standard_autocomplete.hpp b/src/commands/standard_autocomplete.hpp new file mode 100644 index 0000000..7c643a3 --- /dev/null +++ b/src/commands/standard_autocomplete.hpp @@ -0,0 +1,15 @@ +#ifndef STANDARD_AUTOCOMPLETE_HPP +#define STANDARD_AUTOCOMPLETE_HPP + +#include +#include + +#include "command_registry.hpp" + +namespace dropshell { + + +void std_autocomplete(const std::vector& name_list, const CommandContext& ctx); + +} // namespace dropshell +#endif diff --git a/src/main.cpp b/src/main.cpp index 677db78..e040847 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,7 @@ #include "utils/utils.hpp" #include "autocomplete.hpp" #include "utils/hash.hpp" +#include "command_registry.hpp" #include #include @@ -17,7 +18,7 @@ #include #include #include - +#include namespace dropshell { extern const std::string VERSION; @@ -25,6 +26,15 @@ extern const std::string RELEASE_DATE; extern const std::string AUTHOR; extern const std::string LICENSE; +void show_command(const std::string& cmd) { + const auto& cmd_info = CommandRegistry::instance().find_command(cmd); + if (cmd_info) { + std::cout << " " << cmd_info->help_usage + << std::string(' ', std::min(1,(int)(30-cmd_info->help_usage.length()))) + << cmd_info->help_description << std::endl; + } +} + bool print_help() { std::cout << std::endl; maketitle("DropShell version " + VERSION); @@ -32,11 +42,10 @@ bool print_help() { std::cout << "A tool for managing server configurations" << std::endl; std::cout << std::endl; std::cout << "dropshell ..." << std::endl; - std::cout << " help Show this help message" << std::endl; - std::cout << " edit Edit the configuration of dropshell" << std::endl; + show_command("help"); + show_command("edit"); if (gConfig().is_config_set()) { - std::cout << " server NAME Show details for specific server" << std::endl; std::cout << " templates List all available templates" << std::endl; std::cout << std::endl; std::cout << std::endl; @@ -59,19 +68,14 @@ bool print_help() { std::cout << " create-service SERVER TEMPLATE SERVICE" << std::endl; } else { - std::cout << " edit Edit the configuration of dropshell" << std::endl; + show_command("help"); + show_command("edit"); std::cout << std::endl; std::cout << "Other commands available once initialised." << std::endl; } return true; } - -int die(const std::string & msg) { - std::cerr << msg << std::endl; - return 1; -} - struct ServerAndServices { std::string server_name; std::vector servicelist; @@ -91,12 +95,6 @@ bool getCLIServices(const std::string & arg2, const std::string & arg3, return true; } -std::string safearg(int argc, char *argv[], int index) -{ - if (index >= argc) return ""; - return argv[index]; -} - void printversion() { maketitle("DropShell version " + VERSION); std::cout << "Release date: " << RELEASE_DATE << std::endl; @@ -128,23 +126,6 @@ auto command_match = [](const std::string& cmd_list, int argc, char* argv[]) -> } \ } -int edit_config() -{ - if (!gConfig().is_config_set()) - gConfig().save_config(false); // save defaults. - - std::string config_file = localfile::dropshell_json(); - if (!service_runner::edit_file(config_file) || !std::filesystem::exists(config_file)) - return die("Error: Failed to edit config file."); - - gConfig().load_config(); - if (!gConfig().is_config_set()) - return die("Error: Failed to load and parse edited config file!"); - - gConfig().save_config(true); - std::cout << "Successfully edited config file at " << config_file << std::endl; - return 0; -} int main(int argc, char* argv[]) { HAPPYEXIT("hash", hash_demo_raw(safearg(argc,argv,2))) @@ -173,8 +154,6 @@ int main(int argc, char* argv[]) { return autocomplete::autocomplete(argvec) ? 0 : 1; } - if (cmd == "edit" && argc < 3) - return edit_config(); // ------------------------------------------------------------ // from here we require the config file to be loaded. @@ -231,11 +210,6 @@ int main(int argc, char* argv[]) { return 0; } - if (cmd == "edit" && argc < 4) { - ASSERT(argc>=3, "Error: logic error!"); - service_runner::edit_server(safearg(argc,argv,2)); - return 0; - } // handle running a command. std::set commands; diff --git a/src/service_runner.cpp b/src/service_runner.cpp index aaa9f4c..6f37103 100644 --- a/src/service_runner.cpp +++ b/src/service_runner.cpp @@ -194,12 +194,6 @@ bool service_runner::run_command(const std::string& command, std::vector 0) { - editor_cmd = std::string(editor_env) + " " + quote(file_path); - } else if (isatty(STDIN_FILENO)) { - // Check if stdin is connected to a terminal if EDITOR is not set - editor_cmd = "nano -w " + quote(file_path); - } else { - std::cerr << "Error: Standard input is not a terminal and EDITOR environment variable is not set." << std::endl; - std::cerr << "Try setting the EDITOR environment variable (e.g., export EDITOR=nano) or run in an interactive terminal." << std::endl; - std::cerr << "You can manually edit the file at: " << file_path << std::endl; - return false; - } - - std::cout << "Editing file: " << file_path << std::endl; - return execute_local_command(editor_cmd, nullptr, cMode::Interactive | cMode::RawCommand); -} bool service_runner::interactive_ssh_service() { @@ -457,18 +405,6 @@ bool service_runner::interactive_ssh_service() return mServerEnv.run_remote_template_command(mService, "ssh", args, false, {}); } -void service_runner::edit_service_config() -{ - std::string config_file = localfile::service_env(mServer,mService); - if (!fs::exists(config_file)) { - std::cerr << "Error: Service config file not found: " << config_file << std::endl; - return; - } - - if (edit_file(config_file) && std::filesystem::exists(config_file)) - std::cout << "To apply your changes, run:\n dropshell install " + mServer + " " + mService << std::endl; -} - bool service_runner::scp_file_to_remote(const std::string &local_path, const std::string &remote_path, bool silent) { std::string scp_cmd = "scp -P " + mServerEnv.get_SSH_PORT() + " " + quote(local_path) + " " + mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_path) + (silent ? " > /dev/null 2>&1" : ""); diff --git a/src/service_runner.hpp b/src/service_runner.hpp index a1dadd6..4de63cd 100644 --- a/src/service_runner.hpp +++ b/src/service_runner.hpp @@ -55,7 +55,7 @@ class service_runner { std::string healthtick(); std::string healthmark(); - private: + public: // install the service over ssh, using the credentials from server.env (via server_env.hpp), by: // 1. check if the server_name exists, and the service_name refers to a valid template // 2. check if service_name is valid for the server_name @@ -84,9 +84,6 @@ class service_runner { // replaces the current dropshell process with the ssh process bool interactive_ssh_service(); - // edit the service configuration file - void edit_service_config(); - bool rsync_tree_to_remote(const std::string& local_path, const std::string& remote_path, bool silent=false); bool scp_file_to_remote(const std::string& local_path, const std::string& remote_path, bool silent=false); bool scp_file_from_remote(const std::string& remote_path, const std::string& local_path, bool silent=false); @@ -94,8 +91,6 @@ class service_runner { // utility functions static std::string get_latest_backup_file(const std::string& server, const std::string& service); static bool interactive_ssh(const std::string & server_name, const std::string & command); - static void edit_server(const std::string & server_name); - static bool edit_file(const std::string & file_path); static std::map get_all_services_status(std::string server_name); static std::string HealthStatus2String(HealthStatus status); diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index a84d905..cc8d7b1 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -316,4 +316,22 @@ std::string requote(std::string str) { return quote(trim(dequote(trim(str)))); } + +int die(const std::string & msg) { + std::cerr << msg << std::endl; + return 1; +} + +std::string safearg(const std::vector & args, int index) +{ + if (index >= args.size()) return ""; + return args[index]; +} + +std::string safearg(int argc, char *argv[], int index) +{ + if (index >= argc) return ""; + return argv[index]; +} + } // namespace dropshell \ No newline at end of file diff --git a/src/utils/utils.hpp b/src/utils/utils.hpp index 58febfb..603409e 100644 --- a/src/utils/utils.hpp +++ b/src/utils/utils.hpp @@ -41,4 +41,8 @@ std::string replace_with_environment_variables_like_bash(std::string str); std::string random_alphanumeric_string(int length); +int die(const std::string & msg); +std::string safearg(int argc, char *argv[], int index); +std::string safearg(const std::vector & args, int index); + } // namespace dropshell \ No newline at end of file