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:
scawful
2025-10-01 14:34:58 -04:00
parent ec687d737a
commit 04a4d04f4e
13 changed files with 1409 additions and 39 deletions

View File

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

View File

@@ -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.");
}

View File

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

View File

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

View 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

View 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_

View 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

View 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_

View File

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