diff --git a/source/src/commands/publish-template.cpp b/source/src/commands/publish-template.cpp index f8f5164..86032be 100644 --- a/source/src/commands/publish-template.cpp +++ b/source/src/commands/publish-template.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace dropshell { @@ -31,9 +32,9 @@ struct PublishTemplateCommandRegister { false, // hidden true, // requires_config true, // requires_install - 1, // min_args (directory required) - 2, // max_args (optional registry name + directory) - "publish-template [REGISTRY] DIRECTORY", + 1, // min_args + 3, // max_args (--all + optional registry + directory) + "publish-template [--all] [REGISTRY] DIRECTORY", "Publish a template to a template registry.", R"HELP( Publishes a template directory to a template registry. @@ -41,11 +42,15 @@ Publishes a template directory to a template registry. Usage: ds publish-template DIRECTORY ds publish-template REGISTRY_NAME DIRECTORY + ds publish-template --all DIRECTORY + ds publish-template --all REGISTRY_NAME DIRECTORY Arguments: + --all Publish all templates in subdirectories of DIRECTORY. + Only publishes subdirectories containing config/.template_info.env REGISTRY_NAME Optional. Name of the template registry to publish to. If not specified, uses the first registry with a token. - DIRECTORY Path to the template directory to publish. + DIRECTORY Path to the template directory (or parent directory with --all). The template is validated before publishing. Two tags are created: - YYYYMMDD (e.g., 20251228) @@ -64,6 +69,7 @@ Requirements: Example: ds publish-template ./my-template ds publish-template main ./my-template + ds publish-template --all ./templates-dir SOS_WRITE_TOKEN=xxx ds publish-template ./my-template )HELP" }); @@ -73,6 +79,7 @@ Example: void publish_template_autocomplete(const CommandContext& ctx) { if (ctx.args.size() == 0) { + rawout << "--all" << std::endl; // List registry names std::vector registries = gConfig().get_template_registry_urls(); for (const auto& reg : registries) { @@ -262,43 +269,140 @@ static bool publish_to_registry(const std::string& server_url, const std::string } } -int publish_template_handler(const CommandContext& ctx) { - std::string registry_name; - std::string template_dir; +// Check if a directory is a valid template (has config/.template_info.env) +static bool is_valid_template_dir(const std::filesystem::path& dir_path) { + return std::filesystem::exists(dir_path / "config" / ".template_info.env"); +} - // Parse arguments - if (ctx.args.size() == 1) { - // Only directory provided - template_dir = ctx.args[0]; - } else if (ctx.args.size() == 2) { - // Registry name and directory provided - registry_name = ctx.args[0]; - template_dir = ctx.args[1]; - } else { - error << "Usage: ds publish-template [REGISTRY] DIRECTORY" << std::endl; +// Publish a single template directory +// Returns: 0 = success, 1 = error, 2 = skipped (not a template) +static int publish_single_template(const std::string& template_dir, const std::string& server_url, + const std::string& token, bool quiet = false) { + std::filesystem::path dir_path(template_dir); + std::string template_name = dir_path.filename().string(); + + if (!quiet) { + maketitle("Publishing template: " + template_name); + info << "Directory: " << template_dir << std::endl; + std::cout << std::endl; + } + + // Validate template + if (!quiet) info << "=== Validating Template ===" << std::endl; + if (!template_manager::test_template(template_dir)) { + error << "Template '" << template_name << "' validation failed." << std::endl; + if (!quiet) info << "Run: ds validate-template " << template_dir << std::endl; + return 1; + } + if (!quiet) info << " ✓ Template is valid" << std::endl; + if (!quiet) std::cout << std::endl; + + // Calculate directory hash + if (!quiet) info << "=== Calculating Hash ===" << std::endl; + std::string unpacked_hash = hash_directory_recursive(template_dir); + if (unpacked_hash.empty()) { + error << "Failed to calculate directory hash for " << template_name << std::endl; + return 1; + } + if (!quiet) info << " Hash: " << unpacked_hash << std::endl; + if (!quiet) std::cout << std::endl; + + // Create temp directory and tarball + std::string temp_dir = std::filesystem::temp_directory_path().string() + "/dropshell-publish-" + std::to_string(getpid()); + std::filesystem::create_directories(temp_dir); + + // Cleanup on exit + struct TempDirCleaner { + std::string path; + ~TempDirCleaner() { + std::filesystem::remove_all(path); + } + } cleaner{temp_dir}; + + if (!quiet) info << "=== Creating Package ===" << std::endl; + std::string tarball_path = temp_dir + "/" + template_name + ".tgz"; + if (!create_tarball(template_dir, tarball_path)) { + error << "Failed to create tarball for " << template_name << std::endl; return 1; } - // Resolve template directory to absolute path - std::filesystem::path dir_path(template_dir); + auto file_size = std::filesystem::file_size(tarball_path); + if (!quiet) info << " Created " << template_name << ".tgz (" << (file_size / 1024) << " KB)" << std::endl; + if (!quiet) std::cout << std::endl; + + // Publish with tags + if (!quiet) info << "=== Publishing to Registry ===" << std::endl; + + std::string date_tag = get_date_tag(); + std::vector labeltags = { + template_name + ":" + date_tag, + template_name + ":latest" + }; + + if (!quiet) info << " Tags: " << date_tag << ", latest" << std::endl; + if (!quiet) info << " Uploading..." << std::endl; + + if (!publish_to_registry(server_url, token, tarball_path, labeltags, unpacked_hash)) { + error << "Failed to publish template " << template_name << std::endl; + return 1; + } + + if (!quiet) { + info << " ✓ Published successfully" << std::endl; + std::cout << std::endl; + } + + return 0; +} + +int publish_template_handler(const CommandContext& ctx) { + bool publish_all = false; + std::string registry_name; + std::string target_dir; + + // Parse arguments - check for --all flag + std::vector args; + for (const auto& arg : ctx.args) { + if (arg == "--all" || arg == "-a" || arg == "all") { + publish_all = true; + } else { + args.push_back(arg); + } + } + + // Parse remaining arguments + if (args.size() == 1) { + target_dir = args[0]; + } else if (args.size() == 2) { + registry_name = args[0]; + target_dir = args[1]; + } else if (args.empty()) { + error << "Usage: ds publish-template [--all] [REGISTRY] DIRECTORY" << std::endl; + return 1; + } else { + error << "Too many arguments" << std::endl; + error << "Usage: ds publish-template [--all] [REGISTRY] DIRECTORY" << std::endl; + return 1; + } + + // Resolve directory to absolute path + std::filesystem::path dir_path(target_dir); if (dir_path.is_relative()) { dir_path = std::filesystem::current_path() / dir_path; } dir_path = std::filesystem::canonical(dir_path); - template_dir = dir_path.string(); + target_dir = dir_path.string(); // Check directory exists if (!std::filesystem::exists(dir_path) || !std::filesystem::is_directory(dir_path)) { - error << "Directory not found: " << template_dir << std::endl; + error << "Directory not found: " << target_dir << std::endl; return 1; } - std::string template_name = dir_path.filename().string(); - // Find registry and token std::vector registries = gConfig().get_template_registry_urls(); tRegistryEntry* selected_registry = nullptr; - std::string effective_token; // Token to use (may come from config or env var) + std::string effective_token; if (registry_name.empty()) { // Find first registry with a token @@ -367,81 +471,79 @@ int publish_template_handler(const CommandContext& ctx) { server_url.pop_back(); } - maketitle("Publishing template: " + template_name); - info << "Directory: " << template_dir << std::endl; - info << "Registry: " << selected_registry->name << " (" << server_url << ")" << std::endl; - std::cout << std::endl; + if (publish_all) { + // Publish all templates in subdirectories + maketitle("Publishing all templates"); + info << "Parent directory: " << target_dir << std::endl; + info << "Registry: " << selected_registry->name << " (" << server_url << ")" << std::endl; + std::cout << std::endl; - // Step 1: Validate template - info << "=== Validating Template ===" << std::endl; - if (!template_manager::test_template(template_dir)) { - error << "Template validation failed. Please fix issues before publishing." << std::endl; - info << "Run: ds validate-template " << template_dir << std::endl; - return 1; - } - info << " ✓ Template is valid" << std::endl; - std::cout << std::endl; - - // Step 2: Calculate directory hash (for unpacked content verification) - info << "=== Calculating Hash ===" << std::endl; - std::string unpacked_hash = hash_directory_recursive(template_dir); - if (unpacked_hash.empty()) { - error << "Failed to calculate directory hash" << std::endl; - return 1; - } - info << " Hash: " << unpacked_hash << std::endl; - std::cout << std::endl; - - // Step 3: Create temp directory and tarball - std::string temp_dir = std::filesystem::temp_directory_path().string() + "/dropshell-publish-" + std::to_string(getpid()); - std::filesystem::create_directories(temp_dir); - - // Cleanup on exit - struct TempDirCleaner { - std::string path; - ~TempDirCleaner() { - std::filesystem::remove_all(path); + // Find all subdirectories that are valid templates + std::vector template_dirs; + for (const auto& entry : std::filesystem::directory_iterator(dir_path)) { + if (entry.is_directory() && is_valid_template_dir(entry.path())) { + template_dirs.push_back(entry.path()); + } } - } cleaner{temp_dir}; - info << "=== Creating Package ===" << std::endl; - std::string tarball_path = temp_dir + "/" + template_name + ".tgz"; - if (!create_tarball(template_dir, tarball_path)) { - error << "Failed to create tarball" << std::endl; - return 1; + // Sort alphabetically + std::sort(template_dirs.begin(), template_dirs.end()); + + if (template_dirs.empty()) { + error << "No valid templates found in " << target_dir << std::endl; + info << "Templates must have config/.template_info.env" << std::endl; + return 1; + } + + info << "Found " << template_dirs.size() << " template(s) to publish:" << std::endl; + for (const auto& tdir : template_dirs) { + info << " - " << tdir.filename().string() << std::endl; + } + std::cout << std::endl; + + int success_count = 0; + int fail_count = 0; + std::vector failed_templates; + + for (const auto& tdir : template_dirs) { + int result = publish_single_template(tdir.string(), server_url, effective_token, false); + if (result == 0) { + success_count++; + } else { + fail_count++; + failed_templates.push_back(tdir.filename().string()); + } + } + + // Summary + std::cout << std::endl; + maketitle("Publish Summary"); + info << "Successfully published: " << success_count << " template(s)" << std::endl; + if (fail_count > 0) { + error << "Failed to publish: " << fail_count << " template(s)" << std::endl; + for (const auto& name : failed_templates) { + error << " - " << name << std::endl; + } + return 1; + } + + return 0; + } else { + // Single template publish + info << "Registry: " << selected_registry->name << " (" << server_url << ")" << std::endl; + + int result = publish_single_template(target_dir, server_url, effective_token, false); + + if (result == 0) { + std::string template_name = dir_path.filename().string(); + maketitle("Publish Complete"); + info << "Template '" << template_name << "' published successfully!" << std::endl; + info << "Tags: " << get_date_tag() << ", latest" << std::endl; + info << "URL: " << server_url << "/" << template_name << ":latest" << std::endl; + } + + return result; } - - auto file_size = std::filesystem::file_size(tarball_path); - info << " Created " << template_name << ".tgz (" << (file_size / 1024) << " KB)" << std::endl; - std::cout << std::endl; - - // Step 4: Publish with tags - info << "=== Publishing to Registry ===" << std::endl; - - std::string date_tag = get_date_tag(); - std::vector labeltags = { - template_name + ":" + date_tag, - template_name + ":latest" - }; - - info << " Tags: " << date_tag << ", latest" << std::endl; - info << " Uploading..." << std::endl; - - if (!publish_to_registry(server_url, effective_token, tarball_path, labeltags, unpacked_hash)) { - error << "Failed to publish template" << std::endl; - return 1; - } - - info << " ✓ Published successfully" << std::endl; - std::cout << std::endl; - - // Summary - maketitle("Publish Complete"); - info << "Template '" << template_name << "' published successfully!" << std::endl; - info << "Tags: " << date_tag << ", latest" << std::endl; - info << "URL: " << server_url << "/" << template_name << ":latest" << std::endl; - - return 0; } } // namespace dropshell diff --git a/source/src/commands/validate.cpp b/source/src/commands/validate.cpp index 08d297a..bc5066d 100644 --- a/source/src/commands/validate.cpp +++ b/source/src/commands/validate.cpp @@ -254,8 +254,8 @@ int validate_handler(const CommandContext& ctx) { info << "✓ Template '" << template_name << "' passed all checks!" << std::endl; return 0; } else if (errors == 0) { - warning << "Template '" << template_name << "' has " << warnings << " warning(s)" << std::endl; - return 0; // Warnings don't fail validation + 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;