feat: Add add-template command to register local template sources
This commit is contained in:
200
source/src/commands/add-template.cpp
Normal file
200
source/src/commands/add-template.cpp
Normal 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
|
||||
@@ -158,6 +158,7 @@ int help_handler(const CommandContext& ctx) {
|
||||
show_command("create-template");
|
||||
info << std::endl;
|
||||
show_command("validate-template");
|
||||
show_command("add-template");
|
||||
info << std::endl;
|
||||
show_command("pull");
|
||||
}
|
||||
|
||||
@@ -169,7 +169,8 @@
|
||||
}
|
||||
|
||||
// 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
|
||||
std::string rel = std::filesystem::relative(it->path(), root).string();
|
||||
result.push_back(rel);
|
||||
|
||||
@@ -70,13 +70,15 @@ class template_manager {
|
||||
bool pull_all();
|
||||
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:
|
||||
static bool required_file(std::string path, std::string template_name);
|
||||
void resolve_templates();
|
||||
void resolve_source(size_t source_index);
|
||||
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_clone_source(const TemplateSource& source);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user