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:
2026-01-07 14:57:40 +00:00
parent 9284b9a67b
commit 76a667f259
14 changed files with 0 additions and 1688 deletions

View File

@@ -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
)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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_; }

View File

@@ -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_;
};