266 lines
9.2 KiB
C++
266 lines
9.2 KiB
C++
#include "command_registry.hpp"
|
|
#include "config.hpp"
|
|
#include "templates.hpp"
|
|
#include "utils/utils.hpp"
|
|
#include "utils/directories.hpp"
|
|
#include "utils/execute.hpp"
|
|
#include "shared_commands.hpp"
|
|
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <regex>
|
|
#include <set>
|
|
|
|
namespace dropshell {
|
|
|
|
void validate_autocomplete(const CommandContext& ctx);
|
|
int validate_handler(const CommandContext& ctx);
|
|
|
|
static std::vector<std::string> validate_name_list = {"validate-template", "validate", "lint"};
|
|
|
|
// Static registration
|
|
struct ValidateCommandRegister {
|
|
ValidateCommandRegister() {
|
|
CommandRegistry::instance().register_command({
|
|
validate_name_list,
|
|
validate_handler,
|
|
validate_autocomplete,
|
|
false, // hidden
|
|
true, // requires_config
|
|
true, // requires_install
|
|
1, // min_args (template name required)
|
|
1, // max_args
|
|
"validate-template TEMPLATE",
|
|
"Validate a template's structure, syntax, and common issues.",
|
|
R"(
|
|
Validates a dropshell template by checking:
|
|
1. Required files exist (install.sh, uninstall.sh, config files)
|
|
2. Shell script linting via shellcheck (run in container)
|
|
3. Dropshell-specific checks:
|
|
- Scripts source common.sh
|
|
|
|
Example:
|
|
ds validate my-template
|
|
)"
|
|
});
|
|
}
|
|
} validate_command_register;
|
|
|
|
|
|
void validate_autocomplete(const CommandContext& ctx) {
|
|
if (ctx.args.size() == 0) {
|
|
// list all templates
|
|
std::set<std::string> templates = gTemplateManager().get_template_list();
|
|
for (const auto& t : templates) {
|
|
rawout << t << std::endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Validation helper functions
|
|
// ----------------------------------------------------------------------------
|
|
|
|
struct ValidationResult {
|
|
bool passed;
|
|
std::string message;
|
|
std::string details;
|
|
};
|
|
|
|
// Run shellcheck on all shell scripts in a directory using Docker
|
|
// Returns: number of issues found (0 = all passed)
|
|
int run_shellcheck(const std::filesystem::path& template_path, std::vector<std::filesystem::path>& scripts) {
|
|
if (scripts.empty()) {
|
|
return 0;
|
|
}
|
|
|
|
int total_issues = 0;
|
|
|
|
for (const auto& script : scripts) {
|
|
// Get relative path from template_path
|
|
std::string rel_path = std::filesystem::relative(script, template_path).string();
|
|
|
|
// Run shellcheck in container
|
|
// -f gcc gives us parseable output: file:line:col: severity: message
|
|
// -s bash specifies bash dialect
|
|
// -e SC1091 excludes "not following sourced file" warnings (common.sh is external)
|
|
std::string command = "docker run --rm -v " + template_path.string() + ":/mnt:ro koalaman/shellcheck:stable "
|
|
"-f gcc -s bash -e SC1091 /mnt/" + rel_path + " 2>&1";
|
|
|
|
FILE* pipe = popen(command.c_str(), "r");
|
|
if (!pipe) {
|
|
error << "Failed to run shellcheck on " << script.filename() << std::endl;
|
|
total_issues++;
|
|
continue;
|
|
}
|
|
|
|
char buffer[512];
|
|
std::string output;
|
|
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
|
|
output += buffer;
|
|
}
|
|
|
|
int result = pclose(pipe);
|
|
|
|
if (result != 0 && !output.empty()) {
|
|
// Parse and display shellcheck output
|
|
// Format: /mnt/file.sh:line:col: severity: message
|
|
std::istringstream iss(output);
|
|
std::string line;
|
|
int script_issues = 0;
|
|
|
|
while (std::getline(iss, line)) {
|
|
if (line.empty()) continue;
|
|
|
|
// Replace /mnt/ with actual filename for clarity
|
|
size_t mnt_pos = line.find("/mnt/");
|
|
if (mnt_pos != std::string::npos) {
|
|
line = line.substr(mnt_pos + 5); // Skip "/mnt/"
|
|
}
|
|
|
|
// Color based on severity
|
|
if (line.find(": error:") != std::string::npos) {
|
|
error << " " << line << std::endl;
|
|
script_issues++;
|
|
} else if (line.find(": warning:") != std::string::npos) {
|
|
warning << " " << line << std::endl;
|
|
script_issues++;
|
|
} else if (line.find(": note:") != std::string::npos) {
|
|
info << " " << line << std::endl;
|
|
} else {
|
|
warning << " " << line << std::endl;
|
|
script_issues++;
|
|
}
|
|
}
|
|
|
|
if (script_issues > 0) {
|
|
total_issues += script_issues;
|
|
}
|
|
} else {
|
|
info << " ✓ " << script.filename().string() << std::endl;
|
|
}
|
|
}
|
|
|
|
return total_issues;
|
|
}
|
|
|
|
// Check if script sources common.sh (dropshell-specific check)
|
|
ValidationResult check_common_sh_sourced(const std::filesystem::path& script_path) {
|
|
ValidationResult result;
|
|
result.passed = false;
|
|
|
|
std::ifstream file(script_path);
|
|
if (!file.is_open()) {
|
|
result.message = "Could not open " + script_path.filename().string();
|
|
return result;
|
|
}
|
|
|
|
std::string line;
|
|
std::regex source_pattern(R"(source\s+[\"']?\$\{?AGENT_PATH\}?[/\\]+_?common\.sh)");
|
|
std::regex dot_pattern(R"(\.\s+[\"']?\$\{?AGENT_PATH\}?[/\\]+_?common\.sh)");
|
|
|
|
while (std::getline(file, line)) {
|
|
if (std::regex_search(line, source_pattern) || std::regex_search(line, dot_pattern)) {
|
|
result.passed = true;
|
|
result.message = "Sources common.sh: " + script_path.filename().string();
|
|
return result;
|
|
}
|
|
}
|
|
|
|
result.message = "Missing 'source \"${AGENT_PATH}/common.sh\"' in " + script_path.filename().string();
|
|
result.details = "Scripts should source common.sh to use _die, _check_docker_installed, etc.";
|
|
return result;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Main validation handler
|
|
// ----------------------------------------------------------------------------
|
|
|
|
int validate_handler(const CommandContext& ctx) {
|
|
std::string template_name = safearg(ctx.args, 0);
|
|
|
|
if (template_name.empty()) {
|
|
error << "Template name is required" << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
// Get template info
|
|
template_info tinfo = gTemplateManager().get_template_info(template_name);
|
|
if (!tinfo.is_set()) {
|
|
error << "Template not found: " << template_name << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
std::filesystem::path template_path = tinfo.local_template_path();
|
|
|
|
maketitle("Validating template: " + template_name);
|
|
info << "Path: " << template_path.string() << std::endl;
|
|
std::cout << std::endl;
|
|
|
|
int errors = 0;
|
|
int warnings = 0;
|
|
|
|
// Step 1: Run existing template validation
|
|
info << "=== Structure Validation ===" << std::endl;
|
|
if (template_manager::test_template(template_path.string())) {
|
|
info << " ✓ Template structure is valid" << std::endl;
|
|
} else {
|
|
error << " ✗ Template structure validation failed" << std::endl;
|
|
errors++;
|
|
}
|
|
std::cout << std::endl;
|
|
|
|
// Step 2: Find all .sh files
|
|
std::vector<std::filesystem::path> shell_scripts;
|
|
for (const auto& entry : std::filesystem::recursive_directory_iterator(template_path)) {
|
|
if (entry.is_regular_file() && entry.path().extension() == ".sh") {
|
|
shell_scripts.push_back(entry.path());
|
|
}
|
|
}
|
|
|
|
// Step 3: Shellcheck validation (run in container)
|
|
info << "=== Shellcheck Linting ===" << std::endl;
|
|
int shellcheck_issues = run_shellcheck(template_path, shell_scripts);
|
|
if (shellcheck_issues > 0) {
|
|
warnings += shellcheck_issues;
|
|
}
|
|
std::cout << std::endl;
|
|
|
|
// Step 4: Dropshell-specific checks - common.sh sourcing (only for main scripts)
|
|
info << "=== Dropshell: common.sh sourcing ===" << std::endl;
|
|
std::vector<std::string> main_scripts = {"install.sh", "uninstall.sh", "start.sh", "stop.sh", "status.sh", "backup.sh", "restore.sh"};
|
|
for (const auto& script_name : main_scripts) {
|
|
std::filesystem::path script_path = template_path / script_name;
|
|
if (std::filesystem::exists(script_path)) {
|
|
ValidationResult result = check_common_sh_sourced(script_path);
|
|
if (result.passed) {
|
|
info << " ✓ " << result.message << std::endl;
|
|
} else {
|
|
warning << " ⚠ " << result.message << std::endl;
|
|
if (!result.details.empty()) {
|
|
warning << " " << result.details << std::endl;
|
|
}
|
|
warnings++;
|
|
}
|
|
}
|
|
}
|
|
std::cout << std::endl;
|
|
|
|
// Summary
|
|
maketitle("Validation Summary");
|
|
if (errors == 0 && warnings == 0) {
|
|
info << "✓ Template '" << template_name << "' passed all checks!" << std::endl;
|
|
return 0;
|
|
} else if (errors == 0) {
|
|
error << "Template '" << template_name << "' has " << warnings << " warning(s)" << std::endl;
|
|
return 1; // Warnings fail validation
|
|
} else {
|
|
error << "Template '" << template_name << "' has " << errors << " error(s) and " << warnings << " warning(s)" << std::endl;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
} // namespace dropshell
|