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");
|
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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user