diff --git a/dbal/production/src/tools/package_generator/arg_parser.hpp b/dbal/production/src/tools/package_generator/arg_parser.hpp new file mode 100644 index 000000000..e80d89d1e --- /dev/null +++ b/dbal/production/src/tools/package_generator/arg_parser.hpp @@ -0,0 +1,121 @@ +/** + * Command-line argument parser + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace tools { + +class ArgParser { +public: + ArgParser(int argc, char* argv[]) { + parse(argc, argv); + } + + /** + * Get positional argument by index (0-based, excludes flags) + */ + std::string get_positional(size_t index) const { + return index < positional_.size() ? positional_[index] : ""; + } + + /** + * Get option value by name (e.g., --name value) + */ + std::string get_option(const std::string& name, const std::string& default_value = "") const { + auto it = options_.find(name); + return it != options_.end() ? it->second : default_value; + } + + /** + * Get integer option + */ + int get_int_option(const std::string& name, int default_value = 0) const { + auto value = get_option(name); + if (value.empty()) return default_value; + try { + return std::stoi(value); + } catch (...) { + return default_value; + } + } + + /** + * Check if flag is present (e.g., --dry-run) + */ + bool has_flag(const std::string& name) const { + return flags_.count(name) > 0; + } + + /** + * Get comma-separated list option + */ + std::vector get_list_option(const std::string& name) const { + auto value = get_option(name); + if (value.empty()) return {}; + + std::vector result; + std::stringstream ss(value); + std::string item; + + while (std::getline(ss, item, ',')) { + // Trim whitespace + item.erase(0, item.find_first_not_of(" \t")); + item.erase(item.find_last_not_of(" \t") + 1); + if (!item.empty()) { + result.push_back(item); + } + } + + return result; + } + + /** + * Get all positional arguments + */ + const std::vector& positional_args() const { + return positional_; + } + +private: + std::vector positional_; + std::unordered_map options_; + std::unordered_set flags_; + + // Options that take values + const std::unordered_set value_options_ = { + "name", "description", "category", "min-level", + "entities", "components", "deps", "output" + }; + + void parse(int argc, char* argv[]) { + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg.substr(0, 2) == "--") { + auto name = arg.substr(2); + + // Check if this option takes a value + if (value_options_.count(name) && i + 1 < argc) { + options_[name] = argv[++i]; + } else { + flags_.insert(name); + } + } else if (arg[0] == '-') { + // Short flag (e.g., -h, -v) + flags_.insert(arg.substr(1)); + } else { + positional_.push_back(arg); + } + } + } +}; + +} // namespace tools diff --git a/dbal/production/src/tools/package_generator/lua_runner.hpp b/dbal/production/src/tools/package_generator/lua_runner.hpp new file mode 100644 index 000000000..13a0dd79e --- /dev/null +++ b/dbal/production/src/tools/package_generator/lua_runner.hpp @@ -0,0 +1,324 @@ +/** + * Lua Runner - sol2-based Lua script executor + * + * Provides a sandboxed Lua environment for running package template scripts. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#define SOL_ALL_SAFETIES_ON 1 +#include +#include + +namespace fs = std::filesystem; +using json = nlohmann::json; + +namespace tools { + +/** + * Configuration object to pass to Lua functions + */ +class LuaConfig { +public: + json data; + + void set(const std::string& key, const std::string& value) { + if (!value.empty()) { + data[key] = value; + } + } + + void set(const std::string& key, int value) { + data[key] = value; + } + + void set(const std::string& key, bool value) { + data[key] = value; + } + + void set_list(const std::string& key, const std::vector& values) { + if (!values.empty()) { + data[key] = values; + } else { + data[key] = json::array(); + } + } + + static LuaConfig from_json(const std::string& json_str) { + LuaConfig config; + config.data = json::parse(json_str); + return config; + } + + sol::table to_lua_table(sol::state& lua) const { + return json_to_lua(lua, data); + } + +private: + sol::object json_to_lua(sol::state& lua, const json& j) const { + if (j.is_null()) { + return sol::nil; + } else if (j.is_boolean()) { + return sol::make_object(lua, j.get()); + } else if (j.is_number_integer()) { + return sol::make_object(lua, j.get()); + } else if (j.is_number_float()) { + return sol::make_object(lua, j.get()); + } else if (j.is_string()) { + return sol::make_object(lua, j.get()); + } else if (j.is_array()) { + sol::table arr = lua.create_table(); + int idx = 1; + for (const auto& elem : j) { + arr[idx++] = json_to_lua(lua, elem); + } + return arr; + } else if (j.is_object()) { + sol::table obj = lua.create_table(); + for (auto& [key, val] : j.items()) { + obj[key] = json_to_lua(lua, val); + } + return obj; + } + return sol::nil; + } +}; + +/** + * Generated file structure + */ +struct GeneratedFile { + std::string path; + std::string content; +}; + +using GeneratedFiles = std::vector; + +/** + * Validation result from Lua + */ +struct ValidationResult { + bool valid = false; + std::vector errors; +}; + +/** + * Lua script runner with sandboxed environment + */ +class LuaRunner { +public: + explicit LuaRunner(const fs::path& scripts_path) + : scripts_path_(scripts_path) { + initialize(); + } + + /** + * Load a Lua module by name + */ + bool load_module(const std::string& module_name) { + auto module_path = scripts_path_ / module_name / "init.lua"; + if (!fs::exists(module_path)) { + // Try direct file + module_path = scripts_path_ / (module_name + ".lua"); + } + + if (!fs::exists(module_path)) { + return false; + } + + try { + auto result = lua_.safe_script_file(module_path.string()); + if (!result.valid()) { + sol::error err = result; + last_error_ = err.what(); + return false; + } + + // Store the module + loaded_modules_[module_name] = result.get(); + return true; + } catch (const std::exception& e) { + last_error_ = e.what(); + return false; + } + } + + /** + * Call a Lua function with config, returning typed result + */ + template + T call(const std::string& func_path, const LuaConfig& config) { + return call_impl(func_path, config.to_lua_table(lua_)); + } + + /** + * Call a Lua function without arguments + */ + template + T call(const std::string& func_path) { + return call_impl(func_path, sol::nil); + } + + const std::string& last_error() const { return last_error_; } + +private: + sol::state lua_; + fs::path scripts_path_; + std::string last_error_; + std::unordered_map loaded_modules_; + + void initialize() { + // Open safe libraries only (no os, io, debug) + lua_.open_libraries( + sol::lib::base, + sol::lib::string, + sol::lib::table, + sol::lib::math, + sol::lib::utf8 + ); + + // Set up package path + std::string package_path = lua_["package"]["path"]; + package_path += ";" + scripts_path_.string() + "/?.lua"; + package_path += ";" + scripts_path_.string() + "/?/init.lua"; + lua_["package"]["path"] = package_path; + + // Add custom require that respects sandbox + setup_sandboxed_require(); + } + + void setup_sandboxed_require() { + // Override require to prevent loading unsafe modules + lua_.set_function("safe_require", [this](const std::string& name) -> sol::object { + // Block dangerous modules + static const std::vector blocked = { + "os", "io", "debug", "ffi", "package.loadlib" + }; + + for (const auto& b : blocked) { + if (name == b || name.find(b + ".") == 0) { + throw std::runtime_error("Module '" + name + "' is not allowed in sandbox"); + } + } + + // Check if already loaded + if (loaded_modules_.count(name)) { + return loaded_modules_[name]; + } + + // Try to load + if (load_module(name)) { + return loaded_modules_[name]; + } + + throw std::runtime_error("Module '" + name + "' not found"); + }); + + // Replace require + lua_.script(R"( + local original_require = require + require = function(name) + local ok, result = pcall(safe_require, name) + if ok then return result end + return original_require(name) + end + )"); + } + + template + T call_impl(const std::string& func_path, sol::object arg) { + // Parse "module.function" path + auto dot_pos = func_path.find('.'); + if (dot_pos == std::string::npos) { + throw std::runtime_error("Invalid function path: " + func_path); + } + + auto module_name = func_path.substr(0, dot_pos); + auto func_name = func_path.substr(dot_pos + 1); + + if (!loaded_modules_.count(module_name)) { + throw std::runtime_error("Module not loaded: " + module_name); + } + + auto& module = loaded_modules_[module_name]; + sol::function func = module[func_name]; + + if (!func.valid()) { + throw std::runtime_error("Function not found: " + func_path); + } + + sol::protected_function_result result; + if (arg == sol::nil) { + result = func(); + } else { + result = func(arg); + } + + if (!result.valid()) { + sol::error err = result; + throw std::runtime_error(err.what()); + } + + return convert_result(result); + } + + // Specializations for different return types + template + T convert_result(sol::protected_function_result& result); +}; + +// Specialization for GeneratedFiles +template<> +inline GeneratedFiles LuaRunner::convert_result(sol::protected_function_result& result) { + GeneratedFiles files; + sol::table lua_files = result; + + for (auto& pair : lua_files) { + sol::table file_table = pair.second; + GeneratedFile file; + file.path = file_table["path"].get(); + file.content = file_table["content"].get(); + files.push_back(std::move(file)); + } + + return files; +} + +// Specialization for ValidationResult +template<> +inline ValidationResult LuaRunner::convert_result(sol::protected_function_result& result) { + ValidationResult validation; + sol::table lua_result = result; + + validation.valid = lua_result["valid"].get(); + + if (lua_result["errors"].valid()) { + sol::table errors = lua_result["errors"]; + for (auto& pair : errors) { + validation.errors.push_back(pair.second.as()); + } + } + + return validation; +} + +// Specialization for vector +template<> +inline std::vector LuaRunner::convert_result>(sol::protected_function_result& result) { + std::vector vec; + sol::table lua_table = result; + + for (auto& pair : lua_table) { + vec.push_back(pair.second.as()); + } + + return vec; +} + +} // namespace tools diff --git a/dbal/production/src/tools/package_generator/main.cpp b/dbal/production/src/tools/package_generator/main.cpp new file mode 100644 index 000000000..18bc96770 --- /dev/null +++ b/dbal/production/src/tools/package_generator/main.cpp @@ -0,0 +1,280 @@ +/** + * Package Template Generator CLI + * + * C++ CLI that executes Lua scripts to generate MetaBuilder package scaffolding. + * + * Usage: + * package_generator new [options] + * package_generator quick [options] + * package_generator list-categories + * package_generator validate + * + * Examples: + * package_generator new my_package --category tools --min-level 3 + * package_generator quick my_widget --dependency + * package_generator new my_forum --with-schema --entities Thread,Post + */ + +#include +#include +#include +#include +#include +#include + +#include "lua_runner.hpp" +#include "arg_parser.hpp" +#include "file_writer.hpp" + +namespace fs = std::filesystem; + +constexpr const char* VERSION = "1.0.0"; +constexpr const char* PROGRAM_NAME = "package_generator"; + +void print_help() { + std::cout << R"( +Package Template Generator v)" << VERSION << R"( + +USAGE: + )" << PROGRAM_NAME << R"( [options] + +COMMANDS: + new Create a new package with full scaffolding + quick Create a minimal package quickly + list-categories List available package categories + validate Validate a package config JSON file + help Show this help message + +OPTIONS: + --name Display name (default: derived from package_id) + --description Package description + --category Package category (default: ui) + --min-level Minimum access level 0-6 (default: 2) + --primary Package can own routes (default) + --dependency Package is dependency-only + --with-schema Include database schema scaffolding + --entities Entity names for schema (comma-separated) + --with-components Include component scaffolding + --components Component names (comma-separated) + --deps Package dependencies (comma-separated) + --output Output directory (default: ./packages) + --dry-run Preview files without writing + +CATEGORIES: + ui, editors, tools, social, media, gaming, + admin, config, core, demo, development, managers + +EXAMPLES: + )" << PROGRAM_NAME << R"( new my_package + )" << PROGRAM_NAME << R"( new my_forum --with-schema --entities Thread,Post,Reply + )" << PROGRAM_NAME << R"( quick my_widget --dependency --category ui + )" << PROGRAM_NAME << R"( new dashboard --with-components --components StatCard,Chart + +)"; +} + +void print_version() { + std::cout << PROGRAM_NAME << " version " << VERSION << std::endl; +} + +int cmd_new(const tools::ArgParser& args, tools::LuaRunner& lua) { + auto package_id = args.get_positional(1); + if (package_id.empty()) { + std::cerr << "Error: package_id is required\n"; + std::cerr << "Usage: " << PROGRAM_NAME << " new [options]\n"; + return 1; + } + + // Build config table for Lua + tools::LuaConfig config; + config.set("packageId", package_id); + config.set("name", args.get_option("name", "")); + config.set("description", args.get_option("description", "")); + config.set("category", args.get_option("category", "ui")); + config.set("minLevel", args.get_int_option("min-level", 2)); + config.set("primary", !args.has_flag("dependency")); + config.set("withSchema", args.has_flag("with-schema")); + config.set("withTests", true); + config.set("withComponents", args.has_flag("with-components")); + + // Parse comma-separated lists + config.set_list("entities", args.get_list_option("entities")); + config.set_list("components", args.get_list_option("components")); + config.set_list("dependencies", args.get_list_option("deps")); + + // Validate config via Lua + auto validation = lua.call("package_template.validate_config", config); + if (!validation.valid) { + std::cerr << "Validation failed:\n"; + for (const auto& err : validation.errors) { + std::cerr << " - " << err << "\n"; + } + return 1; + } + + // Generate files + auto files = lua.call("package_template.generate", config); + + if (args.has_flag("dry-run")) { + std::cout << "Would generate " << files.size() << " files:\n"; + for (const auto& file : files) { + std::cout << " " << file.path << " (" << file.content.size() << " bytes)\n"; + } + return 0; + } + + // Write files + auto output_dir = args.get_option("output", "packages"); + auto package_path = fs::path(output_dir) / package_id; + + if (fs::exists(package_path)) { + std::cerr << "Error: Package directory already exists: " << package_path << "\n"; + return 1; + } + + tools::FileWriter writer(package_path); + int written = writer.write_all(files); + + std::cout << "\nāœ… Package '" << package_id << "' created successfully!\n"; + std::cout << " Location: " << package_path << "\n"; + std::cout << " Files: " << written << "\n"; + std::cout << "\nNext steps:\n"; + std::cout << " 1. Review generated files in " << package_path << "\n"; + std::cout << " 2. Add package-specific logic to seed/scripts/\n"; + std::cout << " 3. Run tests: npm run test:package " << package_id << "\n"; + + return 0; +} + +int cmd_quick(const tools::ArgParser& args, tools::LuaRunner& lua) { + auto package_id = args.get_positional(1); + if (package_id.empty()) { + std::cerr << "Error: package_id is required\n"; + return 1; + } + + // Minimal config + tools::LuaConfig config; + config.set("packageId", package_id); + config.set("category", args.get_option("category", "ui")); + config.set("minLevel", args.get_int_option("min-level", 2)); + config.set("primary", !args.has_flag("dependency")); + config.set("withSchema", false); + config.set("withTests", false); + config.set("withComponents", false); + + auto files = lua.call("package_template.generate_quick", config); + + if (args.has_flag("dry-run")) { + std::cout << "Would generate " << files.size() << " files:\n"; + for (const auto& file : files) { + std::cout << " " << file.path << "\n"; + } + return 0; + } + + auto output_dir = args.get_option("output", "packages"); + auto package_path = fs::path(output_dir) / package_id; + + if (fs::exists(package_path)) { + std::cerr << "Error: Package directory already exists: " << package_path << "\n"; + return 1; + } + + tools::FileWriter writer(package_path); + writer.write_all(files); + + std::cout << "āœ… Package '" << package_id << "' created (quick mode)\n"; + return 0; +} + +int cmd_list_categories(tools::LuaRunner& lua) { + auto categories = lua.call>("package_template.get_categories"); + + std::cout << "Available package categories:\n\n"; + for (const auto& cat : categories) { + std::cout << " - " << cat << "\n"; + } + return 0; +} + +int cmd_validate(const tools::ArgParser& args, tools::LuaRunner& lua) { + auto config_file = args.get_positional(1); + if (config_file.empty()) { + std::cerr << "Error: config file path is required\n"; + return 1; + } + + if (!fs::exists(config_file)) { + std::cerr << "Error: File not found: " << config_file << "\n"; + return 1; + } + + // Read JSON config + std::ifstream file(config_file); + std::stringstream buffer; + buffer << file.rdbuf(); + + auto config = tools::LuaConfig::from_json(buffer.str()); + auto validation = lua.call("package_template.validate_config", config); + + if (validation.valid) { + std::cout << "āœ… Configuration is valid\n"; + return 0; + } else { + std::cout << "āŒ Configuration has errors:\n"; + for (const auto& err : validation.errors) { + std::cout << " - " << err << "\n"; + } + return 1; + } +} + +int main(int argc, char* argv[]) { + tools::ArgParser args(argc, argv); + + if (args.has_flag("help") || args.has_flag("h") || argc < 2) { + print_help(); + return 0; + } + + if (args.has_flag("version") || args.has_flag("v")) { + print_version(); + return 0; + } + + auto command = args.get_positional(0); + + // Initialize Lua runner with package scripts path + auto scripts_path = fs::current_path() / "packages" / "codegen_studio" / "seed" / "scripts"; + if (!fs::exists(scripts_path)) { + // Try relative to executable + auto exe_path = fs::path(argv[0]).parent_path(); + scripts_path = exe_path / ".." / ".." / "packages" / "codegen_studio" / "seed" / "scripts"; + } + + tools::LuaRunner lua(scripts_path); + + if (!lua.load_module("package_template")) { + std::cerr << "Error: Failed to load package_template module\n"; + std::cerr << "Looked in: " << scripts_path << "\n"; + return 1; + } + + if (command == "new") { + return cmd_new(args, lua); + } else if (command == "quick") { + return cmd_quick(args, lua); + } else if (command == "list-categories") { + return cmd_list_categories(lua); + } else if (command == "validate") { + return cmd_validate(args, lua); + } else if (command == "help") { + print_help(); + return 0; + } else { + std::cerr << "Unknown command: " << command << "\n"; + std::cerr << "Run '" << PROGRAM_NAME << " help' for usage\n"; + return 1; + } +}