feat: Add add-template command to register local template sources
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 40s
Build-Test-Publish / build (linux/arm64) (push) Successful in 2m45s

This commit is contained in:
j
2026-03-23 09:56:23 +13:00
parent 5217ad42d3
commit 63fdc2c0cb
4 changed files with 207 additions and 3 deletions

View File

@@ -0,0 +1,200 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "templates.hpp"
#include "utils/directories.hpp"
#include "utils/output.hpp"
#include "utils/utils.hpp"
#include <iostream>
#include <fstream>
#include <filesystem>
#define JSON_INLINE_ALL
#include <nlohmann/json.hpp>
namespace dropshell {
int add_template_handler(const CommandContext& ctx);
static std::vector<std::string> add_template_name_list = {"add-template"};
struct AddTemplateCommandRegister {
AddTemplateCommandRegister() {
CommandRegistry::instance().register_command({
add_template_name_list,
add_template_handler,
nullptr,
false, // hidden
true, // requires_config
false, // requires_install
0, // min_args
1, // max_args (optional path, defaults to cwd)
"add-template [PATH]",
"Register the current directory (or PATH) as a template source.",
R"(
Adds a directory to template_paths.json as a template source.
add-template Register current directory (or its git repo root)
add-template PATH Register the given directory (or its git repo root)
The command will:
1. Walk up to the git repo root (if inside a git repo)
2. Verify dropshell templates exist in the tree
3. Auto-detect the git remote URL
4. Check the path (or a parent) isn't already registered
5. Add the entry to template_paths.json
6. Create dropshell-templates.list if it doesn't already exist
)"
});
}
} add_template_command_register;
// Find the git repo root containing dir_path, or return empty if not in a git repo.
static std::filesystem::path find_git_root(const std::filesystem::path& dir_path) {
std::string cmd = "git -C " + quote(dir_path.string()) + " rev-parse --show-toplevel 2>/dev/null";
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) return {};
std::string result;
char buffer[512];
while (fgets(buffer, sizeof(buffer), pipe))
result += buffer;
int ret = pclose(pipe);
if (ret != 0) return {};
// Trim
while (!result.empty() && (result.back() == '\n' || result.back() == '\r'))
result.pop_back();
if (result.empty()) return {};
return std::filesystem::canonical(result);
}
// Extract the LOCAL_PATH from a template_paths.json entry string
static std::string entry_local_path(const std::string& entry) {
size_t colon = entry.find(':');
return (colon != std::string::npos) ? entry.substr(0, colon) : entry;
}
// Check if candidate is equal to or a child of parent
static bool is_same_or_child(const std::string& candidate, const std::string& parent) {
if (candidate == parent) return true;
// Check if candidate starts with parent + "/"
return candidate.size() > parent.size() &&
candidate.compare(0, parent.size(), parent) == 0 &&
candidate[parent.size()] == '/';
}
int add_template_handler(const CommandContext& ctx) {
// 1. Determine the starting directory
std::filesystem::path start_path;
if (ctx.args.empty())
start_path = std::filesystem::current_path();
else
start_path = std::filesystem::path(ctx.args[0]);
if (start_path.is_relative())
start_path = std::filesystem::current_path() / start_path;
std::error_code ec;
start_path = std::filesystem::canonical(start_path, ec);
if (ec || !std::filesystem::is_directory(start_path)) {
error << "Not a valid directory: " << start_path << std::endl;
return 1;
}
// 2. Walk up to git repo root if we're inside a git repo
std::filesystem::path dir_path = start_path;
std::filesystem::path git_root = find_git_root(start_path);
if (!git_root.empty()) {
if (git_root != start_path)
info << "Using git repo root: " << git_root.string() << std::endl;
dir_path = git_root;
}
std::string local_path = dir_path.string();
// 3. Check for templates before doing anything else
auto templates = gTemplateManager().discover_templates(local_path);
if (templates.empty()) {
error << "No dropshell templates found in " << local_path << std::endl;
info << " (looked for directories containing " << filenames::template_info_env << ")" << std::endl;
return 1;
}
// 4. Find the template_paths.json file location
auto server_def_paths = gConfig().get_local_server_definition_paths();
if (server_def_paths.empty()) {
error << "No server definition paths configured" << std::endl;
return 1;
}
std::filesystem::path json_path = std::filesystem::path(server_def_paths[0]) / filenames::template_paths_json;
// 5. Load existing template_paths.json (or start fresh)
nlohmann::json j = nlohmann::json::array();
if (std::filesystem::exists(json_path)) {
std::ifstream f(json_path);
if (f.is_open()) {
try {
j = nlohmann::json::parse(f);
} catch (...) {
error << "Failed to parse " << json_path << std::endl;
return 1;
}
}
}
// 6. Check if this path or a parent is already registered
for (const auto& entry : j) {
if (!entry.is_string()) continue;
std::string existing = entry_local_path(entry.get<std::string>());
if (is_same_or_child(local_path, existing)) {
info << "Already covered by existing entry: " << existing << std::endl;
return 0;
}
}
// 7. Auto-detect git remote URL
std::string git_url;
if (!git_root.empty()) {
std::string cmd = "git -C " + quote(local_path) + " remote get-url origin 2>/dev/null";
FILE* pipe = popen(cmd.c_str(), "r");
if (pipe) {
char buffer[512];
while (fgets(buffer, sizeof(buffer), pipe))
git_url += buffer;
pclose(pipe);
while (!git_url.empty() && (git_url.back() == '\n' || git_url.back() == '\r'))
git_url.pop_back();
}
}
// 8. Build entry and add
std::string new_entry = local_path;
if (!git_url.empty())
new_entry += ":" + git_url;
j.push_back(new_entry);
// 9. Write template_paths.json
std::filesystem::create_directories(json_path.parent_path());
std::ofstream out(json_path);
if (!out.is_open()) {
error << "Failed to write " << json_path << std::endl;
return 1;
}
out << j.dump(4) << std::endl;
out.close();
info << "Added to " << json_path << ":" << std::endl;
info << " " << new_entry << std::endl;
// 10. Create dropshell-templates.list if it doesn't exist
std::filesystem::path list_file = dir_path / filenames::dropshell_templates_list;
if (!std::filesystem::exists(list_file)) {
gTemplateManager().write_list_file(local_path, templates);
} else {
info << "Using existing " << list_file << std::endl;
}
info << "Found " << templates.size() << " template(s)" << std::endl;
return 0;
}
} // namespace dropshell

View File

@@ -158,6 +158,7 @@ int help_handler(const CommandContext& ctx) {
show_command("create-template"); show_command("create-template");
info << std::endl; info << std::endl;
show_command("validate-template"); show_command("validate-template");
show_command("add-template");
info << std::endl; info << std::endl;
show_command("pull"); show_command("pull");
} }

View File

@@ -169,7 +169,8 @@
} }
// Check if this directory contains template_info.env // Check if this directory contains template_info.env
if (std::filesystem::exists(it->path() / filenames::template_info_env)) { std::error_code ec;
if (std::filesystem::exists(it->path() / filenames::template_info_env, ec) && !ec) {
// Get relative path from root // Get relative path from root
std::string rel = std::filesystem::relative(it->path(), root).string(); std::string rel = std::filesystem::relative(it->path(), root).string();
result.push_back(rel); result.push_back(rel);

View File

@@ -70,13 +70,15 @@ class template_manager {
bool pull_all(); bool pull_all();
bool pull_for_template(const std::string& template_name); bool pull_for_template(const std::string& template_name);
// Template discovery and list file management
std::vector<std::string> discover_templates(const std::string& local_path) const;
void write_list_file(const std::string& local_path, const std::vector<std::string>& template_dirs) const;
private: private:
static bool required_file(std::string path, std::string template_name); static bool required_file(std::string path, std::string template_name);
void resolve_templates(); void resolve_templates();
void resolve_source(size_t source_index); void resolve_source(size_t source_index);
std::vector<std::string> parse_list_file(const std::string& local_path) const; std::vector<std::string> parse_list_file(const std::string& local_path) const;
std::vector<std::string> discover_templates(const std::string& local_path) const;
void write_list_file(const std::string& local_path, const std::vector<std::string>& template_dirs) const;
bool git_pull_source(const TemplateSource& source); bool git_pull_source(const TemplateSource& source);
bool git_clone_source(const TemplateSource& source); bool git_clone_source(const TemplateSource& source);