mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
Remove DBAL and package command implementations
- Deleted dbal_commands.h and package_commands.h files, removing DBAL command handlers and package command definitions. - Removed package_commands.cpp, which contained the implementation of package commands including list, run, and generate functionalities. - Eliminated lua_runner.cpp and lua_runner.h, which provided the Lua script execution environment and related functionalities. - Removed main.cpp and http_client files, which were responsible for the CLI entry point and HTTP client interactions.
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.27)
|
||||
project(metabuilder_cli VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
include(${CMAKE_BINARY_DIR}/conan_toolchain.cmake OPTIONAL)
|
||||
|
||||
add_executable(metabuilder-cli
|
||||
src/main.cpp
|
||||
src/commands/command_dispatch.cpp
|
||||
src/commands/dbal_commands.cpp
|
||||
src/commands/package_commands.cpp
|
||||
src/lua/lua_runner.cpp
|
||||
src/utils/http_client.cpp
|
||||
)
|
||||
|
||||
find_package(cpr CONFIG REQUIRED)
|
||||
find_package(lua REQUIRED)
|
||||
find_package(sol2 REQUIRED)
|
||||
find_package(nlohmann_json REQUIRED)
|
||||
|
||||
target_link_libraries(metabuilder-cli PRIVATE
|
||||
cpr::cpr
|
||||
lua::lua
|
||||
sol2::sol2
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
target_compile_features(metabuilder-cli PRIVATE cxx_std_20)
|
||||
target_include_directories(metabuilder-cli PRIVATE src)
|
||||
|
||||
install(TARGETS metabuilder-cli
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
@@ -1,114 +0,0 @@
|
||||
# MetaBuilder CLI
|
||||
|
||||
This CLI targets MetaBuilder services via HTTP and includes a Lua runtime for executing package scripts. Uses Conan + CMake for dependency management and builds.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Conan 2](https://docs.conan.io/) (used for dependency resolution)
|
||||
- CMake 3.27+ (the Conan toolchain generator targets this minimum)
|
||||
- Ninja (build backend)
|
||||
- A running MetaBuilder frontend (defaults to `http://localhost:3000`)
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cd frontends/cli
|
||||
conan install . --output-folder build --build missing
|
||||
cmake -S . -B build -G Ninja
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
Conan provisions these dependencies:
|
||||
- [`cpr`](https://github.com/libcpr/cpr) - HTTP requests
|
||||
- [`lua`](https://www.lua.org/) - Lua 5.4 interpreter
|
||||
- [`sol2`](https://github.com/ThePhD/sol2) - C++/Lua binding
|
||||
- [`nlohmann_json`](https://github.com/nlohmann/json) - JSON handling
|
||||
|
||||
## Running
|
||||
|
||||
The executable looks for `METABUILDER_BASE_URL` (default `http://localhost:3000`):
|
||||
|
||||
```bash
|
||||
# API commands
|
||||
./build/bin/metabuilder-cli auth session
|
||||
./build/bin/metabuilder-cli user list
|
||||
|
||||
# Package commands (run from project root)
|
||||
./build/bin/metabuilder-cli package list
|
||||
./build/bin/metabuilder-cli package generate my_forum --category social --with-schema --entities Thread,Post
|
||||
./build/bin/metabuilder-cli package run codegen_studio package_template get_categories
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### API Commands
|
||||
|
||||
```bash
|
||||
metabuilder-cli auth session # Show current session
|
||||
metabuilder-cli auth login <email> <password> # Authenticate
|
||||
metabuilder-cli user list # List users
|
||||
metabuilder-cli user get <userId> # Get user by ID
|
||||
metabuilder-cli tenant list # List tenants
|
||||
metabuilder-cli tenant get <tenantId> # Get tenant by ID
|
||||
metabuilder-cli dbal <subcommand> # DBAL operations
|
||||
```
|
||||
|
||||
### Package Commands
|
||||
|
||||
```bash
|
||||
metabuilder-cli package list # List packages with scripts
|
||||
metabuilder-cli package run <pkg> <script> # Run a Lua script from a package
|
||||
metabuilder-cli package generate <id> [opts] # Generate a new package
|
||||
```
|
||||
|
||||
#### Generate Options
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `--name <name>` | Display name | Derived from package_id |
|
||||
| `--description <desc>` | Package description | Auto-generated |
|
||||
| `--category <cat>` | Package category | `ui` |
|
||||
| `--min-level <n>` | Minimum access level 0-6 | `2` |
|
||||
| `--primary` | Package can own routes | Yes |
|
||||
| `--dependency` | Package is dependency-only | No |
|
||||
| `--with-schema` | Include database schema | No |
|
||||
| `--entities <e1,e2>` | Entity names (comma-separated) | None |
|
||||
| `--with-components` | Include component scaffolding | No |
|
||||
| `--components <c1,c2>` | Component names | None |
|
||||
| `--deps <d1,d2>` | Package dependencies | None |
|
||||
| `--output <dir>` | Output directory | `./packages` |
|
||||
| `--dry-run` | Preview without writing | No |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Generate a forum package with schema
|
||||
metabuilder-cli package generate my_forum \
|
||||
--category social \
|
||||
--with-schema \
|
||||
--entities ForumThread,ForumPost,ForumReply
|
||||
|
||||
# Generate a UI widget as dependency
|
||||
metabuilder-cli package generate stat_widget \
|
||||
--category ui \
|
||||
--dependency \
|
||||
--with-components \
|
||||
--components StatCard,StatChart
|
||||
|
||||
# Preview without creating files
|
||||
metabuilder-cli package generate test_pkg --dry-run
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `METABUILDER_BASE_URL` | API base URL | `http://localhost:3000` |
|
||||
| `METABUILDER_PACKAGES` | Packages directory | `./packages` |
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
Changes under `frontends/cli/` trigger `.github/workflows/ci/cli.yml`, which:
|
||||
1. Runs Conan to install dependencies
|
||||
2. Configures and builds with CMake/Ninja
|
||||
3. Validates that `metabuilder-cli --help` exits cleanly
|
||||
@@ -1,16 +0,0 @@
|
||||
[requires]
|
||||
cpr/1.10.0
|
||||
lua/5.4.7
|
||||
sol2/3.3.1
|
||||
nlohmann_json/3.11.3
|
||||
|
||||
[generators]
|
||||
CMakeDeps
|
||||
CMakeToolchain
|
||||
|
||||
[options]
|
||||
cpr:ssl_backend=openssl
|
||||
lua/*:shared=False
|
||||
|
||||
[tool_requires]
|
||||
cmake/3.27.1
|
||||
@@ -1,146 +0,0 @@
|
||||
#include "command_dispatch.h"
|
||||
#include "dbal_commands.h"
|
||||
#include "package_commands.h"
|
||||
|
||||
#include <cpr/cpr.h>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
void print_help() {
|
||||
std::cout << R"(Usage: metabuilder-cli <command> [options]
|
||||
Available commands:
|
||||
auth session Show the current authentication session
|
||||
auth login <email> <password> Authenticate with credentials
|
||||
user list List all users
|
||||
user get <userId> Get a user by ID
|
||||
tenant list List all tenants
|
||||
tenant get <tenantId> Get a tenant by ID
|
||||
dbal <subcommand> DBAL operations (use 'dbal help' for details)
|
||||
package <subcommand> Package operations (use 'package help' for details)
|
||||
)";
|
||||
}
|
||||
|
||||
void print_response(const cpr::Response &response) {
|
||||
std::cout << "status: " << response.status_code << '\n';
|
||||
if (response.error) {
|
||||
std::cout << "error: " << response.error.message << '\n';
|
||||
}
|
||||
std::cout << response.text << '\n';
|
||||
}
|
||||
|
||||
int handle_auth(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.size() < 2) {
|
||||
std::cout << "auth command requires a subcommand\n";
|
||||
print_help();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (args[1] == "session") {
|
||||
print_response(client.get("/api/auth/session"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (args[1] == "login") {
|
||||
if (args.size() != 4) {
|
||||
std::cout << "auth login requires email and password\n";
|
||||
return 1;
|
||||
}
|
||||
std::string body =
|
||||
"{\"email\":\"" + args[2] + "\",\"password\":\"" + args[3] + "\"}";
|
||||
print_response(client.post("/api/auth/login", body));
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::cout << "unknown auth subcommand\n";
|
||||
print_help();
|
||||
return 1;
|
||||
}
|
||||
|
||||
int handle_user(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.size() < 2) {
|
||||
std::cout << "user command requires a subcommand\n";
|
||||
print_help();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (args[1] == "list") {
|
||||
print_response(client.get("/api/users"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (args[1] == "get") {
|
||||
if (args.size() != 3) {
|
||||
std::cout << "user get requires a user ID\n";
|
||||
return 1;
|
||||
}
|
||||
print_response(client.get("/api/users/" + args[2]));
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::cout << "unknown user subcommand\n";
|
||||
print_help();
|
||||
return 1;
|
||||
}
|
||||
|
||||
int handle_tenant(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.size() < 2) {
|
||||
std::cout << "tenant command requires a subcommand\n";
|
||||
print_help();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (args[1] == "list") {
|
||||
print_response(client.get("/api/tenants"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (args[1] == "get") {
|
||||
if (args.size() != 3) {
|
||||
std::cout << "tenant get requires a tenant ID\n";
|
||||
return 1;
|
||||
}
|
||||
print_response(client.get("/api/tenants/" + args[2]));
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::cout << "unknown tenant subcommand\n";
|
||||
print_help();
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace commands {
|
||||
|
||||
int dispatch(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.empty()) {
|
||||
print_help();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (args[0] == "auth") {
|
||||
return handle_auth(client, args);
|
||||
}
|
||||
|
||||
if (args[0] == "user") {
|
||||
return handle_user(client, args);
|
||||
}
|
||||
|
||||
if (args[0] == "tenant") {
|
||||
return handle_tenant(client, args);
|
||||
}
|
||||
|
||||
if (args[0] == "dbal") {
|
||||
return handle_dbal(client, args);
|
||||
}
|
||||
|
||||
if (args[0] == "package") {
|
||||
return handle_package(args);
|
||||
}
|
||||
|
||||
print_help();
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace commands
|
||||
@@ -1,11 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../utils/http_client.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace commands {
|
||||
|
||||
int dispatch(const HttpClient &client, const std::vector<std::string> &args);
|
||||
|
||||
} // namespace commands
|
||||
@@ -1,445 +0,0 @@
|
||||
/**
|
||||
* @file dbal_commands.cpp
|
||||
* @brief DBAL command handler implementations
|
||||
*/
|
||||
|
||||
#include "dbal_commands.h"
|
||||
#include <cpr/cpr.h>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
|
||||
void print_response(const cpr::Response &response) {
|
||||
std::cout << "status: " << response.status_code << '\n';
|
||||
if (response.error) {
|
||||
std::cout << "error: " << response.error.message << '\n';
|
||||
}
|
||||
std::cout << response.text << '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build JSON body from key=value pairs
|
||||
*/
|
||||
std::string build_json_body(const std::vector<std::string> &pairs) {
|
||||
if (pairs.empty()) {
|
||||
return "{}";
|
||||
}
|
||||
|
||||
std::ostringstream json;
|
||||
json << "{";
|
||||
|
||||
bool first = true;
|
||||
for (const auto &pair : pairs) {
|
||||
auto eq_pos = pair.find('=');
|
||||
if (eq_pos == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!first) {
|
||||
json << ",";
|
||||
}
|
||||
first = false;
|
||||
|
||||
std::string key = pair.substr(0, eq_pos);
|
||||
std::string value = pair.substr(eq_pos + 1);
|
||||
|
||||
// Simple type detection
|
||||
if (value == "true" || value == "false" ||
|
||||
(value.find_first_not_of("0123456789.-") == std::string::npos && !value.empty())) {
|
||||
// Boolean or number - don't quote
|
||||
json << "\"" << key << "\":" << value;
|
||||
} else {
|
||||
// String - quote it
|
||||
json << "\"" << key << "\":\"" << value << "\"";
|
||||
}
|
||||
}
|
||||
|
||||
json << "}";
|
||||
return json.str();
|
||||
}
|
||||
|
||||
int dbal_ping(const HttpClient &client) {
|
||||
print_response(client.get("/api/dbal/ping"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dbal_create(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.size() < 3) {
|
||||
std::cout << "Usage: dbal create <entity> <field=value> [field=value...]\n";
|
||||
std::cout << "Example: dbal create User name=John email=john@example.com level=1\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string entity = args[2];
|
||||
std::vector<std::string> fields(args.begin() + 3, args.end());
|
||||
std::string body = build_json_body(fields);
|
||||
|
||||
std::cout << "Creating " << entity << " with: " << body << "\n";
|
||||
print_response(client.post("/api/dbal/" + entity, body));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dbal_read(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.size() != 4) {
|
||||
std::cout << "Usage: dbal read <entity> <id>\n";
|
||||
std::cout << "Example: dbal read User clx123abc\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string entity = args[2];
|
||||
std::string id = args[3];
|
||||
|
||||
print_response(client.get("/api/dbal/" + entity + "/" + id));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dbal_update(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.size() < 4) {
|
||||
std::cout << "Usage: dbal update <entity> <id> <field=value> [field=value...]\n";
|
||||
std::cout << "Example: dbal update User clx123abc name=Jane level=2\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string entity = args[2];
|
||||
std::string id = args[3];
|
||||
std::vector<std::string> fields(args.begin() + 4, args.end());
|
||||
std::string body = build_json_body(fields);
|
||||
|
||||
std::cout << "Updating " << entity << "/" << id << " with: " << body << "\n";
|
||||
print_response(client.patch("/api/dbal/" + entity + "/" + id, body));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dbal_delete(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.size() != 4) {
|
||||
std::cout << "Usage: dbal delete <entity> <id>\n";
|
||||
std::cout << "Example: dbal delete User clx123abc\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string entity = args[2];
|
||||
std::string id = args[3];
|
||||
|
||||
std::cout << "Deleting " << entity << "/" << id << "\n";
|
||||
print_response(client.del("/api/dbal/" + entity + "/" + id));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dbal_list(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.size() < 3) {
|
||||
std::cout << "Usage: dbal list <entity> [where.field=value] [take=N] [skip=N]\n";
|
||||
std::cout << "Example: dbal list User where.level=1 take=10\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string entity = args[2];
|
||||
|
||||
// Build query parameters
|
||||
std::string query;
|
||||
for (size_t i = 3; i < args.size(); ++i) {
|
||||
auto eq_pos = args[i].find('=');
|
||||
if (eq_pos != std::string::npos) {
|
||||
if (!query.empty()) {
|
||||
query += "&";
|
||||
}
|
||||
query += args[i];
|
||||
}
|
||||
}
|
||||
|
||||
std::string url = "/api/dbal/" + entity;
|
||||
if (!query.empty()) {
|
||||
url += "?" + query;
|
||||
}
|
||||
|
||||
print_response(client.get(url));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dbal_execute(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.size() < 3) {
|
||||
std::cout << "Usage: dbal execute <operation> [params...]\n";
|
||||
std::cout << "Example: dbal execute findFirst entity=User where.email=admin@test.com\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string operation = args[2];
|
||||
std::vector<std::string> params(args.begin() + 3, args.end());
|
||||
|
||||
// Build request body
|
||||
std::ostringstream body;
|
||||
body << "{\"operation\":\"" << operation << "\"";
|
||||
|
||||
if (!params.empty()) {
|
||||
body << ",\"params\":" << build_json_body(params);
|
||||
}
|
||||
|
||||
body << "}";
|
||||
|
||||
std::cout << "Executing " << operation << "\n";
|
||||
print_response(client.post("/api/dbal/execute", body.str()));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dbal_rest(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.size() < 5) {
|
||||
std::cout << "Usage: dbal rest <tenant> <package> <entity> [id] [method|action] [data...]\n";
|
||||
std::cout << "\nExamples:\n";
|
||||
std::cout << " dbal rest acme forum_forge posts # GET list\n";
|
||||
std::cout << " dbal rest acme forum_forge posts 123 # GET by id\n";
|
||||
std::cout << " dbal rest acme forum_forge posts POST title=Hello # POST create\n";
|
||||
std::cout << " dbal rest acme forum_forge posts 123 PUT title=New # PUT update\n";
|
||||
std::cout << " dbal rest acme forum_forge posts 123 DELETE # DELETE\n";
|
||||
std::cout << " dbal rest acme forum_forge posts 123 like POST # Custom action\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string tenant = args[2];
|
||||
std::string package = args[3];
|
||||
std::string entity = args[4];
|
||||
std::string id;
|
||||
std::string method = "GET";
|
||||
std::string action;
|
||||
std::vector<std::string> data_args;
|
||||
|
||||
// Parse remaining arguments
|
||||
size_t i = 5;
|
||||
|
||||
// Check if next arg is an ID (not a method)
|
||||
if (i < args.size()) {
|
||||
std::string arg = args[i];
|
||||
// If it's not a method keyword, treat as ID
|
||||
if (arg != "GET" && arg != "POST" && arg != "PUT" && arg != "PATCH" && arg != "DELETE") {
|
||||
id = arg;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for method or action
|
||||
if (i < args.size()) {
|
||||
std::string arg = args[i];
|
||||
if (arg == "GET" || arg == "POST" || arg == "PUT" || arg == "PATCH" || arg == "DELETE") {
|
||||
method = arg;
|
||||
i++;
|
||||
} else if (!id.empty()) {
|
||||
// If we have an ID and this isn't a method, it might be an action
|
||||
action = arg;
|
||||
i++;
|
||||
// Check if next is a method
|
||||
if (i < args.size()) {
|
||||
arg = args[i];
|
||||
if (arg == "GET" || arg == "POST" || arg == "PUT" || arg == "PATCH" || arg == "DELETE") {
|
||||
method = arg;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining args are data
|
||||
while (i < args.size()) {
|
||||
data_args.push_back(args[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
// Build URL
|
||||
std::string url = "/" + tenant + "/" + package + "/" + entity;
|
||||
if (!id.empty()) {
|
||||
url += "/" + id;
|
||||
}
|
||||
if (!action.empty()) {
|
||||
url += "/" + action;
|
||||
}
|
||||
|
||||
std::cout << method << " " << url << "\n";
|
||||
|
||||
// Build body if we have data
|
||||
std::string body;
|
||||
if (!data_args.empty()) {
|
||||
body = build_json_body(data_args);
|
||||
std::cout << "Body: " << body << "\n";
|
||||
}
|
||||
|
||||
// Make request based on method
|
||||
if (method == "GET") {
|
||||
print_response(client.get(url));
|
||||
} else if (method == "POST") {
|
||||
print_response(client.post(url, body.empty() ? "{}" : body));
|
||||
} else if (method == "PUT") {
|
||||
print_response(client.put(url, body.empty() ? "{}" : body));
|
||||
} else if (method == "PATCH") {
|
||||
print_response(client.patch(url, body.empty() ? "{}" : body));
|
||||
} else if (method == "DELETE") {
|
||||
print_response(client.del(url));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dbal_schema(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.size() < 3) {
|
||||
std::cout << "Usage: dbal schema <subcommand>\n";
|
||||
std::cout << " dbal schema list List all registered schemas\n";
|
||||
std::cout << " dbal schema pending Show pending migrations\n";
|
||||
std::cout << " dbal schema entity <name> Show schema for entity\n";
|
||||
std::cout << " dbal schema scan Scan packages for schema changes\n";
|
||||
std::cout << " dbal schema approve <id> Approve a migration (or 'all')\n";
|
||||
std::cout << " dbal schema reject <id> Reject a migration\n";
|
||||
std::cout << " dbal schema generate Generate Prisma fragment\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string subcommand = args[2];
|
||||
|
||||
if (subcommand == "list") {
|
||||
print_response(client.get("/api/dbal/schema"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (subcommand == "pending") {
|
||||
print_response(client.get("/api/dbal/schema"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (subcommand == "entity" && args.size() >= 4) {
|
||||
print_response(client.get("/api/dbal/schema/" + args[3]));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (subcommand == "scan") {
|
||||
std::cout << "Scanning packages for schema changes...\n";
|
||||
print_response(client.post("/api/dbal/schema", "{\"action\":\"scan\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (subcommand == "approve" && args.size() >= 4) {
|
||||
std::string id = args[3];
|
||||
std::cout << "Approving migration: " << id << "\n";
|
||||
print_response(client.post("/api/dbal/schema", "{\"action\":\"approve\",\"id\":\"" + id + "\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (subcommand == "reject" && args.size() >= 4) {
|
||||
std::string id = args[3];
|
||||
std::cout << "Rejecting migration: " << id << "\n";
|
||||
print_response(client.post("/api/dbal/schema", "{\"action\":\"reject\",\"id\":\"" + id + "\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (subcommand == "generate") {
|
||||
std::cout << "Generating Prisma fragment from approved migrations...\n";
|
||||
print_response(client.post("/api/dbal/schema", "{\"action\":\"generate\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::cout << "Unknown schema subcommand: " << subcommand << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace commands {
|
||||
|
||||
void print_dbal_help() {
|
||||
std::cout << R"(DBAL Commands:
|
||||
dbal ping Check DBAL connection
|
||||
dbal create <entity> <field=value...> Create a new record
|
||||
dbal read <entity> <id> Read a record by ID
|
||||
dbal update <entity> <id> <field=value...> Update a record
|
||||
dbal delete <entity> <id> Delete a record
|
||||
dbal list <entity> [filters...] List records with optional filters
|
||||
dbal execute <operation> [params...] Execute a DBAL operation
|
||||
|
||||
RESTful Multi-Tenant Operations:
|
||||
dbal rest <tenant> <package> <entity> [id] [action] [method] [data...]
|
||||
Examples:
|
||||
dbal rest acme forum_forge posts # GET - list posts
|
||||
dbal rest acme forum_forge posts 123 # GET - read post
|
||||
dbal rest acme forum_forge posts POST title=Hello # POST - create
|
||||
dbal rest acme forum_forge posts 123 PUT title=New # PUT - update
|
||||
dbal rest acme forum_forge posts 123 DELETE # DELETE
|
||||
dbal rest acme forum_forge posts 123 like POST # Custom action
|
||||
|
||||
Schema Management:
|
||||
dbal schema list List registered entity schemas
|
||||
dbal schema pending Show pending schema migrations
|
||||
dbal schema entity <name> Show schema for an entity
|
||||
dbal schema scan Scan packages for schema changes
|
||||
dbal schema approve <id|all> Approve a migration
|
||||
dbal schema reject <id> Reject a migration
|
||||
dbal schema generate Generate Prisma fragment
|
||||
|
||||
Filter syntax for list:
|
||||
where.field=value Filter by field value
|
||||
take=N Limit results
|
||||
skip=N Skip first N results
|
||||
orderBy.field=asc Sort ascending
|
||||
orderBy.field=desc Sort descending
|
||||
|
||||
Examples:
|
||||
dbal ping
|
||||
dbal create User name=Alice email=alice@test.com level=1
|
||||
dbal read User clx123abc
|
||||
dbal update User clx123abc level=2
|
||||
dbal list User where.level=1 take=10
|
||||
dbal list AuditLog where.entity=User orderBy.timestamp=desc take=20
|
||||
dbal delete User clx123abc
|
||||
dbal execute findFirst entity=User where.email=admin@test.com
|
||||
)";
|
||||
}
|
||||
|
||||
int handle_dbal(const HttpClient &client, const std::vector<std::string> &args) {
|
||||
if (args.size() < 2) {
|
||||
print_dbal_help();
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string subcommand = args[1];
|
||||
|
||||
if (subcommand == "ping") {
|
||||
return dbal_ping(client);
|
||||
}
|
||||
|
||||
if (subcommand == "create") {
|
||||
return dbal_create(client, args);
|
||||
}
|
||||
|
||||
if (subcommand == "read") {
|
||||
return dbal_read(client, args);
|
||||
}
|
||||
|
||||
if (subcommand == "update") {
|
||||
return dbal_update(client, args);
|
||||
}
|
||||
|
||||
if (subcommand == "delete") {
|
||||
return dbal_delete(client, args);
|
||||
}
|
||||
|
||||
if (subcommand == "list") {
|
||||
return dbal_list(client, args);
|
||||
}
|
||||
|
||||
if (subcommand == "execute") {
|
||||
return dbal_execute(client, args);
|
||||
}
|
||||
|
||||
if (subcommand == "rest") {
|
||||
return dbal_rest(client, args);
|
||||
}
|
||||
|
||||
if (subcommand == "schema") {
|
||||
return dbal_schema(client, args);
|
||||
}
|
||||
|
||||
if (subcommand == "help" || subcommand == "-h" || subcommand == "--help") {
|
||||
print_dbal_help();
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::cout << "Unknown DBAL subcommand: " << subcommand << "\n";
|
||||
print_dbal_help();
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace commands
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* @file dbal_commands.h
|
||||
* @brief DBAL command handlers for CLI
|
||||
*
|
||||
* Provides CLI commands for DBAL operations:
|
||||
* - dbal ping Check DBAL connection
|
||||
* - dbal create Create a record
|
||||
* - dbal read Read a record
|
||||
* - dbal update Update a record
|
||||
* - dbal delete Delete a record
|
||||
* - dbal list List records with filters
|
||||
* - dbal execute Execute raw DBAL query
|
||||
*/
|
||||
|
||||
#ifndef DBAL_COMMANDS_H
|
||||
#define DBAL_COMMANDS_H
|
||||
|
||||
#include "../utils/http_client.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace commands {
|
||||
|
||||
/**
|
||||
* @brief Handle DBAL-related commands
|
||||
* @param client HTTP client instance
|
||||
* @param args Command arguments (first element is "dbal")
|
||||
* @return Exit code (0 = success)
|
||||
*/
|
||||
int handle_dbal(const HttpClient &client, const std::vector<std::string> &args);
|
||||
|
||||
/**
|
||||
* @brief Print DBAL command help
|
||||
*/
|
||||
void print_dbal_help();
|
||||
|
||||
} // namespace commands
|
||||
|
||||
#endif // DBAL_COMMANDS_H
|
||||
@@ -1,345 +0,0 @@
|
||||
#include "package_commands.h"
|
||||
#include "../lua/lua_runner.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
void print_package_help() {
|
||||
std::cout << R"(Usage: metabuilder-cli package <command> [options]
|
||||
|
||||
Commands:
|
||||
list List available packages with scripts
|
||||
run <package> <script> [args] Run a Lua script from a package
|
||||
generate <package_id> [opts] Generate a new package
|
||||
|
||||
Generate options:
|
||||
--name <name> Display name (default: derived from package_id)
|
||||
--description <desc> Package description
|
||||
--category <cat> Package category (default: ui)
|
||||
--min-level <n> 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 <e1,e2> Entity names for schema (comma-separated)
|
||||
--with-components Include component scaffolding
|
||||
--components <c1,c2> Component names (comma-separated)
|
||||
--deps <d1,d2> Package dependencies (comma-separated)
|
||||
--output <dir> Output directory (default: ./packages)
|
||||
--dry-run Preview files without writing
|
||||
|
||||
Examples:
|
||||
metabuilder-cli package list
|
||||
metabuilder-cli package run codegen_studio package_template
|
||||
metabuilder-cli package generate my_forum --category social --with-schema --entities Thread,Post
|
||||
)";
|
||||
}
|
||||
|
||||
fs::path find_packages_dir() {
|
||||
// Check environment variable first
|
||||
const char* env_path = std::getenv("METABUILDER_PACKAGES");
|
||||
if (env_path && fs::exists(env_path)) {
|
||||
return env_path;
|
||||
}
|
||||
|
||||
// Try relative to current directory
|
||||
if (fs::exists("packages")) {
|
||||
return fs::absolute("packages");
|
||||
}
|
||||
|
||||
// Try relative to executable
|
||||
// (would need to pass argv[0] for this)
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string> split_csv(const std::string& str) {
|
||||
std::vector<std::string> result;
|
||||
std::stringstream ss(str);
|
||||
std::string item;
|
||||
while (std::getline(ss, item, ',')) {
|
||||
// Trim
|
||||
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;
|
||||
}
|
||||
|
||||
int handle_list(const fs::path& packages_dir) {
|
||||
std::cout << "Available packages with scripts:\n\n";
|
||||
|
||||
int count = 0;
|
||||
for (const auto& entry : fs::directory_iterator(packages_dir)) {
|
||||
if (!entry.is_directory()) continue;
|
||||
|
||||
auto scripts_path = entry.path() / "seed" / "scripts";
|
||||
if (!fs::exists(scripts_path)) continue;
|
||||
|
||||
std::cout << " " << entry.path().filename().string() << "\n";
|
||||
|
||||
// List available scripts/modules
|
||||
for (const auto& script : fs::directory_iterator(scripts_path)) {
|
||||
if (script.is_directory()) {
|
||||
auto init = script.path() / "init.lua";
|
||||
if (fs::exists(init)) {
|
||||
std::cout << " - " << script.path().filename().string() << "\n";
|
||||
}
|
||||
} else if (script.path().extension() == ".lua") {
|
||||
auto name = script.path().stem().string();
|
||||
if (name != "init") {
|
||||
std::cout << " - " << name << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
++count;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
std::cout << " (no packages with scripts found)\n";
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handle_run(const fs::path& packages_dir, const std::vector<std::string>& args) {
|
||||
if (args.size() < 4) {
|
||||
std::cerr << "Usage: metabuilder-cli package run <package> <script> [function] [args...]\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& package_id = args[2];
|
||||
const auto& script_name = args[3];
|
||||
std::string func_name = args.size() > 4 ? args[4] : "main";
|
||||
|
||||
lua::LuaRunner runner(packages_dir);
|
||||
|
||||
if (!runner.load_module(package_id, script_name)) {
|
||||
std::cerr << "Error: " << runner.last_error() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Build config from remaining args
|
||||
lua::LuaConfig config;
|
||||
for (size_t i = 5; i < args.size(); ++i) {
|
||||
const auto& arg = args[i];
|
||||
if (arg.substr(0, 2) == "--" && i + 1 < args.size()) {
|
||||
auto key = arg.substr(2);
|
||||
auto value = args[++i];
|
||||
config[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
auto result = runner.call(func_name, config);
|
||||
|
||||
if (!result.success) {
|
||||
std::cerr << "Error: " << result.error << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!result.output.empty()) {
|
||||
std::cout << result.output << "\n";
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handle_generate(const fs::path& packages_dir, const std::vector<std::string>& args) {
|
||||
if (args.size() < 3) {
|
||||
std::cerr << "Usage: metabuilder-cli package generate <package_id> [options]\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& package_id = args[2];
|
||||
|
||||
// Validate package_id format
|
||||
if (package_id.empty() || !std::isalpha(package_id[0])) {
|
||||
std::cerr << "Error: package_id must start with a letter\n";
|
||||
return 1;
|
||||
}
|
||||
for (char c : package_id) {
|
||||
if (!std::isalnum(c) && c != '_') {
|
||||
std::cerr << "Error: package_id must contain only letters, numbers, and underscores\n";
|
||||
return 1;
|
||||
}
|
||||
if (std::isupper(c)) {
|
||||
std::cerr << "Error: package_id must be lowercase\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse options
|
||||
lua::LuaConfig config;
|
||||
config["packageId"] = package_id;
|
||||
config["category"] = std::string("ui");
|
||||
config["minLevel"] = int64_t(2);
|
||||
config["primary"] = true;
|
||||
config["withSchema"] = false;
|
||||
config["withTests"] = true;
|
||||
config["withComponents"] = false;
|
||||
|
||||
bool dry_run = false;
|
||||
std::string output_dir = packages_dir.string();
|
||||
|
||||
for (size_t i = 3; i < args.size(); ++i) {
|
||||
const auto& arg = args[i];
|
||||
|
||||
if (arg == "--name" && i + 1 < args.size()) {
|
||||
config["name"] = args[++i];
|
||||
} else if (arg == "--description" && i + 1 < args.size()) {
|
||||
config["description"] = args[++i];
|
||||
} else if (arg == "--category" && i + 1 < args.size()) {
|
||||
config["category"] = args[++i];
|
||||
} else if (arg == "--min-level" && i + 1 < args.size()) {
|
||||
config["minLevel"] = int64_t(std::stoi(args[++i]));
|
||||
} else if (arg == "--primary") {
|
||||
config["primary"] = true;
|
||||
} else if (arg == "--dependency") {
|
||||
config["primary"] = false;
|
||||
} else if (arg == "--with-schema") {
|
||||
config["withSchema"] = true;
|
||||
} else if (arg == "--entities" && i + 1 < args.size()) {
|
||||
config["entities"] = split_csv(args[++i]);
|
||||
} else if (arg == "--with-components") {
|
||||
config["withComponents"] = true;
|
||||
} else if (arg == "--components" && i + 1 < args.size()) {
|
||||
config["components"] = split_csv(args[++i]);
|
||||
} else if (arg == "--deps" && i + 1 < args.size()) {
|
||||
config["dependencies"] = split_csv(args[++i]);
|
||||
} else if (arg == "--output" && i + 1 < args.size()) {
|
||||
output_dir = args[++i];
|
||||
} else if (arg == "--dry-run") {
|
||||
dry_run = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Load package_template module from codegen_studio
|
||||
lua::LuaRunner runner(packages_dir);
|
||||
|
||||
if (!runner.load_module("codegen_studio", "package_template")) {
|
||||
std::cerr << "Error: Could not load package_template module\n";
|
||||
std::cerr << " " << runner.last_error() << "\n";
|
||||
std::cerr << " Make sure you're running from the MetaBuilder project root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Validate config
|
||||
auto validation = runner.validate("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 result = runner.call("generate", config);
|
||||
|
||||
if (!result.success) {
|
||||
std::cerr << "Error generating package: " << result.error << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (result.files.empty()) {
|
||||
std::cerr << "Error: No files generated\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Check if package already exists
|
||||
fs::path package_path = fs::path(output_dir) / package_id;
|
||||
if (fs::exists(package_path) && !dry_run) {
|
||||
std::cerr << "Error: Package directory already exists: " << package_path << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (dry_run) {
|
||||
std::cout << "Would generate " << result.files.size() << " files in " << package_path << ":\n\n";
|
||||
for (const auto& file : result.files) {
|
||||
std::cout << " " << file.path << " (" << file.content.size() << " bytes)\n";
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Write files
|
||||
std::cout << "Generating package: " << package_id << "\n";
|
||||
std::cout << " Location: " << package_path << "\n\n";
|
||||
|
||||
int written = 0;
|
||||
for (const auto& file : result.files) {
|
||||
fs::path full_path = package_path / file.path;
|
||||
fs::path dir = full_path.parent_path();
|
||||
|
||||
if (!dir.empty() && !fs::exists(dir)) {
|
||||
fs::create_directories(dir);
|
||||
}
|
||||
|
||||
std::ofstream out(full_path, std::ios::binary);
|
||||
if (!out) {
|
||||
std::cerr << " Error writing: " << file.path << "\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
out << file.content;
|
||||
out.close();
|
||||
|
||||
std::cout << " Created: " << file.path << "\n";
|
||||
++written;
|
||||
}
|
||||
|
||||
std::cout << "\n✅ Package '" << package_id << "' created successfully!\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: npm run packages:index\n";
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace commands {
|
||||
|
||||
int handle_package(const std::vector<std::string>& args) {
|
||||
if (args.size() < 2 || args[1] == "help" || args[1] == "--help") {
|
||||
print_package_help();
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto packages_dir = find_packages_dir();
|
||||
if (packages_dir.empty()) {
|
||||
std::cerr << "Error: Could not find packages directory\n";
|
||||
std::cerr << "Run from the MetaBuilder project root or set METABUILDER_PACKAGES\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& subcommand = args[1];
|
||||
|
||||
if (subcommand == "list") {
|
||||
return handle_list(packages_dir);
|
||||
}
|
||||
|
||||
if (subcommand == "run") {
|
||||
return handle_run(packages_dir, args);
|
||||
}
|
||||
|
||||
if (subcommand == "generate") {
|
||||
return handle_generate(packages_dir, args);
|
||||
}
|
||||
|
||||
std::cerr << "Unknown package subcommand: " << subcommand << "\n";
|
||||
print_package_help();
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace commands
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace commands {
|
||||
|
||||
/**
|
||||
* Handle package commands
|
||||
*
|
||||
* Usage:
|
||||
* package list List available packages
|
||||
* package run <pkg> <script> [args] Run a Lua script from a package
|
||||
* package generate <pkg_id> Generate a new package (uses package_generator)
|
||||
*/
|
||||
int handle_package(const std::vector<std::string>& args);
|
||||
|
||||
} // namespace commands
|
||||
@@ -1,268 +0,0 @@
|
||||
#include "lua_runner.h"
|
||||
|
||||
#define SOL_ALL_SAFETIES_ON 1
|
||||
#include <sol/sol.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
namespace lua {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct LuaRunner::Impl {
|
||||
sol::state lua;
|
||||
sol::table current_module;
|
||||
};
|
||||
|
||||
LuaRunner::LuaRunner(const fs::path& scripts_base)
|
||||
: impl_(std::make_unique<Impl>())
|
||||
, scripts_base_(scripts_base) {
|
||||
setup_sandbox();
|
||||
}
|
||||
|
||||
LuaRunner::~LuaRunner() = default;
|
||||
|
||||
void LuaRunner::setup_sandbox() {
|
||||
// Open only safe libraries
|
||||
impl_->lua.open_libraries(
|
||||
sol::lib::base,
|
||||
sol::lib::string,
|
||||
sol::lib::table,
|
||||
sol::lib::math,
|
||||
sol::lib::utf8
|
||||
);
|
||||
|
||||
// Remove dangerous functions from base
|
||||
impl_->lua["dofile"] = sol::nil;
|
||||
impl_->lua["loadfile"] = sol::nil;
|
||||
|
||||
// Custom print that captures output
|
||||
impl_->lua.set_function("print", [](sol::variadic_args args) {
|
||||
for (auto arg : args) {
|
||||
std::cout << arg.as<std::string>();
|
||||
std::cout << "\t";
|
||||
}
|
||||
std::cout << "\n";
|
||||
});
|
||||
}
|
||||
|
||||
fs::path LuaRunner::find_module_path(const std::string& package_id, const std::string& module_name) {
|
||||
// Try: packages/{package_id}/seed/scripts/{module_name}/init.lua
|
||||
auto path1 = scripts_base_ / package_id / "seed" / "scripts" / module_name / "init.lua";
|
||||
if (fs::exists(path1)) return path1;
|
||||
|
||||
// Try: packages/{package_id}/seed/scripts/{module_name}.lua
|
||||
auto path2 = scripts_base_ / package_id / "seed" / "scripts" / (module_name + ".lua");
|
||||
if (fs::exists(path2)) return path2;
|
||||
|
||||
// Try: {package_id}/seed/scripts/{module_name}/init.lua (if scripts_base includes packages/)
|
||||
auto path3 = scripts_base_ / "packages" / package_id / "seed" / "scripts" / module_name / "init.lua";
|
||||
if (fs::exists(path3)) return path3;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool LuaRunner::load_module(const std::string& package_id, const std::string& module_name) {
|
||||
auto module_path = find_module_path(package_id, module_name);
|
||||
|
||||
if (module_path.empty()) {
|
||||
last_error_ = "Module not found: " + package_id + "/" + module_name;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set up package.path to include the scripts directory
|
||||
auto scripts_dir = module_path.parent_path();
|
||||
if (module_path.filename() == "init.lua") {
|
||||
scripts_dir = scripts_dir.parent_path();
|
||||
}
|
||||
|
||||
std::string package_path = impl_->lua["package"]["path"].get<std::string>();
|
||||
package_path += ";" + scripts_dir.string() + "/?.lua";
|
||||
package_path += ";" + scripts_dir.string() + "/?/init.lua";
|
||||
impl_->lua["package"]["path"] = package_path;
|
||||
|
||||
try {
|
||||
auto result = impl_->lua.safe_script_file(module_path.string());
|
||||
if (!result.valid()) {
|
||||
sol::error err = result;
|
||||
last_error_ = err.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
impl_->current_module = result.get<sol::table>();
|
||||
module_loaded_ = true;
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
last_error_ = e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Convert LuaConfig to sol::table
|
||||
sol::table config_to_lua(sol::state& lua, const LuaConfig& config) {
|
||||
sol::table tbl = lua.create_table();
|
||||
|
||||
for (const auto& [key, value] : config) {
|
||||
std::visit([&](auto&& v) {
|
||||
using T = std::decay_t<decltype(v)>;
|
||||
if constexpr (std::is_same_v<T, std::nullptr_t>) {
|
||||
tbl[key] = sol::nil;
|
||||
} else if constexpr (std::is_same_v<T, std::vector<std::string>>) {
|
||||
sol::table arr = lua.create_table();
|
||||
int i = 1;
|
||||
for (const auto& s : v) {
|
||||
arr[i++] = s;
|
||||
}
|
||||
tbl[key] = arr;
|
||||
} else if constexpr (std::is_same_v<T, std::unordered_map<std::string, std::string>>) {
|
||||
sol::table map = lua.create_table();
|
||||
for (const auto& [k, val] : v) {
|
||||
map[k] = val;
|
||||
}
|
||||
tbl[key] = map;
|
||||
} else {
|
||||
tbl[key] = v;
|
||||
}
|
||||
}, value);
|
||||
}
|
||||
|
||||
return tbl;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RunResult LuaRunner::call(const std::string& func_name, const LuaConfig& config) {
|
||||
RunResult result;
|
||||
|
||||
if (!module_loaded_) {
|
||||
result.error = "No module loaded";
|
||||
return result;
|
||||
}
|
||||
|
||||
sol::function func = impl_->current_module[func_name];
|
||||
if (!func.valid()) {
|
||||
result.error = "Function not found: " + func_name;
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
auto lua_config = config_to_lua(impl_->lua, config);
|
||||
sol::protected_function_result call_result = func(lua_config);
|
||||
|
||||
if (!call_result.valid()) {
|
||||
sol::error err = call_result;
|
||||
result.error = err.what();
|
||||
return result;
|
||||
}
|
||||
|
||||
result.success = true;
|
||||
|
||||
// Try to extract output/files from result
|
||||
if (call_result.get_type() == sol::type::table) {
|
||||
sol::table tbl = call_result;
|
||||
|
||||
if (tbl["success"].valid()) {
|
||||
result.success = tbl["success"].get<bool>();
|
||||
}
|
||||
if (tbl["output"].valid()) {
|
||||
result.output = tbl["output"].get<std::string>();
|
||||
}
|
||||
if (tbl["error"].valid()) {
|
||||
result.error = tbl["error"].get<std::string>();
|
||||
}
|
||||
if (tbl["files"].valid()) {
|
||||
sol::table files = tbl["files"];
|
||||
for (auto& pair : files) {
|
||||
sol::table file = pair.second;
|
||||
GeneratedFile gf;
|
||||
gf.path = file["path"].get<std::string>();
|
||||
gf.content = file["content"].get<std::string>();
|
||||
result.files.push_back(std::move(gf));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (const std::exception& e) {
|
||||
result.error = e.what();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
RunResult LuaRunner::call(const std::string& func_name) {
|
||||
return call(func_name, {});
|
||||
}
|
||||
|
||||
ValidationResult LuaRunner::validate(const std::string& func_name, const LuaConfig& config) {
|
||||
ValidationResult result;
|
||||
|
||||
if (!module_loaded_) {
|
||||
result.errors.push_back("No module loaded");
|
||||
return result;
|
||||
}
|
||||
|
||||
sol::function func = impl_->current_module[func_name];
|
||||
if (!func.valid()) {
|
||||
result.errors.push_back("Function not found: " + func_name);
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
auto lua_config = config_to_lua(impl_->lua, config);
|
||||
sol::protected_function_result call_result = func(lua_config);
|
||||
|
||||
if (!call_result.valid()) {
|
||||
sol::error err = call_result;
|
||||
result.errors.push_back(err.what());
|
||||
return result;
|
||||
}
|
||||
|
||||
sol::table tbl = call_result;
|
||||
result.valid = tbl["valid"].get<bool>();
|
||||
|
||||
if (tbl["errors"].valid()) {
|
||||
sol::table errs = tbl["errors"];
|
||||
for (auto& pair : errs) {
|
||||
result.errors.push_back(pair.second.as<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (const std::exception& e) {
|
||||
result.errors.push_back(e.what());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> LuaRunner::get_list(const std::string& func_name) {
|
||||
std::vector<std::string> result;
|
||||
|
||||
if (!module_loaded_) return result;
|
||||
|
||||
sol::function func = impl_->current_module[func_name];
|
||||
if (!func.valid()) return result;
|
||||
|
||||
try {
|
||||
sol::protected_function_result call_result = func();
|
||||
if (!call_result.valid()) return result;
|
||||
|
||||
sol::table tbl = call_result;
|
||||
for (auto& pair : tbl) {
|
||||
result.push_back(pair.second.as<std::string>());
|
||||
}
|
||||
} catch (...) {}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<GeneratedFile> LuaRunner::get_files(const std::string& func_name, const LuaConfig& config) {
|
||||
auto result = call(func_name, config);
|
||||
return std::move(result.files);
|
||||
}
|
||||
|
||||
} // namespace lua
|
||||
@@ -1,135 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace lua {
|
||||
|
||||
/**
|
||||
* Lua value variant for passing data to/from Lua
|
||||
*/
|
||||
using LuaValue = std::variant<
|
||||
std::nullptr_t,
|
||||
bool,
|
||||
int64_t,
|
||||
double,
|
||||
std::string,
|
||||
std::vector<std::string>,
|
||||
std::unordered_map<std::string, std::string>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Configuration map for Lua function calls
|
||||
*/
|
||||
using LuaConfig = std::unordered_map<std::string, LuaValue>;
|
||||
|
||||
/**
|
||||
* Generated file from Lua script
|
||||
*/
|
||||
struct GeneratedFile {
|
||||
std::string path;
|
||||
std::string content;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validation result from Lua
|
||||
*/
|
||||
struct ValidationResult {
|
||||
bool valid = false;
|
||||
std::vector<std::string> errors;
|
||||
};
|
||||
|
||||
/**
|
||||
* Result from running a Lua script
|
||||
*/
|
||||
struct RunResult {
|
||||
bool success = false;
|
||||
std::string output;
|
||||
std::string error;
|
||||
std::vector<GeneratedFile> files;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sandboxed Lua script runner
|
||||
*
|
||||
* Executes Lua scripts from MetaBuilder packages in a secure sandbox
|
||||
* that prevents access to os, io, debug, and other dangerous modules.
|
||||
*/
|
||||
class LuaRunner {
|
||||
public:
|
||||
/**
|
||||
* Create runner with base scripts path
|
||||
* @param scripts_base Base path to search for scripts (e.g., packages/)
|
||||
*/
|
||||
explicit LuaRunner(const fs::path& scripts_base);
|
||||
~LuaRunner();
|
||||
|
||||
// Non-copyable
|
||||
LuaRunner(const LuaRunner&) = delete;
|
||||
LuaRunner& operator=(const LuaRunner&) = delete;
|
||||
|
||||
/**
|
||||
* Load a module from a package
|
||||
* @param package_id Package containing the module
|
||||
* @param module_name Module name (e.g., "package_template")
|
||||
* @return true if loaded successfully
|
||||
*/
|
||||
bool load_module(const std::string& package_id, const std::string& module_name);
|
||||
|
||||
/**
|
||||
* Call a Lua function with config
|
||||
* @param func_name Function name (e.g., "generate")
|
||||
* @param config Configuration to pass
|
||||
* @return Result of the call
|
||||
*/
|
||||
RunResult call(const std::string& func_name, const LuaConfig& config);
|
||||
|
||||
/**
|
||||
* Call a Lua function without arguments
|
||||
*/
|
||||
RunResult call(const std::string& func_name);
|
||||
|
||||
/**
|
||||
* Get validation result from Lua
|
||||
*/
|
||||
ValidationResult validate(const std::string& func_name, const LuaConfig& config);
|
||||
|
||||
/**
|
||||
* Get list of strings from Lua function
|
||||
*/
|
||||
std::vector<std::string> get_list(const std::string& func_name);
|
||||
|
||||
/**
|
||||
* Get generated files from Lua function
|
||||
*/
|
||||
std::vector<GeneratedFile> get_files(const std::string& func_name, const LuaConfig& config);
|
||||
|
||||
/**
|
||||
* Get last error message
|
||||
*/
|
||||
const std::string& last_error() const { return last_error_; }
|
||||
|
||||
/**
|
||||
* Check if a module is loaded
|
||||
*/
|
||||
bool is_module_loaded() const { return module_loaded_; }
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
|
||||
fs::path scripts_base_;
|
||||
std::string last_error_;
|
||||
bool module_loaded_ = false;
|
||||
|
||||
void setup_sandbox();
|
||||
fs::path find_module_path(const std::string& package_id, const std::string& module_name);
|
||||
};
|
||||
|
||||
} // namespace lua
|
||||
@@ -1,27 +0,0 @@
|
||||
#include "commands/command_dispatch.h"
|
||||
#include "utils/http_client.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
std::vector<std::string> args;
|
||||
args.reserve(std::max(0, argc - 1));
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
args.emplace_back(argv[i]);
|
||||
}
|
||||
|
||||
const char *env_base = std::getenv("METABUILDER_BASE_URL");
|
||||
const std::string base_url = env_base ? env_base : "http://localhost:3000";
|
||||
|
||||
try {
|
||||
HttpClient client(base_url);
|
||||
return commands::dispatch(client, args);
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "failed to create HTTP client: " << e.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
#include "utils/http_client.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string build_url(const std::string &base, const std::string &path) {
|
||||
std::string result = base;
|
||||
if (!result.empty() && result.back() == '/') {
|
||||
result.pop_back();
|
||||
}
|
||||
|
||||
if (!path.empty()) {
|
||||
if (path.front() != '/') {
|
||||
result.push_back('/');
|
||||
}
|
||||
result.append(path);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
HttpClient::HttpClient(std::string base_url) : base_url_(std::move(base_url)) {
|
||||
if (base_url_.empty()) {
|
||||
throw std::invalid_argument("base URL cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
cpr::Response HttpClient::get(const std::string &path) const {
|
||||
return cpr::Get(cpr::Url{build_url(base_url_, path)});
|
||||
}
|
||||
|
||||
cpr::Response HttpClient::post(const std::string &path,
|
||||
const std::string &body,
|
||||
const std::string &content_type) const {
|
||||
return cpr::Post(cpr::Url{build_url(base_url_, path)},
|
||||
cpr::Body{body},
|
||||
cpr::Header{{"Content-Type", content_type}});
|
||||
}
|
||||
|
||||
cpr::Response HttpClient::put(const std::string &path,
|
||||
const std::string &body,
|
||||
const std::string &content_type) const {
|
||||
return cpr::Put(cpr::Url{build_url(base_url_, path)},
|
||||
cpr::Body{body},
|
||||
cpr::Header{{"Content-Type", content_type}});
|
||||
}
|
||||
|
||||
cpr::Response HttpClient::patch(const std::string &path,
|
||||
const std::string &body,
|
||||
const std::string &content_type) const {
|
||||
return cpr::Patch(cpr::Url{build_url(base_url_, path)},
|
||||
cpr::Body{body},
|
||||
cpr::Header{{"Content-Type", content_type}});
|
||||
}
|
||||
|
||||
cpr::Response HttpClient::del(const std::string &path) const {
|
||||
return cpr::Delete(cpr::Url{build_url(base_url_, path)});
|
||||
}
|
||||
|
||||
const std::string &HttpClient::base_url() const noexcept { return base_url_; }
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cpr/cpr.h>
|
||||
#include <string>
|
||||
|
||||
class HttpClient {
|
||||
public:
|
||||
explicit HttpClient(std::string base_url);
|
||||
|
||||
cpr::Response get(const std::string &path) const;
|
||||
cpr::Response post(const std::string &path,
|
||||
const std::string &body,
|
||||
const std::string &content_type = "application/json") const;
|
||||
cpr::Response put(const std::string &path,
|
||||
const std::string &body,
|
||||
const std::string &content_type = "application/json") const;
|
||||
cpr::Response patch(const std::string &path,
|
||||
const std::string &body,
|
||||
const std::string &content_type = "application/json") const;
|
||||
cpr::Response del(const std::string &path) const;
|
||||
|
||||
[[nodiscard]] const std::string &base_url() const noexcept;
|
||||
|
||||
private:
|
||||
std::string base_url_;
|
||||
};
|
||||
Reference in New Issue
Block a user