#include "command_registry.hpp" #include "config.hpp" #include "utils/utils.hpp" #include "utils/directories.hpp" #include "utils/service_env_validator.hpp" #include "shared_commands.hpp" #include "services.hpp" #include "templates.hpp" #include #include #include #include #include #include #include #include namespace dropshell { int edit_handler(const CommandContext& ctx); static std::vector edit_name_list={"edit","create-config"}; // Static registration struct EditCommandRegister { EditCommandRegister() { CommandRegistry::instance().register_command({ edit_name_list, edit_handler, shared_commands::std_autocomplete, false, // hidden false, // requires_config false, // requires_install 0, // min_args (after command) 2, // max_args (after command) "edit [SERVER] [SERVICE] | edit override", "Edit dropshell, server, service, or override configuration", // heredoc R"( Edit dropshell, server or service configuration. edit edit the dropshell config. edit override edit the overrides.env for a server location. edit edit the server config. edit edit the service config. )" }); } } edit_command_register; // ------------------------------------------------------------------------------------------------ // edit command implementation // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ // utility function to edit a file // ------------------------------------------------------------------------------------------------ bool edit_file(const std::string &file_path, bool has_bb64) { // 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 { error << "Standard input is not a terminal and EDITOR environment variable is not set." << std::endl; info << "Try setting the EDITOR environment variable (e.g., export EDITOR=nano) or run in an interactive terminal." << std::endl; info << "You can manually edit the file at: " << file_path << std::endl; return false; } info << "Editing file: " << file_path << std::endl; if (has_bb64) { return execute_local_command("", editor_cmd, {}, nullptr, cMode::Interactive); } else { // might not have bb64 at this early stage. Direct edit. int ret = system(editor_cmd.c_str()); return EXITSTATUSCHECK(ret); } } int create_config() { if (!gConfig().is_config_set()) { bool ok = gConfig().save_config(); // save defaults. info << "Default dropshell.json created." << std::endl; return (ok ? 0 : 1); } else info << "Existing dropshell.json unchanged." << std::endl; return 0; } // ------------------------------------------------------------------------------------------------ // edit config // ------------------------------------------------------------------------------------------------ int edit_config() { create_config(); std::string config_file = localfile::dropshell_json(); if (!edit_file(config_file, false) || !std::filesystem::exists(config_file)) return return_die("Failed to edit config file."); gConfig().load_config(); if (!gConfig().is_config_set()) return return_die("Failed to load and parse edited config file!"); // Don't save_config after loading - it would rewrite the file the user just edited! // The config is already saved by the editor, we just validated it by loading it. gConfig().create_aux_directories(); info << "Successfully edited config file at " << config_file << std::endl; return 0; } // ------------------------------------------------------------------------------------------------ // edit server // ------------------------------------------------------------------------------------------------ int edit_server(const std::string &server_name) { if (localpath::server(server_name).empty()) { error << "Server not found: " << server_name << std::endl; return -1; } std::string config_file = localfile::server_json(server_name); if (!edit_file(config_file, true)) { error << "Failed to edit server config" << std::endl; info << "You can manually edit this file at: " << config_file << std::endl; return 1; } info << "If you have changed DROPSHELL_DIR, you should manually move the files to the new location NOW." << std::endl; info << "You can ssh in to the remote server with: dropshell ssh "< directories; for (const auto &file : std::filesystem::directory_iterator(dir)) { if (first) { if (!msg.empty()) info << msg << std::endl; first=false; } if (std::filesystem::is_directory(file.path())) directories.push_back(file.path()); else info << " " << file.path() << std::endl; } for (const auto &dir : directories) list_directory(dir, ""); } // ------------------------------------------------------------------------------------------------ // edit service config // ------------------------------------------------------------------------------------------------ int edit_service_config(const std::string &server, const std::string &service) { std::string config_file = localfile::service_env(server, service); if (!std::filesystem::exists(config_file)) { error << "Service config file not found: " << config_file << std::endl; return 1; } // Validate service.env matches template BEFORE editing (adds any missing variables) { LocalServiceInfo service_info = get_service_info(server, service); if (!SIvalid(service_info)) { error << "Failed to get service info for " << service << std::endl; return 1; } template_info tinfo = gTemplateManager().get_template_info(service_info.template_name); if (!tinfo.is_set()) { error << "Template not found: " << service_info.template_name << std::endl; return 1; } std::filesystem::path template_service_env = tinfo.local_template_path() / "config" / "service.env"; std::filesystem::path template_info_env = tinfo.local_template_path() / "config" / ".template_info.env"; std::vector missing_vars; std::vector extra_vars; if (!validate_and_fix_service_env(template_service_env.string(), config_file, missing_vars, extra_vars, template_info_env.string())) { info << "Service environment file updated to match template:" << std::endl; if (!missing_vars.empty()) { info << " Added missing variables: "; for (size_t i = 0; i < missing_vars.size(); ++i) { info << missing_vars[i]; if (i < missing_vars.size() - 1) info << ", "; } info << std::endl; } if (!extra_vars.empty()) { info << " Commented out extra variables: "; for (size_t i = 0; i < extra_vars.size(); ++i) { info << extra_vars[i]; if (i < extra_vars.size() - 1) info << ", "; } info << std::endl; } } } if (edit_file(config_file, true) && std::filesystem::exists(config_file)) info << "Successfully edited service config file at " << config_file << std::endl; std::string service_dir = localpath::service(server, service); list_directory(service_dir, "You may wish to edit the other files in " + service_dir); info << "Then to apply your changes, run:" << std::endl; info << " dropshell uninstall " + server + " " + service << std::endl; info << " dropshell install " + server + " " + service << std::endl; return 0; } // ------------------------------------------------------------------------------------------------ // edit override // ------------------------------------------------------------------------------------------------ int edit_override() { auto paths = gConfig().get_local_server_definition_paths(); if (paths.empty()) { error << "No server definition paths configured." << std::endl; return 1; } std::string chosen_path; if (paths.size() == 1) { chosen_path = paths[0]; } else { // Multiple paths - ask user to choose with single keypress info << "Multiple server locations found. Choose one:" << std::endl; for (size_t i = 0; i < paths.size() && i < 9; ++i) info << " " << (i + 1) << ") " << paths[i] << std::endl; info << "Enter choice [1-" << std::min(paths.size(), (size_t)9) << "]: " << std::flush; // Read single keypress struct termios oldt, newt; tcgetattr(STDIN_FILENO, &oldt); newt = oldt; newt.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, &newt); int ch = getchar(); tcsetattr(STDIN_FILENO, TCSANOW, &oldt); info << (char)ch << std::endl; int idx = ch - '1'; if (idx < 0 || idx >= (int)paths.size()) { error << "Invalid choice." << std::endl; return 1; } chosen_path = paths[idx]; } std::string override_file = (std::filesystem::path(chosen_path) / filenames::overrides_env).string(); // Create with comment header if it doesn't exist if (!std::filesystem::exists(override_file)) { std::ofstream f(override_file); f << "# Overrides for all services in this server location." << std::endl; f << "# Variables set here will be forced into every service.env" << std::endl; f << "# during create-service and install." << std::endl; f << "#" << std::endl; f << "# Format is the same as service.env:" << std::endl; f << "# VARIABLE_NAME=\"value\"" << std::endl; f << std::endl; info << "Created new overrides file: " << override_file << std::endl; } return edit_file(override_file, true) ? 0 : 1; } // ------------------------------------------------------------------------------------------------ // edit command handler // ------------------------------------------------------------------------------------------------ int edit_handler(const CommandContext& ctx) { // edit dropshell config if (ctx.args.size() < 1) { if (ctx.command=="create-config") { int rval = create_config(); gConfig().create_aux_directories(); return rval; } else return edit_config(); } // edit override - check before server/service dispatch if (safearg(ctx.args, 0) == "override") return edit_override(); // edit server config if (ctx.args.size() < 2) { edit_server(safearg(ctx.args,0)); return 0; } // edit service config if (ctx.args.size() < 3) { edit_service_config(safearg(ctx.args,0), safearg(ctx.args,1)); return 0; } info << "Edit handler called with " << ctx.args.size() << " args\n"; return -1; } } // namespace dropshell