feat(cli): Enhance CLI with resource catalog and sandbox management
- Added resource catalog for introspecting CLI resources, including schemas for palettes, ROMs, patches, overworlds, dungeons, and agents. - Implemented serialization methods for resource schemas in both JSON and YAML formats. - Introduced RomSandboxManager to manage sandboxed ROM copies, allowing for safe experimentation with ROM modifications. - Updated ModernCLI to include new commands for palette management and enhanced help output. - Added unit tests for resource catalog serialization and schema validation.
This commit is contained in:
@@ -1,41 +1,134 @@
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/flags/flag.h"
|
||||
#include "absl/flags/parse.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/match.h"
|
||||
|
||||
#include "cli/modern_cli.h"
|
||||
#include "cli/tui.h"
|
||||
#include "yaze_config.h"
|
||||
|
||||
ABSL_FLAG(bool, tui, false, "Launch Text User Interface");
|
||||
ABSL_FLAG(std::string, rom, "", "Path to the ROM file");
|
||||
ABSL_FLAG(std::string, output, "", "Output file path");
|
||||
ABSL_FLAG(bool, verbose, false, "Enable verbose output");
|
||||
ABSL_FLAG(bool, dry_run, false, "Perform operations without making changes");
|
||||
ABSL_FLAG(bool, backup, true, "Create a backup before modifying files");
|
||||
ABSL_FLAG(std::string, test, "", "Name of the test to run");
|
||||
ABSL_FLAG(bool, show_gui, false, "Show the test engine GUI");
|
||||
|
||||
namespace {
|
||||
|
||||
struct ParsedGlobals {
|
||||
std::vector<char*> positional;
|
||||
bool show_help = false;
|
||||
bool show_version = false;
|
||||
std::optional<std::string> error;
|
||||
};
|
||||
|
||||
ParsedGlobals ParseGlobalFlags(int argc, char* argv[]) {
|
||||
ParsedGlobals result;
|
||||
if (argc <= 0 || argv == nullptr) {
|
||||
result.error = "Invalid argv provided";
|
||||
return result;
|
||||
}
|
||||
|
||||
result.positional.reserve(argc);
|
||||
result.positional.push_back(argv[0]);
|
||||
|
||||
bool passthrough = false;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
char* current = argv[i];
|
||||
std::string_view token(current);
|
||||
|
||||
if (!passthrough) {
|
||||
if (token == "--") {
|
||||
passthrough = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token == "--help" || token == "-h") {
|
||||
result.show_help = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token == "--version") {
|
||||
result.show_version = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token == "--tui") {
|
||||
absl::SetFlag(&FLAGS_tui, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (absl::StartsWith(token, "--rom=")) {
|
||||
absl::SetFlag(&FLAGS_rom, std::string(token.substr(6)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token == "--rom") {
|
||||
if (i + 1 >= argc) {
|
||||
result.error = "--rom flag requires a value";
|
||||
return result;
|
||||
}
|
||||
absl::SetFlag(&FLAGS_rom, std::string(argv[++i]));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.positional.push_back(current);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void PrintVersion() {
|
||||
std::cout << absl::StrFormat("yaze %d.%d.%d", YAZE_VERSION_MAJOR,
|
||||
YAZE_VERSION_MINOR, YAZE_VERSION_PATCH)
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
// Parse command line flags
|
||||
absl::ParseCommandLine(argc, argv);
|
||||
ParsedGlobals globals = ParseGlobalFlags(argc, argv);
|
||||
|
||||
if (globals.error.has_value()) {
|
||||
std::cerr << "Error: " << *globals.error << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (globals.show_version) {
|
||||
PrintVersion();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
// Check if TUI mode is requested
|
||||
if (absl::GetFlag(FLAGS_tui)) {
|
||||
yaze::cli::ShowMain();
|
||||
return 0;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
yaze::cli::ModernCLI cli;
|
||||
|
||||
if (globals.show_help) {
|
||||
cli.PrintTopLevelHelp();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
if (globals.positional.size() <= 1) {
|
||||
cli.PrintTopLevelHelp();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
// Run CLI commands
|
||||
yaze::cli::ModernCLI cli;
|
||||
auto status = cli.Run(argc, argv);
|
||||
auto status = cli.Run(static_cast<int>(globals.positional.size()),
|
||||
globals.positional.data());
|
||||
|
||||
if (!status.ok()) {
|
||||
std::cerr << "Error: " << status.message() << std::endl;
|
||||
return 1;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
#include "cli/z3ed.h"
|
||||
#include "cli/modern_cli.h"
|
||||
#include "cli/service/ai_service.h"
|
||||
#include "cli/service/resource_catalog.h"
|
||||
#include "cli/service/rom_sandbox_manager.h"
|
||||
#include "util/macro.h"
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/ascii.h"
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
#include <cstdlib> // For EXIT_FAILURE
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
|
||||
// Platform-specific includes for process management and executable path detection
|
||||
#if !defined(_WIN32)
|
||||
@@ -24,17 +34,82 @@ namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
struct DescribeOptions {
|
||||
std::optional<std::string> resource;
|
||||
std::string format = "json";
|
||||
std::optional<std::string> output_path;
|
||||
std::string version = "0.1.0";
|
||||
std::optional<std::string> last_updated;
|
||||
};
|
||||
|
||||
absl::StatusOr<DescribeOptions> ParseDescribeArgs(
|
||||
const std::vector<std::string>& args) {
|
||||
DescribeOptions options;
|
||||
for (size_t i = 0; i < args.size(); ++i) {
|
||||
const std::string& token = args[i];
|
||||
std::string flag = token;
|
||||
std::optional<std::string> inline_value;
|
||||
|
||||
if (absl::StartsWith(token, "--")) {
|
||||
auto eq_pos = token.find('=');
|
||||
if (eq_pos != std::string::npos) {
|
||||
flag = token.substr(0, eq_pos);
|
||||
inline_value = token.substr(eq_pos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
auto require_value = [&](absl::string_view flag_name) -> absl::StatusOr<std::string> {
|
||||
if (inline_value.has_value()) {
|
||||
return *inline_value;
|
||||
}
|
||||
if (i + 1 >= args.size()) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Flag %s requires a value", flag_name));
|
||||
}
|
||||
return args[++i];
|
||||
};
|
||||
|
||||
if (flag == "--resource") {
|
||||
ASSIGN_OR_RETURN(auto value, require_value("--resource"));
|
||||
options.resource = std::move(value);
|
||||
} else if (flag == "--format") {
|
||||
ASSIGN_OR_RETURN(auto value, require_value("--format"));
|
||||
options.format = std::move(value);
|
||||
} else if (flag == "--output") {
|
||||
ASSIGN_OR_RETURN(auto value, require_value("--output"));
|
||||
options.output_path = std::move(value);
|
||||
} else if (flag == "--version") {
|
||||
ASSIGN_OR_RETURN(auto value, require_value("--version"));
|
||||
options.version = std::move(value);
|
||||
} else if (flag == "--last-updated") {
|
||||
ASSIGN_OR_RETURN(auto value, require_value("--last-updated"));
|
||||
options.last_updated = std::move(value);
|
||||
} else {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrFormat("Unknown flag for agent describe: %s", token));
|
||||
}
|
||||
}
|
||||
|
||||
options.format = absl::AsciiStrToLower(options.format);
|
||||
if (options.format != "json" && options.format != "yaml") {
|
||||
return absl::InvalidArgumentError("--format must be either json or yaml");
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
absl::Status HandleRunCommand(const std::vector<std::string>& arg_vec, Rom& rom) {
|
||||
if (arg_vec.size() < 2 || arg_vec[0] != "--prompt") {
|
||||
return absl::InvalidArgumentError("Usage: agent run --prompt <prompt>");
|
||||
}
|
||||
std::string prompt = arg_vec[1];
|
||||
|
||||
// Save a temporary copy of the ROM
|
||||
// Save a sandbox copy of the ROM for proposal tracking.
|
||||
if (rom.is_loaded()) {
|
||||
auto status = rom.SaveToFile({.save_new = true, .filename = "temp_rom.sfc"});
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
auto sandbox_or = RomSandboxManager::Instance().CreateSandbox(
|
||||
rom, "agent-run");
|
||||
if (!sandbox_or.ok()) {
|
||||
return sandbox_or.status();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,8 +172,13 @@ absl::Status HandlePlanCommand(const std::vector<std::string>& arg_vec) {
|
||||
|
||||
absl::Status HandleDiffCommand(Rom& rom) {
|
||||
if (rom.is_loaded()) {
|
||||
auto sandbox_or = RomSandboxManager::Instance().ActiveSandbox();
|
||||
if (!sandbox_or.ok()) {
|
||||
return sandbox_or.status();
|
||||
}
|
||||
RomDiff diff_handler;
|
||||
auto status = diff_handler.Run({rom.filename(), "temp_rom.sfc"});
|
||||
auto status = diff_handler.Run(
|
||||
{rom.filename(), sandbox_or->rom_path.string()});
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
@@ -225,11 +305,69 @@ absl::Status HandleRevertCommand(Rom& rom) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status HandleDescribeCommand(const std::vector<std::string>& arg_vec) {
|
||||
ASSIGN_OR_RETURN(auto options, ParseDescribeArgs(arg_vec));
|
||||
|
||||
const auto& catalog = ResourceCatalog::Instance();
|
||||
std::optional<ResourceSchema> resource_schema;
|
||||
if (options.resource.has_value()) {
|
||||
auto resource_or = catalog.GetResource(*options.resource);
|
||||
if (!resource_or.ok()) {
|
||||
return resource_or.status();
|
||||
}
|
||||
resource_schema = resource_or.value();
|
||||
}
|
||||
|
||||
std::string payload;
|
||||
if (options.format == "json") {
|
||||
if (resource_schema.has_value()) {
|
||||
payload = catalog.SerializeResource(*resource_schema);
|
||||
} else {
|
||||
payload = catalog.SerializeResources(catalog.AllResources());
|
||||
}
|
||||
} else {
|
||||
std::string last_updated = options.last_updated.has_value()
|
||||
? *options.last_updated
|
||||
: absl::FormatTime("%Y-%m-%d", absl::Now(),
|
||||
absl::LocalTimeZone());
|
||||
if (resource_schema.has_value()) {
|
||||
std::vector<ResourceSchema> schemas{*resource_schema};
|
||||
payload = catalog.SerializeResourcesAsYaml(
|
||||
schemas, options.version, last_updated);
|
||||
} else {
|
||||
payload = catalog.SerializeResourcesAsYaml(
|
||||
catalog.AllResources(), options.version, last_updated);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.output_path.has_value()) {
|
||||
std::ofstream out(*options.output_path, std::ios::binary | std::ios::trunc);
|
||||
if (!out.is_open()) {
|
||||
return absl::InternalError(absl::StrFormat(
|
||||
"Failed to open %s for writing", *options.output_path));
|
||||
}
|
||||
out << payload;
|
||||
out.close();
|
||||
if (!out) {
|
||||
return absl::InternalError(absl::StrFormat(
|
||||
"Failed to write schema to %s", *options.output_path));
|
||||
}
|
||||
std::cout << absl::StrFormat("Wrote %s schema to %s", options.format,
|
||||
*options.output_path)
|
||||
<< std::endl;
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
std::cout << payload << std::endl;
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
absl::Status Agent::Run(const std::vector<std::string>& arg_vec) {
|
||||
if (arg_vec.empty()) {
|
||||
return absl::InvalidArgumentError("Usage: agent <run|plan|diff|test|learn|commit|revert> [options]");
|
||||
return absl::InvalidArgumentError(
|
||||
"Usage: agent <run|plan|diff|test|learn|commit|revert|describe> [options]");
|
||||
}
|
||||
|
||||
std::string subcommand = arg_vec[0];
|
||||
@@ -249,6 +387,8 @@ absl::Status Agent::Run(const std::vector<std::string>& arg_vec) {
|
||||
return HandleCommitCommand(rom_);
|
||||
} else if (subcommand == "revert") {
|
||||
return HandleRevertCommand(rom_);
|
||||
} else if (subcommand == "describe") {
|
||||
return HandleDescribeCommand(subcommand_args);
|
||||
} else {
|
||||
return absl::InvalidArgumentError("Invalid subcommand for agent command.");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "absl/flags/declare.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
|
||||
#include "app/core/asar_wrapper.h"
|
||||
#include "app/rom.h"
|
||||
@@ -59,7 +60,9 @@ void ModernCLI::SetupCommands() {
|
||||
commands_["agent"] = {
|
||||
.name = "agent",
|
||||
.description = "Interact with the AI agent",
|
||||
.usage = "z3ed agent <run|plan|diff|test> [options]",
|
||||
.usage = "z3ed agent <run|plan|diff|test|learn|commit|revert|describe> [options]\n"
|
||||
" describe options: [--resource <name>] [--format json|yaml] [--output <path>]\n"
|
||||
" [--version <value>] [--last-updated <date>]",
|
||||
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
|
||||
return HandleAgentCommand(args);
|
||||
}
|
||||
@@ -156,6 +159,15 @@ void ModernCLI::SetupCommands() {
|
||||
};
|
||||
|
||||
commands_["palette"] = {
|
||||
.name = "palette",
|
||||
.description = "Manage palette data (export/import)",
|
||||
.usage = "z3ed palette <export|import> [options]",
|
||||
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
|
||||
return HandlePaletteCommand(args);
|
||||
}
|
||||
};
|
||||
|
||||
commands_["palette export"] = {
|
||||
.name = "palette export",
|
||||
.description = "Export a palette to a file",
|
||||
.usage = "z3ed palette export --group <group> --id <id> --to <file>",
|
||||
@@ -209,12 +221,9 @@ void ModernCLI::ShowHelp() {
|
||||
std::cout << std::endl;
|
||||
std::cout << "GLOBAL FLAGS:" << std::endl;
|
||||
std::cout << " --tui Launch Text User Interface" << std::endl;
|
||||
std::cout << " --version Show version information" << std::endl;
|
||||
std::cout << " --verbose Enable verbose output" << std::endl;
|
||||
std::cout << " --rom=<file> Specify ROM file to use" << std::endl;
|
||||
std::cout << " --output=<file> Specify output file path" << std::endl;
|
||||
std::cout << " --dry-run Perform operations without making changes" << std::endl;
|
||||
std::cout << " --backup=<bool> Create backup before modifying (default: true)" << std::endl;
|
||||
std::cout << " --version Show version information" << std::endl;
|
||||
std::cout << " --help Show this help message" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "COMMANDS:" << std::endl;
|
||||
|
||||
@@ -234,31 +243,51 @@ void ModernCLI::ShowHelp() {
|
||||
std::cout << " z3ed help <resource> <action>" << std::endl;
|
||||
}
|
||||
|
||||
void ModernCLI::PrintTopLevelHelp() const {
|
||||
const_cast<ModernCLI*>(this)->ShowHelp();
|
||||
}
|
||||
|
||||
absl::Status ModernCLI::Run(int argc, char* argv[]) {
|
||||
if (argc < 2) {
|
||||
ShowHelp();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
std::string command;
|
||||
std::vector<std::string> command_args;
|
||||
std::vector<std::string> args;
|
||||
args.reserve(argc - 1);
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
args.emplace_back(argv[i]);
|
||||
}
|
||||
|
||||
if (argc >= 3) {
|
||||
command = std::string(argv[1]) + " " + std::string(argv[2]);
|
||||
for (int i = 3; i < argc; ++i) {
|
||||
command_args.push_back(argv[i]);
|
||||
const CommandInfo* command_info = nullptr;
|
||||
size_t consumed_tokens = 0;
|
||||
|
||||
if (args.size() >= 2) {
|
||||
std::string candidate = absl::StrCat(args[0], " ", args[1]);
|
||||
auto it = commands_.find(candidate);
|
||||
if (it != commands_.end()) {
|
||||
command_info = &it->second;
|
||||
consumed_tokens = 2;
|
||||
}
|
||||
} else {
|
||||
command = argv[1];
|
||||
}
|
||||
|
||||
auto it = commands_.find(command);
|
||||
if (it == commands_.end()) {
|
||||
if (command_info == nullptr && !args.empty()) {
|
||||
auto it = commands_.find(args[0]);
|
||||
if (it != commands_.end()) {
|
||||
command_info = &it->second;
|
||||
consumed_tokens = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (command_info == nullptr) {
|
||||
ShowHelp();
|
||||
return absl::NotFoundError(absl::StrCat("Unknown command: ", command));
|
||||
std::string joined = args.empty() ? std::string() : absl::StrJoin(args, " ");
|
||||
return absl::NotFoundError(
|
||||
absl::StrCat("Unknown command: ", joined.empty() ? "<none>" : joined));
|
||||
}
|
||||
|
||||
return it->second.handler(command_args);
|
||||
std::vector<std::string> command_args(args.begin() + consumed_tokens, args.end());
|
||||
return command_info->handler(command_args);
|
||||
}
|
||||
|
||||
CommandHandler* ModernCLI::GetCommandHandler(const std::string& name) {
|
||||
|
||||
@@ -24,6 +24,7 @@ class ModernCLI {
|
||||
ModernCLI();
|
||||
absl::Status Run(int argc, char* argv[]);
|
||||
CommandHandler* GetCommandHandler(const std::string& name);
|
||||
void PrintTopLevelHelp() const;
|
||||
|
||||
std::map<std::string, CommandInfo> commands_;
|
||||
|
||||
|
||||
444
src/cli/service/resource_catalog.cc
Normal file
444
src/cli/service/resource_catalog.cc
Normal file
@@ -0,0 +1,444 @@
|
||||
#include "cli/service/resource_catalog.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
ResourceSchema MakePaletteSchema() {
|
||||
ResourceSchema schema;
|
||||
schema.resource = "palette";
|
||||
schema.description =
|
||||
"Palette manipulation commands covering export, import, and color editing.";
|
||||
|
||||
ResourceAction export_action;
|
||||
export_action.name = "export";
|
||||
export_action.synopsis = "z3ed palette export --group <group> --id <id> --to <file>";
|
||||
export_action.stability = "experimental";
|
||||
export_action.arguments = {
|
||||
ResourceArgument{"--group", "integer", true, "Palette group id (0-31)."},
|
||||
ResourceArgument{"--id", "integer", true, "Palette index inside the group."},
|
||||
ResourceArgument{"--to", "path", true, "Destination file path for binary export."},
|
||||
};
|
||||
export_action.effects = {
|
||||
"Reads ROM palette buffer and writes binary palette data to disk."};
|
||||
|
||||
ResourceAction import_action;
|
||||
import_action.name = "import";
|
||||
import_action.synopsis =
|
||||
"z3ed palette import --group <group> --id <id> --from <file>";
|
||||
import_action.stability = "experimental";
|
||||
import_action.arguments = {
|
||||
ResourceArgument{"--group", "integer", true, "Palette group id (0-31)."},
|
||||
ResourceArgument{"--id", "integer", true, "Palette index inside the group."},
|
||||
ResourceArgument{"--from", "path", true, "Source binary palette file."},
|
||||
};
|
||||
import_action.effects = {
|
||||
"Writes imported palette bytes into ROM buffer and marks project dirty."};
|
||||
|
||||
schema.actions = {export_action, import_action};
|
||||
return schema;
|
||||
}
|
||||
|
||||
ResourceSchema MakeRomSchema() {
|
||||
ResourceSchema schema;
|
||||
schema.resource = "rom";
|
||||
schema.description = "ROM validation, diffing, and snapshot helpers.";
|
||||
|
||||
ResourceAction validate_action;
|
||||
validate_action.name = "validate";
|
||||
validate_action.synopsis = "z3ed rom validate --rom <file>";
|
||||
validate_action.stability = "stable";
|
||||
validate_action.arguments = {
|
||||
ResourceArgument{"--rom", "path", true, "Path to ROM file configured via global flag."},
|
||||
};
|
||||
validate_action.effects = {
|
||||
"Reads ROM from disk, verifies checksum, and reports header status."};
|
||||
validate_action.returns = {
|
||||
{"report", "object",
|
||||
"Structured validation summary with checksum and header results."}};
|
||||
|
||||
ResourceAction diff_action;
|
||||
diff_action.name = "diff";
|
||||
diff_action.synopsis = "z3ed rom diff <rom_a> <rom_b>";
|
||||
diff_action.stability = "beta";
|
||||
diff_action.arguments = {
|
||||
ResourceArgument{"rom_a", "path", true, "Reference ROM path."},
|
||||
ResourceArgument{"rom_b", "path", true, "Candidate ROM path."},
|
||||
};
|
||||
diff_action.effects = {
|
||||
"Reads two ROM images, compares bytes, and streams differences to stdout."};
|
||||
diff_action.returns = {
|
||||
{"differences", "integer", "Count of mismatched bytes between ROMs."}};
|
||||
|
||||
ResourceAction generate_action;
|
||||
generate_action.name = "generate-golden";
|
||||
generate_action.synopsis =
|
||||
"z3ed rom generate-golden <rom_file> <golden_file>";
|
||||
generate_action.stability = "experimental";
|
||||
generate_action.arguments = {
|
||||
ResourceArgument{"rom_file", "path", true, "Source ROM to snapshot."},
|
||||
ResourceArgument{"golden_file", "path", true, "Output path for golden image."},
|
||||
};
|
||||
generate_action.effects = {
|
||||
"Writes out exact ROM image for tooling baselines and diff workflows."};
|
||||
generate_action.returns = {
|
||||
{"artifact", "path", "Absolute path to the generated golden image."}};
|
||||
|
||||
schema.actions = {validate_action, diff_action, generate_action};
|
||||
return schema;
|
||||
}
|
||||
|
||||
ResourceSchema MakePatchSchema() {
|
||||
ResourceSchema schema;
|
||||
schema.resource = "patch";
|
||||
schema.description =
|
||||
"Patch authoring and application commands covering BPS and Asar flows.";
|
||||
|
||||
ResourceAction apply_action;
|
||||
apply_action.name = "apply";
|
||||
apply_action.synopsis = "z3ed patch apply <rom_file> <bps_patch>";
|
||||
apply_action.stability = "beta";
|
||||
apply_action.arguments = {
|
||||
ResourceArgument{"rom_file", "path", true,
|
||||
"Source ROM image that will receive the patch."},
|
||||
ResourceArgument{"bps_patch", "path", true,
|
||||
"BPS patch to apply to the ROM."},
|
||||
};
|
||||
apply_action.effects = {
|
||||
"Loads ROM from disk, applies a BPS patch, and writes `patched.sfc`."};
|
||||
apply_action.returns = {
|
||||
{"artifact", "path",
|
||||
"Absolute path to the patched ROM image produced on success."}};
|
||||
|
||||
ResourceAction asar_action;
|
||||
asar_action.name = "apply-asar";
|
||||
asar_action.synopsis = "z3ed patch apply-asar <patch.asm>";
|
||||
asar_action.stability = "prototype";
|
||||
asar_action.arguments = {
|
||||
ResourceArgument{"patch.asm", "path", true,
|
||||
"Assembly patch consumed by the bundled Asar runtime."},
|
||||
ResourceArgument{"--rom", "path", false,
|
||||
"ROM path supplied via global --rom flag."},
|
||||
};
|
||||
asar_action.effects = {
|
||||
"Invokes Asar against the active ROM buffer and applies assembled changes."};
|
||||
asar_action.returns = {
|
||||
{"log", "string", "Assembler diagnostics emitted during application."}};
|
||||
|
||||
ResourceAction create_action;
|
||||
create_action.name = "create";
|
||||
create_action.synopsis =
|
||||
"z3ed patch create --source <rom> --target <rom> --out <patch.bps>";
|
||||
create_action.stability = "experimental";
|
||||
create_action.arguments = {
|
||||
ResourceArgument{"--source", "path", true,
|
||||
"Baseline ROM used when computing the patch."},
|
||||
ResourceArgument{"--target", "path", true,
|
||||
"Modified ROM to diff against the baseline."},
|
||||
ResourceArgument{"--out", "path", true,
|
||||
"Output path for the generated BPS patch."},
|
||||
};
|
||||
create_action.effects = {
|
||||
"Compares source and target images to synthesize a distributable BPS patch."};
|
||||
create_action.returns = {
|
||||
{"artifact", "path", "File system path to the generated patch."}};
|
||||
|
||||
schema.actions = {apply_action, asar_action, create_action};
|
||||
return schema;
|
||||
}
|
||||
|
||||
ResourceSchema MakeOverworldSchema() {
|
||||
ResourceSchema schema;
|
||||
schema.resource = "overworld";
|
||||
schema.description = "Overworld tile inspection and manipulation commands.";
|
||||
|
||||
ResourceAction get_tile;
|
||||
get_tile.name = "get-tile";
|
||||
get_tile.synopsis = "z3ed overworld get-tile --map <map_id> --x <x> --y <y>";
|
||||
get_tile.stability = "stable";
|
||||
get_tile.arguments = {
|
||||
ResourceArgument{"--map", "integer", true, "Overworld map identifier (0-63)."},
|
||||
ResourceArgument{"--x", "integer", true, "Tile x coordinate."},
|
||||
ResourceArgument{"--y", "integer", true, "Tile y coordinate."},
|
||||
};
|
||||
get_tile.returns = {
|
||||
{"tile", "integer",
|
||||
"Tile id located at the supplied coordinates."}};
|
||||
|
||||
ResourceAction set_tile;
|
||||
set_tile.name = "set-tile";
|
||||
set_tile.synopsis =
|
||||
"z3ed overworld set-tile --map <map_id> --x <x> --y <y> --tile <tile_id>";
|
||||
set_tile.stability = "experimental";
|
||||
set_tile.arguments = {
|
||||
ResourceArgument{"--map", "integer", true, "Overworld map identifier (0-63)."},
|
||||
ResourceArgument{"--x", "integer", true, "Tile x coordinate."},
|
||||
ResourceArgument{"--y", "integer", true, "Tile y coordinate."},
|
||||
ResourceArgument{"--tile", "integer", true, "Tile id to write."},
|
||||
};
|
||||
set_tile.effects = {
|
||||
"Mutates overworld tile map and enqueues render invalidation."};
|
||||
|
||||
schema.actions = {get_tile, set_tile};
|
||||
return schema;
|
||||
}
|
||||
|
||||
ResourceSchema MakeDungeonSchema() {
|
||||
ResourceSchema schema;
|
||||
schema.resource = "dungeon";
|
||||
schema.description = "Dungeon room export and inspection utilities.";
|
||||
|
||||
ResourceAction export_action;
|
||||
export_action.name = "export";
|
||||
export_action.synopsis = "z3ed dungeon export <room_id>";
|
||||
export_action.stability = "prototype";
|
||||
export_action.arguments = {
|
||||
ResourceArgument{"room_id", "integer", true,
|
||||
"Dungeon room identifier to inspect."},
|
||||
};
|
||||
export_action.effects = {
|
||||
"Loads the active ROM via --rom and prints metadata for the requested room."};
|
||||
export_action.returns = {
|
||||
{"metadata", "object",
|
||||
"Structured room summary including blockset, spriteset, palette, and layout."}};
|
||||
|
||||
ResourceAction list_objects_action;
|
||||
list_objects_action.name = "list-objects";
|
||||
list_objects_action.synopsis = "z3ed dungeon list-objects <room_id>";
|
||||
list_objects_action.stability = "prototype";
|
||||
list_objects_action.arguments = {
|
||||
ResourceArgument{"room_id", "integer", true,
|
||||
"Dungeon room identifier whose objects should be listed."},
|
||||
};
|
||||
list_objects_action.effects = {
|
||||
"Streams parsed dungeon object records for the requested room to stdout."};
|
||||
list_objects_action.returns = {
|
||||
{"objects", "array",
|
||||
"Collection of tile object records with ids, coordinates, and layers."}};
|
||||
|
||||
schema.actions = {export_action, list_objects_action};
|
||||
return schema;
|
||||
}
|
||||
|
||||
ResourceSchema MakeAgentSchema() {
|
||||
ResourceSchema schema;
|
||||
schema.resource = "agent";
|
||||
schema.description =
|
||||
"Agent workflow helpers including planning, diffing, and schema discovery.";
|
||||
|
||||
ResourceAction describe_action;
|
||||
describe_action.name = "describe";
|
||||
describe_action.synopsis = "z3ed agent describe --resource <name>";
|
||||
describe_action.stability = "prototype";
|
||||
describe_action.arguments = {
|
||||
ResourceArgument{"--resource", "string", false,
|
||||
"Optional resource name to filter results."},
|
||||
};
|
||||
describe_action.returns = {
|
||||
{"schema", "object",
|
||||
"JSON schema describing resource arguments and semantics."}};
|
||||
|
||||
schema.actions = {describe_action};
|
||||
return schema;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const ResourceCatalog& ResourceCatalog::Instance() {
|
||||
static ResourceCatalog* instance = new ResourceCatalog();
|
||||
return *instance;
|
||||
}
|
||||
|
||||
ResourceCatalog::ResourceCatalog()
|
||||
: resources_({MakeRomSchema(), MakePatchSchema(), MakePaletteSchema(),
|
||||
MakeOverworldSchema(), MakeDungeonSchema(), MakeAgentSchema()}) {}
|
||||
|
||||
absl::StatusOr<ResourceSchema> ResourceCatalog::GetResource(absl::string_view name) const {
|
||||
for (const auto& resource : resources_) {
|
||||
if (resource.resource == name) {
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
return absl::NotFoundError(absl::StrCat("Resource not found: ", name));
|
||||
}
|
||||
|
||||
const std::vector<ResourceSchema>& ResourceCatalog::AllResources() const { return resources_; }
|
||||
|
||||
std::string ResourceCatalog::SerializeResource(const ResourceSchema& schema) const {
|
||||
return SerializeResources({schema});
|
||||
}
|
||||
|
||||
std::string ResourceCatalog::SerializeResources(const std::vector<ResourceSchema>& schemas) const {
|
||||
std::vector<std::string> entries;
|
||||
entries.reserve(schemas.size());
|
||||
for (const auto& resource : schemas) {
|
||||
std::vector<std::string> action_entries;
|
||||
action_entries.reserve(resource.actions.size());
|
||||
for (const auto& action : resource.actions) {
|
||||
std::vector<std::string> arg_entries;
|
||||
arg_entries.reserve(action.arguments.size());
|
||||
for (const auto& arg : action.arguments) {
|
||||
arg_entries.push_back(absl::StrCat(
|
||||
"{\"flag\":\"", EscapeJson(arg.flag),
|
||||
"\",\"type\":\"", EscapeJson(arg.type),
|
||||
"\",\"required\":", arg.required ? "true" : "false",
|
||||
",\"description\":\"", EscapeJson(arg.description), "\"}"));
|
||||
}
|
||||
std::vector<std::string> effect_entries;
|
||||
effect_entries.reserve(action.effects.size());
|
||||
for (const auto& effect : action.effects) {
|
||||
effect_entries.push_back(absl::StrCat("\"", EscapeJson(effect), "\""));
|
||||
}
|
||||
std::vector<std::string> return_entries;
|
||||
return_entries.reserve(action.returns.size());
|
||||
for (const auto& ret : action.returns) {
|
||||
return_entries.push_back(absl::StrCat(
|
||||
"{\"field\":\"", EscapeJson(ret.field),
|
||||
"\",\"type\":\"", EscapeJson(ret.type),
|
||||
"\",\"description\":\"", EscapeJson(ret.description), "\"}"));
|
||||
}
|
||||
action_entries.push_back(absl::StrCat(
|
||||
"{\"name\":\"", EscapeJson(action.name),
|
||||
"\",\"synopsis\":\"", EscapeJson(action.synopsis),
|
||||
"\",\"stability\":\"", EscapeJson(action.stability),
|
||||
"\",\"arguments\":[", absl::StrJoin(arg_entries, ","), "],",
|
||||
"\"effects\":[", absl::StrJoin(effect_entries, ","), "],",
|
||||
"\"returns\":[", absl::StrJoin(return_entries, ","), "]}"));
|
||||
}
|
||||
entries.push_back(absl::StrCat(
|
||||
"{\"resource\":\"", EscapeJson(resource.resource),
|
||||
"\",\"description\":\"", EscapeJson(resource.description),
|
||||
"\",\"actions\":[", absl::StrJoin(action_entries, ","), "]}"));
|
||||
}
|
||||
return absl::StrCat("{\"resources\":[", absl::StrJoin(entries, ","), "]}");
|
||||
}
|
||||
|
||||
std::string ResourceCatalog::EscapeJson(absl::string_view value) {
|
||||
std::string out;
|
||||
out.reserve(value.size());
|
||||
for (char c : value) {
|
||||
switch (c) {
|
||||
case '\\':
|
||||
out += "\\\\";
|
||||
break;
|
||||
case '\"':
|
||||
out += "\\\"";
|
||||
break;
|
||||
case '\n':
|
||||
out += "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
out += "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
out += "\\t";
|
||||
break;
|
||||
default:
|
||||
out += c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string ResourceCatalog::SerializeResourcesAsYaml(
|
||||
const std::vector<ResourceSchema>& schemas,
|
||||
absl::string_view version,
|
||||
absl::string_view last_updated) const {
|
||||
std::string out;
|
||||
absl::StrAppend(&out, "# Auto-generated resource catalogue\n");
|
||||
absl::StrAppend(&out, "version: ", EscapeYaml(version), "\n");
|
||||
absl::StrAppend(&out, "last_updated: ", EscapeYaml(last_updated), "\n");
|
||||
absl::StrAppend(&out, "resources:\n");
|
||||
|
||||
for (const auto& resource : schemas) {
|
||||
absl::StrAppend(&out, " - name: ", EscapeYaml(resource.resource), "\n");
|
||||
absl::StrAppend(&out, " description: ", EscapeYaml(resource.description), "\n");
|
||||
|
||||
if (resource.actions.empty()) {
|
||||
absl::StrAppend(&out, " actions: []\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
absl::StrAppend(&out, " actions:\n");
|
||||
for (const auto& action : resource.actions) {
|
||||
absl::StrAppend(&out, " - name: ", EscapeYaml(action.name), "\n");
|
||||
absl::StrAppend(&out, " synopsis: ", EscapeYaml(action.synopsis), "\n");
|
||||
absl::StrAppend(&out, " stability: ", EscapeYaml(action.stability), "\n");
|
||||
|
||||
if (action.arguments.empty()) {
|
||||
absl::StrAppend(&out, " args: []\n");
|
||||
} else {
|
||||
absl::StrAppend(&out, " args:\n");
|
||||
for (const auto& arg : action.arguments) {
|
||||
absl::StrAppend(&out, " - flag: ", EscapeYaml(arg.flag), "\n");
|
||||
absl::StrAppend(&out, " type: ", EscapeYaml(arg.type), "\n");
|
||||
absl::StrAppend(&out, " required: ", arg.required ? "true\n" : "false\n");
|
||||
absl::StrAppend(&out, " description: ", EscapeYaml(arg.description), "\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (action.effects.empty()) {
|
||||
absl::StrAppend(&out, " effects: []\n");
|
||||
} else {
|
||||
absl::StrAppend(&out, " effects:\n");
|
||||
for (const auto& effect : action.effects) {
|
||||
absl::StrAppend(&out, " - ", EscapeYaml(effect), "\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (action.returns.empty()) {
|
||||
absl::StrAppend(&out, " returns: []\n");
|
||||
} else {
|
||||
absl::StrAppend(&out, " returns:\n");
|
||||
for (const auto& ret : action.returns) {
|
||||
absl::StrAppend(&out, " - field: ", EscapeYaml(ret.field), "\n");
|
||||
absl::StrAppend(&out, " type: ", EscapeYaml(ret.type), "\n");
|
||||
absl::StrAppend(&out, " description: ", EscapeYaml(ret.description), "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string ResourceCatalog::EscapeYaml(absl::string_view value) {
|
||||
std::string out;
|
||||
out.reserve(value.size() + 2);
|
||||
out.push_back('"');
|
||||
for (char c : value) {
|
||||
switch (c) {
|
||||
case '\\':
|
||||
out += "\\\\";
|
||||
break;
|
||||
case '"':
|
||||
out += "\\\"";
|
||||
break;
|
||||
case '\n':
|
||||
out += "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
out += "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
out += "\\t";
|
||||
break;
|
||||
default:
|
||||
out.push_back(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
out.push_back('"');
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
71
src/cli/service/resource_catalog.h
Normal file
71
src/cli/service/resource_catalog.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#ifndef YAZE_SRC_CLI_SERVICE_RESOURCE_CATALOG_H_
|
||||
#define YAZE_SRC_CLI_SERVICE_RESOURCE_CATALOG_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
struct ResourceArgument {
|
||||
std::string flag;
|
||||
std::string type;
|
||||
bool required;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
struct ResourceAction {
|
||||
std::string name;
|
||||
std::string synopsis;
|
||||
std::string stability;
|
||||
std::vector<ResourceArgument> arguments;
|
||||
std::vector<std::string> effects;
|
||||
struct ReturnValue {
|
||||
std::string field;
|
||||
std::string type;
|
||||
std::string description;
|
||||
};
|
||||
std::vector<ReturnValue> returns;
|
||||
};
|
||||
|
||||
struct ResourceSchema {
|
||||
std::string resource;
|
||||
std::string description;
|
||||
std::vector<ResourceAction> actions;
|
||||
};
|
||||
|
||||
// ResourceCatalog exposes a machine-readable description of CLI resources so that
|
||||
// both humans and AI agents can introspect capabilities at runtime.
|
||||
class ResourceCatalog {
|
||||
public:
|
||||
static const ResourceCatalog& Instance();
|
||||
|
||||
absl::StatusOr<ResourceSchema> GetResource(absl::string_view name) const;
|
||||
const std::vector<ResourceSchema>& AllResources() const;
|
||||
|
||||
// Serialize helpers for `z3ed agent describe`. These return compact JSON
|
||||
// strings so we avoid introducing a hard dependency on nlohmann::json.
|
||||
std::string SerializeResource(const ResourceSchema& schema) const;
|
||||
std::string SerializeResources(const std::vector<ResourceSchema>& schemas) const;
|
||||
std::string SerializeResourcesAsYaml(
|
||||
const std::vector<ResourceSchema>& schemas,
|
||||
absl::string_view version,
|
||||
absl::string_view last_updated) const;
|
||||
|
||||
private:
|
||||
ResourceCatalog();
|
||||
|
||||
static std::string EscapeJson(absl::string_view value);
|
||||
static std::string EscapeYaml(absl::string_view value);
|
||||
|
||||
std::vector<ResourceSchema> resources_;
|
||||
};
|
||||
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_SRC_CLI_SERVICE_RESOURCE_CATALOG_H_
|
||||
212
src/cli/service/rom_sandbox_manager.cc
Normal file
212
src/cli/service/rom_sandbox_manager.cc
Normal file
@@ -0,0 +1,212 @@
|
||||
#include "cli/service/rom_sandbox_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
#include "util/macro.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path DetermineDefaultRoot() {
|
||||
if (const char* env_root = std::getenv("YAZE_SANDBOX_ROOT")) {
|
||||
return std::filesystem::path(env_root);
|
||||
}
|
||||
std::error_code ec;
|
||||
auto temp_dir = std::filesystem::temp_directory_path(ec);
|
||||
if (ec) {
|
||||
// Fallback to current working directory if temp is unavailable.
|
||||
return std::filesystem::current_path() / "yaze" / "sandboxes";
|
||||
}
|
||||
return temp_dir / "yaze" / "sandboxes";
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveUniqueDirectory(
|
||||
const std::filesystem::path& root,
|
||||
absl::string_view id) {
|
||||
return root / std::string(id);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RomSandboxManager& RomSandboxManager::Instance() {
|
||||
static RomSandboxManager* instance = new RomSandboxManager();
|
||||
return *instance;
|
||||
}
|
||||
|
||||
RomSandboxManager::RomSandboxManager()
|
||||
: root_directory_(DetermineDefaultRoot()) {}
|
||||
|
||||
void RomSandboxManager::SetRootDirectory(const std::filesystem::path& root) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
root_directory_ = root;
|
||||
(void)EnsureRootExistsLocked();
|
||||
}
|
||||
|
||||
const std::filesystem::path& RomSandboxManager::RootDirectory() const {
|
||||
return root_directory_;
|
||||
}
|
||||
|
||||
absl::Status RomSandboxManager::EnsureRootExistsLocked() {
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(root_directory_, ec)) {
|
||||
if (!std::filesystem::create_directories(root_directory_, ec) && ec) {
|
||||
return absl::InternalError(absl::StrCat(
|
||||
"Failed to create sandbox root at ", root_directory_.string(),
|
||||
": ", ec.message()));
|
||||
}
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
std::string RomSandboxManager::GenerateSandboxIdLocked() {
|
||||
absl::Time now = absl::Now();
|
||||
std::string time_component = absl::FormatTime("%Y%m%dT%H%M%S", now,
|
||||
absl::LocalTimeZone());
|
||||
++sequence_;
|
||||
return absl::StrCat(time_component, "-", sequence_);
|
||||
}
|
||||
|
||||
absl::StatusOr<RomSandboxManager::SandboxMetadata>
|
||||
RomSandboxManager::CreateSandbox(Rom& rom, absl::string_view description) {
|
||||
if (!rom.is_loaded()) {
|
||||
return absl::FailedPreconditionError(
|
||||
"Cannot create sandbox: ROM is not loaded");
|
||||
}
|
||||
|
||||
std::filesystem::path source_path(rom.filename());
|
||||
if (source_path.empty()) {
|
||||
return absl::FailedPreconditionError(
|
||||
"Cannot create sandbox: ROM filename is empty");
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
RETURN_IF_ERROR(EnsureRootExistsLocked());
|
||||
|
||||
std::string id = GenerateSandboxIdLocked();
|
||||
std::filesystem::path sandbox_dir =
|
||||
ResolveUniqueDirectory(root_directory_, id);
|
||||
lock.unlock();
|
||||
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::create_directories(sandbox_dir, ec) && ec) {
|
||||
return absl::InternalError(absl::StrCat(
|
||||
"Failed to create sandbox directory at ", sandbox_dir.string(),
|
||||
": ", ec.message()));
|
||||
}
|
||||
|
||||
std::filesystem::path sandbox_rom_path = sandbox_dir / source_path.filename();
|
||||
|
||||
Rom::SaveSettings settings;
|
||||
settings.filename = sandbox_rom_path.string();
|
||||
settings.save_new = false;
|
||||
settings.backup = false;
|
||||
settings.z3_save = true;
|
||||
|
||||
absl::Status save_status = rom.SaveToFile(settings);
|
||||
if (!save_status.ok()) {
|
||||
std::error_code cleanup_ec;
|
||||
std::filesystem::remove_all(sandbox_dir, cleanup_ec);
|
||||
return save_status;
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
sandboxes_[id] = SandboxMetadata{
|
||||
.id = id,
|
||||
.directory = sandbox_dir,
|
||||
.rom_path = sandbox_rom_path,
|
||||
.source_rom = source_path.string(),
|
||||
.description = std::string(description),
|
||||
.created_at = absl::Now(),
|
||||
};
|
||||
active_sandbox_id_ = id;
|
||||
|
||||
return sandboxes_.at(id);
|
||||
}
|
||||
|
||||
absl::StatusOr<RomSandboxManager::SandboxMetadata>
|
||||
RomSandboxManager::ActiveSandbox() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!active_sandbox_id_.has_value()) {
|
||||
return absl::NotFoundError("No active sandbox");
|
||||
}
|
||||
auto it = sandboxes_.find(*active_sandbox_id_);
|
||||
if (it == sandboxes_.end()) {
|
||||
return absl::NotFoundError("Active sandbox metadata missing");
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
absl::StatusOr<std::filesystem::path>
|
||||
RomSandboxManager::ActiveSandboxRomPath() const {
|
||||
ASSIGN_OR_RETURN(auto meta, ActiveSandbox());
|
||||
return meta.rom_path;
|
||||
}
|
||||
|
||||
std::vector<RomSandboxManager::SandboxMetadata>
|
||||
RomSandboxManager::ListSandboxes() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::vector<SandboxMetadata> list;
|
||||
list.reserve(sandboxes_.size());
|
||||
for (const auto& [_, metadata] : sandboxes_) {
|
||||
list.push_back(metadata);
|
||||
}
|
||||
std::sort(list.begin(), list.end(),
|
||||
[](const SandboxMetadata& a, const SandboxMetadata& b) {
|
||||
return a.created_at < b.created_at;
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
absl::Status RomSandboxManager::RemoveSandbox(const std::string& id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = sandboxes_.find(id);
|
||||
if (it == sandboxes_.end()) {
|
||||
return absl::NotFoundError("Sandbox not found");
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(it->second.directory, ec);
|
||||
if (ec) {
|
||||
return absl::InternalError(absl::StrCat(
|
||||
"Failed to remove sandbox directory: ", ec.message()));
|
||||
}
|
||||
sandboxes_.erase(it);
|
||||
if (active_sandbox_id_.has_value() && *active_sandbox_id_ == id) {
|
||||
active_sandbox_id_.reset();
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<int> RomSandboxManager::CleanupOlderThan(absl::Duration max_age) {
|
||||
std::vector<std::string> to_remove;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
absl::Time threshold = absl::Now() - max_age;
|
||||
for (const auto& [id, metadata] : sandboxes_) {
|
||||
if (metadata.created_at < threshold) {
|
||||
to_remove.push_back(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int removed = 0;
|
||||
for (const auto& id : to_remove) {
|
||||
absl::Status status = RemoveSandbox(id);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
++removed;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
92
src/cli/service/rom_sandbox_manager.h
Normal file
92
src/cli/service/rom_sandbox_manager.h
Normal file
@@ -0,0 +1,92 @@
|
||||
#ifndef YAZE_SRC_CLI_SERVICE_ROM_SANDBOX_MANAGER_H_
|
||||
#define YAZE_SRC_CLI_SERVICE_ROM_SANDBOX_MANAGER_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/base/thread_annotations.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
#include "app/rom.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
// RomSandboxManager coordinates creation and lifecycle management of sandboxed
|
||||
// ROM copies. Agent workflows operate on sandboxes so that proposals can be
|
||||
// reviewed before landing in the primary project ROM. The manager currently
|
||||
// tracks sandboxes in-memory for the running process and persists files to a
|
||||
// configurable root directory on disk.
|
||||
class RomSandboxManager {
|
||||
public:
|
||||
struct SandboxMetadata {
|
||||
std::string id;
|
||||
std::filesystem::path directory;
|
||||
std::filesystem::path rom_path;
|
||||
std::string source_rom;
|
||||
std::string description;
|
||||
absl::Time created_at;
|
||||
};
|
||||
|
||||
static RomSandboxManager& Instance();
|
||||
|
||||
// Set the root directory used for new sandboxes. Must be called before any
|
||||
// sandboxes are created. If not set, a default rooted at the system temporary
|
||||
// directory is used (or the value of the YAZE_SANDBOX_ROOT environment
|
||||
// variable when present).
|
||||
void SetRootDirectory(const std::filesystem::path& root);
|
||||
|
||||
const std::filesystem::path& RootDirectory() const;
|
||||
|
||||
// Creates a new sandbox by copying the provided ROM into a unique directory
|
||||
// under the root. The new sandbox becomes the active sandbox for the current
|
||||
// process. Metadata is returned describing the sandbox on success.
|
||||
absl::StatusOr<SandboxMetadata> CreateSandbox(Rom& rom,
|
||||
absl::string_view description);
|
||||
|
||||
// Returns the metadata for the active sandbox if one exists.
|
||||
absl::StatusOr<SandboxMetadata> ActiveSandbox() const;
|
||||
|
||||
// Returns the absolute path to the active sandbox ROM copy. Equivalent to
|
||||
// ActiveSandbox()->rom_path but with more descriptive errors when unset.
|
||||
absl::StatusOr<std::filesystem::path> ActiveSandboxRomPath() const;
|
||||
|
||||
// List all sandboxes tracked during the current process lifetime. This will
|
||||
// include the active sandbox (if any) and previously created sandboxes that
|
||||
// have not been cleaned up.
|
||||
std::vector<SandboxMetadata> ListSandboxes() const;
|
||||
|
||||
// Removes the sandbox identified by |id| from the index and deletes its on
|
||||
// disk directory. If the sandbox is currently active the active sandbox is
|
||||
// cleared. Missing sandboxes result in a NotFound status.
|
||||
absl::Status RemoveSandbox(const std::string& id);
|
||||
|
||||
// Deletes any sandboxes that are older than |max_age|, returning the number
|
||||
// of sandboxes removed. This is currently best-effort; individual removals
|
||||
// may produce errors which are aggregated into the returned status.
|
||||
absl::StatusOr<int> CleanupOlderThan(absl::Duration max_age);
|
||||
|
||||
private:
|
||||
RomSandboxManager();
|
||||
|
||||
absl::Status EnsureRootExistsLocked();
|
||||
std::string GenerateSandboxIdLocked();
|
||||
|
||||
std::filesystem::path root_directory_;
|
||||
mutable std::mutex mutex_;
|
||||
std::unordered_map<std::string, SandboxMetadata> sandboxes_;
|
||||
std::optional<std::string> active_sandbox_id_;
|
||||
int sequence_ ABSL_GUARDED_BY(mutex_) = 0;
|
||||
};
|
||||
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_SRC_CLI_SERVICE_ROM_SANDBOX_MANAGER_H_
|
||||
@@ -43,6 +43,8 @@ add_executable(
|
||||
cli/handlers/project.cc
|
||||
cli/handlers/agent.cc
|
||||
cli/service/ai_service.cc
|
||||
cli/service/resource_catalog.cc
|
||||
cli/service/rom_sandbox_manager.cc
|
||||
cli/service/gemini_ai_service.cc
|
||||
app/rom.cc
|
||||
app/core/project.cc
|
||||
|
||||
Reference in New Issue
Block a user