Created: October 11, 2025
Status: Implementation Complete
Overview
This guide documents the new command abstraction layer for z3ed CLI commands. The abstraction layer eliminates ~500+ lines of duplicated code across tool commands and provides a consistent, maintainable architecture for future command development.
Problem Statement
Before Abstraction
The original tool_commands.cc
(1549 lines) had severe code duplication:
- ROM Loading: Every command had 20-30 lines of identical ROM loading logic
- Argument Parsing: Each command manually parsed
--format
, --rom
, --type
, etc.
- Output Formatting: JSON vs text formatting was duplicated across every command
- Label Initialization: Resource label loading was repeated in every handler
- Error Handling: Inconsistent error messages and validation patterns
Code Duplication Example
Rom rom_storage;
Rom* rom = nullptr;
if (rom_context != nullptr && rom_context->is_loaded()) {
rom = rom_context;
} else {
auto rom_or = LoadRomFromFlag();
if (!rom_or.ok()) {
return rom_or.status();
}
rom_storage = std::move(rom_or.value());
rom = &rom_storage;
}
if (rom->resource_label()) {
if (!rom->resource_label()->labels_loaded_) {
core::YazeProject project;
project.use_embedded_labels = true;
auto labels_status = project.InitializeEmbeddedLabels();
}
}
std::string format = "json";
for (size_t i = 0; i < arg_vec.size(); ++i) {
const std::string& token = arg_vec[i];
if (token == "--format") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--format requires a value.");
}
format = arg_vec[++i];
} else if (absl::StartsWith(token, "--format=")) {
format = token.substr(9);
}
}
if (format == "json") {
std::cout << "{\n";
std::cout << " \"field\": \"value\",\n";
std::cout << "}\n";
} else {
std::cout << "Field: value\n";
}
Solution Architecture
Three-Layer Abstraction
- CommandContext - ROM loading, context management
- ArgumentParser - Unified argument parsing
- OutputFormatter - Consistent output formatting
- CommandHandler (Optional) - Base class for structured commands
File Structure
src/cli/service/resources/
├── command_context.h # Context management
├── command_context.cc
├── command_handler.h # Base handler class
├── command_handler.cc
└── (existing files...)
src/cli/handlers/agent/
├── tool_commands.cc # Original (to be refactored)
├── tool_commands_refactored.cc # Example refactored commands
└── (other handlers...)
Core Components
1. CommandContext
Encapsulates ROM loading and common context:
CommandContext::Config config;
config.external_rom_context = rom_context;
config.rom_path = "/path/to/rom.sfc";
config.use_mock_rom = false;
config.format = "json";
CommandContext context(config);
#define RETURN_IF_ERROR(expression)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Benefits:
- Single location for ROM loading logic
- Automatic error handling
- Mock ROM support for testing
- Label management abstraction
2. ArgumentParser
Unified argument parsing with type safety:
ArgumentParser parser(arg_vec);
auto type = parser.GetString("type");
auto format = parser.GetString("format").value_or("json");
if (parser.HasFlag("verbose")) {
}
Benefits:
- Consistent argument parsing across all commands
- Type-safe with proper error handling
- Supports both
--arg=value
and --arg value
forms
- Built-in hex parsing for ROM addresses
3. OutputFormatter
Consistent JSON/text output:
formatter.BeginObject("Room Information");
formatter.AddField("room_id", "0x12");
formatter.AddHexField("address", 0x1234, 4);
formatter.AddField("sprite_count", 5);
formatter.BeginArray("sprites");
formatter.AddArrayItem("Sprite 1");
formatter.AddArrayItem("Sprite 2");
formatter.EndArray();
formatter.EndObject();
formatter.Print();
Output (JSON):
{
"room_id": "0x12",
"address": "0x1234",
"sprite_count": 5,
"sprites": [
"Sprite 1",
"Sprite 2"
]
}
Output (Text):
=== Room Information ===
room_id : 0x12
address : 0x1234
sprite_count : 5
sprites:
- Sprite 1
- Sprite 2
Benefits:
- No manual JSON escaping
- Consistent formatting rules
- Easy to switch between JSON and text
- Proper indentation handling
4. CommandHandler (Optional Base Class)
For more complex commands, use the base class pattern:
class MyCommandHandler : public CommandHandler {
protected:
std::string GetUsage() const override {
return "agent my-command --required <value> [--format <json|text>]";
}
absl::Status ValidateArgs(const ArgumentParser& parser) override {
return parser.RequireArgs({"required"});
}
absl::Status Execute(Rom* rom, const ArgumentParser& parser,
OutputFormatter& formatter) override {
auto value = parser.GetString("required").value();
formatter.AddField("result", value);
return absl::OkStatus();
}
bool RequiresLabels() const override { return true; }
};
absl::Status HandleMyCommand(const std::vector<std::string>& args, Rom* rom) {
MyCommandHandler handler;
return handler.Run(args, rom);
}
Benefits:
- Enforces consistent structure
- Automatic context setup and teardown
- Built-in error handling
- Easy to test individual components
Migration Guide
Step-by-Step Refactoring
Before (80 lines):
absl::Status HandleResourceListCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
std::string type;
std::string format = "table";
for (size_t i = 0; i < arg_vec.size(); ++i) {
const std::string& token = arg_vec[i];
if (token == "--type") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--type requires a value.");
}
type = arg_vec[++i];
} else if (absl::StartsWith(token, "--type=")) {
type = token.substr(7);
}
}
if (type.empty()) {
return absl::InvalidArgumentError("Usage: ...");
}
Rom rom_storage;
Rom* rom = nullptr;
if (rom_context != nullptr && rom_context->is_loaded()) {
rom = rom_context;
} else {
if (!rom_or.ok()) {
return rom_or.status();
}
rom_storage = std::move(rom_or.value());
rom = &rom_storage;
}
if (rom->resource_label()) {
if (!rom->resource_label()->labels_loaded_) {
core::YazeProject project;
project.use_embedded_labels = true;
auto labels_status = project.InitializeEmbeddedLabels();
if (labels_status.ok()) {
rom->resource_label()->labels_ = project.resource_labels;
rom->resource_label()->labels_loaded_ = true;
}
}
}
ResourceContextBuilder context_builder(rom);
auto labels_or = context_builder.GetLabels(type);
if (!labels_or.ok()) {
return labels_or.status();
}
auto labels = std::move(labels_or.value());
if (format == "json") {
std::cout << "{\n";
for (const auto& [key, value] : labels) {
std::cout << " \"" << key << "\": \"" << value << "\",\n";
}
std::cout << "}\n";
} else {
for (const auto& [key, value] : labels) {
std::cout << key << ": " << value << "\n";
}
}
return absl::OkStatus();
}
absl::StatusOr< Rom > LoadRomFromFlag()
After (30 lines):
absl::Status HandleResourceListCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
ArgumentParser parser(arg_vec);
auto type = parser.GetString("type");
auto format_str = parser.GetString("format").value_or("table");
if (!type.has_value()) {
return absl::InvalidArgumentError(
"Usage: agent resource-list --type <type> [--format <table|json>]");
}
CommandContext::Config config;
config.external_rom_context = rom_context;
CommandContext context(config);
ResourceContextBuilder builder(rom);
formatter.BeginObject("Labels");
for (const auto& [key, value] : labels) {
formatter.AddField(key, value);
}
formatter.EndObject();
formatter.Print();
return absl::OkStatus();
}
Savings: 50+ lines eliminated, clearer intent, easier to maintain
Commands to Refactor
Priority order for refactoring (based on duplication level):
- ✅ High Priority (Heavy duplication):
HandleResourceListCommand
- Example provided ✓
HandleResourceSearchCommand
- Example provided ✓
HandleDungeonDescribeRoomCommand
- 80 lines → ~35 lines
HandleOverworldDescribeMapCommand
- 100 lines → ~40 lines
HandleOverworldListWarpsCommand
- 120 lines → ~45 lines
- Medium Priority (Moderate duplication):
HandleDungeonListSpritesCommand
HandleOverworldFindTileCommand
HandleOverworldListSpritesCommand
HandleOverworldGetEntranceCommand
HandleOverworldTileStatsCommand
- Low Priority (Simple commands, less duplication):
HandleMessageListCommand
(delegates to message handler)
HandleMessageReadCommand
(delegates to message handler)
HandleMessageSearchCommand
(delegates to message handler)
Estimated Impact
Metric | Before | After | Savings |
Lines of code (tool_commands.cc) | 1549 | ~800 | 48% |
Duplicated ROM loading | ~600 lines | 0 | 600 lines |
Duplicated arg parsing | ~400 lines | 0 | 400 lines |
Duplicated formatting | ~300 lines | 0 | 300 lines |
Total Duplication Removed | | | **~1300 lines** |
Testing Strategy
Unit Testing
TEST(CommandContextTest, LoadsRomFromConfig) {
CommandContext::Config config;
config.rom_path = "test.sfc";
CommandContext context(config);
auto rom_or = context.GetRom();
EXPECT_TRUE(rom_or.value()->is_loaded());
}
TEST(ArgumentParserTest, ParsesStringArguments) {
std::vector<std::string> args = {"--type=dungeon", "--format", "json"};
ArgumentParser parser(args);
EXPECT_EQ(parser.GetString("type").value(), "dungeon");
EXPECT_EQ(parser.GetString("format").value(), "json");
}
TEST(OutputFormatterTest, GeneratesValidJson) {
auto formatter = OutputFormatter::FromString("json").value();
formatter.BeginObject("Test");
formatter.AddField("key", "value");
formatter.EndObject();
std::string output = formatter.GetOutput();
EXPECT_THAT(output, HasSubstr("\"key\": \"value\""));
}
TEST(ResourceCatalogTest, SerializeResourceIncludesReturnsArray)
Integration Testing
TEST(ResourceListCommandTest, ListsDungeons) {
std::vector<std::string> args = {"--type=dungeon", "--format=json"};
Rom rom;
rom.LoadFromFile("test.sfc");
auto status = HandleResourceListCommand(args, &rom);
}
Benefits Summary
For Developers
- Less Code to Write: New commands take 30-40 lines instead of 80-120
- Consistent Patterns: All commands follow the same structure
- Better Error Handling: Standardized error messages and validation
- Easier Testing: Each component can be tested independently
- Self-Documenting: Clear separation of concerns
For Maintainability
- Single Source of Truth: ROM loading logic in one place
- Easy to Update: Change all commands by updating one class
- Consistent Behavior: All commands handle errors the same way
- Reduced Bugs: Less duplication = fewer places for bugs
For AI Integration
- Predictable Structure: AI can generate commands using templates
- Type Safety: ArgumentParser prevents common errors
- Consistent Output: AI can reliably parse JSON responses
- Easy to Extend: New tool types follow existing patterns
Next Steps
Immediate (Current PR)
- ✅ Create abstraction layer (CommandContext, ArgumentParser, OutputFormatter)
- ✅ Add CommandHandler base class
- ✅ Provide refactored examples
- ✅ Update build system
- ✅ Document architecture
Phase 2 (Next PR)
- Refactor high-priority commands (5 commands)
- Add comprehensive unit tests
- Update AI tool dispatcher to use new patterns
- Create command generator templates for AI
Phase 3 (Future)
- Refactor remaining commands
- Remove old helper functions
- Add performance benchmarks
- Create VS Code snippets for command development
Migration Checklist
For each command being refactored:
- [ ] Replace manual argument parsing with ArgumentParser
- [ ] Replace ROM loading with CommandContext
- [ ] Replace label initialization with context.EnsureLabelsLoaded()
- [ ] Replace manual formatting with OutputFormatter
- [ ] Update error messages to use GetUsage()
- [ ] Add unit tests for the command
- [ ] Update documentation
- [ ] Test with both JSON and text output
- [ ] Test with missing/invalid arguments
- [ ] Test with mock ROM
References
- Implementation:
src/cli/service/resources/command_context.{h,cc}
- Examples:
src/cli/handlers/agent/tool_commands_refactored.cc
- Base class:
src/cli/service/resources/command_handler.{h,cc}
- Build config:
src/cli/agent.cmake
Questions & Answers
Q: Should I refactor all commands at once?
A: No. Refactor in phases to minimize risk. Start with 2-3 commands as proof of concept.
Q: What if my command needs custom argument handling?
A: ArgumentParser is flexible. You can still access raw args or add custom parsing logic.
Q: Can I use both old and new patterns temporarily?
A: Yes. The new abstraction layer works alongside existing code. Migrate gradually.
Q: Will this affect AI tool calling?
A: No breaking changes. The command interfaces remain the same. Internal implementation improves.
Q: How do I test commands with the new abstractions?
A: Use CommandContext with mock ROM, or pass external rom_context in tests.
Last Updated: October 11, 2025
Author: AI Assistant
Review Status: Ready for Implementation