7#include "absl/strings/str_cat.h"
8#include "absl/strings/str_format.h"
9#include "absl/strings/str_join.h"
18 escaped.reserve(input.size());
19 for (
char c : input) {
22 escaped.append(
"\\\\");
25 escaped.append(
"\\\"");
28 escaped.append(
"\\n");
31 escaped.append(
"\\r");
34 escaped.append(
"\\t");
44 const std::vector<std::string>& values) {
46 for (
size_t i = 0; i < values.size(); ++i) {
57 static bool initialized =
false;
66 std::unique_ptr<resources::CommandHandler> handler,
68 std::string name = handler->GetName();
74 for (
const auto& alias : metadata.
aliases) {
83 std::vector<std::unique_ptr<resources::CommandHandler>> handlers) {
84 for (
auto& handler : handlers) {
85 std::string name = handler->GetName();
92 auto descriptor = handler->Describe();
95 metadata.
usage = handler->GetUsage();
101 if (name.find(
"resource-") == 0) {
103 metadata.
description =
"Resource inspection and search";
104 if (name ==
"resource-list") {
105 metadata.
examples = {
"z3ed resource-list --type=dungeon --format=json",
106 "z3ed resource-list --type=sprite --format=table"};
108 }
else if (name.find(
"dungeon-") == 0) {
110 metadata.
description =
"Dungeon inspection and editing";
111 if (name ==
"dungeon-describe-room") {
113 "z3ed dungeon-describe-room --room=5 --format=json"};
114 }
else if (name ==
"dungeon-stream-plan") {
116 "Read-only stream inventory, alias/overlap, and free-space "
117 "diagnostics; replacement-aware immutable move output is pending";
119 "z3ed dungeon-stream-plan --kind=objects "
120 "--manifest=hack_manifest.json --rom=game.sfc --format=json",
121 "z3ed dungeon-stream-plan --kind=pot_items "
122 "--manifest=hack_manifest.json --rom=game.sfc --format=text"};
123 }
else if (name ==
"dungeon-place-sprite") {
125 "Place a dungeon sprite (dry-run by default, --write to apply)";
127 "z3ed dungeon-place-sprite --room=0x77 --id=0xA3 --x=16 --y=21 "
128 "--subtype=4 --rom=Roms/oos168.sfc --format=json",
129 "z3ed dungeon-place-sprite --room=0x77 --id=0xA3 --x=16 --y=21 "
130 "--subtype=4 --write --rom=/tmp/oos-work.sfc --format=json"};
131 }
else if (name ==
"dungeon-remove-sprite") {
133 "Remove a dungeon sprite by index or exact coordinates";
135 "z3ed dungeon-remove-sprite --room=0x77 --index=2 "
136 "--rom=/tmp/oos-work.sfc --format=json",
137 "z3ed dungeon-remove-sprite --room=0x77 --x=16 --y=21 --write "
138 "--rom=/tmp/oos-work.sfc --format=json"};
139 }
else if (name ==
"dungeon-place-object") {
141 "Place a dungeon object with immutable capacity/COW preflight "
142 "(dry-run by default)";
144 "z3ed dungeon-place-object --room=0x98 --id=0x0031 --x=20 --y=20 "
145 "--size=4 --rom=/tmp/oos-work.sfc --format=json",
146 "z3ed dungeon-place-object --room=0x98 --id=0x0031 --x=20 --y=20 "
147 "--size=4 --manifest=hack_manifest.json --write "
148 "--rom=/tmp/oos-work.sfc --format=json"};
149 }
else if (name ==
"dungeon-remove-object") {
151 "Remove one ordinary dungeon object with exact identity guards "
152 "(dry-run by default)";
154 "z3ed dungeon-remove-object --room=0x88 --index=117 "
155 "--expect-id=0x0DD --expect-x=30 --expect-y=12 "
156 "--expect-size=0 --expect-layer=0 "
157 "--rom=/tmp/oos-work.sfc --format=json",
158 "z3ed dungeon-remove-object --room=0x88 --index=117 "
159 "--expect-id=0x0DD --expect-x=30 --expect-y=12 "
160 "--expect-size=0 --expect-layer=0 "
161 "--manifest=hack_manifest.json --write "
162 "--rom=/tmp/oos-work.sfc --format=json"};
163 }
else if (name ==
"dungeon-get-palette") {
165 "Resolve a US/OOS room's full 8-bit palette-set ID to its shared "
166 "global dungeon palette and report every affected room; other "
167 "regional layouts fail closed";
169 "z3ed dungeon-get-palette --room=0xA8 "
170 "--rom=Roms/oos168.sfc --format=json",
171 "z3ed dungeon-get-palette --room=0xA8 --index=0x07 "
172 "--rom=Roms/oos168.sfc --format=json"};
173 }
else if (name ==
"dungeon-set-palette-color") {
175 "Dry-run or persist one US/OOS shared dungeon palette color with "
176 "palette-set/index/color CAS, manifest ownership, exact fence, "
177 "clean-file baseline, required backup, atomic save, and external "
180 "z3ed dungeon-set-palette-color --room=0xA8 --index=7 "
181 "--expect-palette-set=0x07 --expect-palette-index=7 "
182 "--expect-color=0x7FFF --color=0x7FFE "
183 "--manifest=Roms/hack_manifest.json "
184 "--rom=Roms/oos168.sfc --format=json",
185 "z3ed dungeon-set-palette-color --room=0xA8 --index=7 "
186 "--expect-palette-set=0x07 --expect-palette-index=7 "
187 "--expect-color=0x7FFF --color=0x7FFE "
188 "--manifest=Roms/hack_manifest.json --write "
189 "--rom=/tmp/oos-work.sfc --format=json"};
190 }
else if (name ==
"dungeon-set-door-type") {
192 "Change one existing door type with coordinate/CAS and manifest "
193 "guards (dry-run by default)";
195 "z3ed dungeon-set-door-type --room=0xA8 --x=46 --y=58 "
196 "--type=0x00 --expect-type=0x18 "
197 "--manifest=hack_manifest.json --rom=Roms/oos168.sfc "
199 "z3ed dungeon-set-door-type --room=0xA8 --x=46 --y=58 "
200 "--type=0x00 --expect-type=0x18 "
201 "--manifest=hack_manifest.json --write "
202 "--rom=/tmp/oos-work.sfc --format=json"};
203 }
else if (name ==
"dungeon-list-pot-items") {
205 "List existing pot items with raw positions and ROM addresses";
207 "z3ed dungeon-list-pot-items --room=0xA8 "
208 "--rom=Roms/oos168.sfc --format=json"};
209 }
else if (name ==
"dungeon-set-pot-item") {
211 "Change one existing pot item with index/position/item CAS and "
212 "manifest guards (dry-run by default)";
214 "z3ed dungeon-set-pot-item --room=0xA8 --index=0 "
215 "--expect-position=0x198C --expect-item=0x05 --item=0x06 "
216 "--manifest=Roms/hack_manifest.json --rom=Roms/oos168.sfc "
218 "z3ed dungeon-set-pot-item --room=0xA8 --index=0 "
219 "--expect-position=0x198C --expect-item=0x05 --item=0x06 "
220 "--manifest=Roms/hack_manifest.json --write "
221 "--rom=/tmp/oos-work.sfc --format=json"};
222 }
else if (name ==
"dungeon-oracle-preflight") {
224 "Oracle ROM safety preflight: water-fill region/table, collision "
225 "maps, and optional required-room checks";
227 "z3ed dungeon-oracle-preflight --rom oos168x.sfc --format=json",
228 "z3ed dungeon-oracle-preflight --rom oos168x.sfc "
229 "--required-water-fill-rooms=0x25,0x27 --format=json",
230 "z3ed dungeon-oracle-preflight --rom oos168x.sfc "
231 "--report=/tmp/preflight.json"};
232 }
else if (name ==
"dungeon-set-collision-tile") {
234 "Set one or more custom collision tiles in a dungeon room";
236 "z3ed dungeon-set-collision-tile --room=0xB8 "
237 "--tiles=\"10,5,0xB7;50,45,0xBA\" --rom=/tmp/oos-work.sfc "
239 "z3ed dungeon-set-collision-tile --room=0xB8 "
240 "--tiles=\"10,5,0xB7;50,45,0xBA\" --write "
241 "--rom=/tmp/oos-work.sfc --format=json"};
243 }
else if (name.find(
"overworld-") == 0) {
245 metadata.
description =
"Overworld inspection and editing";
246 if (name ==
"overworld-find-tile") {
248 "z3ed overworld-find-tile --tile=0x42 --format=json"};
250 }
else if (name.find(
"project-bundle-") == 0) {
253 if (name ==
"project-bundle-verify") {
254 metadata.
description =
"Project bundle verification";
256 "z3ed project-bundle-verify --project MyProject.yazeproj "
258 "z3ed project-bundle-verify --project project.yaze "
259 "--report report.json",
260 "z3ed project-bundle-verify --project MyProject.yazeproj "
261 "--check-rom-hash --format=json"};
262 }
else if (name ==
"project-bundle-pack") {
263 metadata.
description =
"Pack bundle into zip archive";
265 "z3ed project-bundle-pack --project MyProject.yazeproj "
266 "--out MyProject.zip --format=json",
267 "z3ed project-bundle-pack --project MyProject.yazeproj "
268 "--out MyProject.zip --overwrite"};
269 }
else if (name ==
"project-bundle-unpack") {
270 metadata.
description =
"Unpack zip archive into bundle";
272 "z3ed project-bundle-unpack --archive MyProject.zip "
273 "--out ./projects --format=json",
274 "z3ed project-bundle-unpack --archive MyProject.zip "
275 "--out ./projects --overwrite",
276 "z3ed project-bundle-unpack --archive MyProject.zip "
277 "--out ./projects --keep-partial-output",
278 "z3ed project-bundle-unpack --archive MyProject.zip "
279 "--out ./projects --dry-run"};
281 }
else if (name.find(
"rom-") == 0) {
283 metadata.
description =
"ROM inspection and validation";
284 if (name ==
"rom-info") {
285 metadata.
examples = {
"z3ed rom-info --rom=zelda3.sfc"};
286 }
else if (name ==
"rom-validate") {
287 metadata.
examples = {
"z3ed rom-validate --rom=zelda3.sfc"};
288 }
else if (name ==
"rom-doctor") {
289 metadata.
examples = {
"z3ed rom-doctor --rom=zelda3.sfc --format=json"};
290 }
else if (name ==
"rom-diff") {
292 "z3ed rom-diff --rom_a=base.sfc --rom_b=target.sfc"};
293 }
else if (name ==
"rom-compare") {
295 "z3ed rom-compare --rom=target.sfc --baseline=vanilla.sfc"};
297 }
else if (name.find(
"emulator-") == 0) {
299 metadata.
description =
"Emulator control and debugging";
301 if (name ==
"emulator-set-breakpoint") {
303 "z3ed emulator-set-breakpoint --address=0x83D7 --description='NMI "
306 }
else if (name.find(
"mesen-") == 0) {
308 metadata.
description =
"Mesen2 socket automation and introspection";
310 if (name ==
"mesen-state-capture") {
312 "z3ed mesen-state-capture --state=foo.state "
313 "--rom-file=Roms/oos168x.sfc --scenario=d6_room_88",
314 "z3ed mesen-state-capture --state=Roms/SaveStates/d6_room_88.state "
315 "--rom-file=Roms/oos168x.sfc --slot=1 "
316 "--states-dir=~/Library/Application\\ Support/Mesen2/SaveStates"};
317 }
else if (name ==
"mesen-state-hook") {
319 "z3ed mesen-state-hook --state=foo.state "
320 "--rom-file=Roms/oos168x.sfc --scenario=d6_room_88",
321 "z3ed mesen-state-hook --state=foo.state "
322 "--rom-file=Roms/oos168x.sfc --format=json"};
324 }
else if (name.find(
"gui-") == 0) {
328 }
else if (name.find(
"hex-") == 0) {
331 }
else if (name.find(
"palette-") == 0) {
334 }
else if (name.find(
"sprite-") == 0) {
337 }
else if (name.find(
"message-") == 0 || name.find(
"dialogue-") == 0) {
339 metadata.
description = name.find(
"message-") == 0 ?
"Message inspection"
340 :
"Dialogue inspection";
341 if (name ==
"message-source-sync") {
343 "Dry-run or CAS-publish a complete canonical expanded-message "
344 "bundle and deterministic no-org Asar include";
346 "z3ed message-source-sync --project=Oracle-of-Secrets.yaze "
347 "--file=/tmp/message-edits.json --format=json",
348 "z3ed message-source-sync --project=Oracle-of-Secrets.yaze "
349 "--file=/tmp/message-edits.json "
350 "--expected-source-sha256=<sha256> --write --format=json"};
352 }
else if (name.find(
"music-") == 0) {
355 }
else if (name.find(
"oracle-") == 0) {
357 metadata.
description =
"Oracle-of-Secrets project tooling";
358 if (name ==
"oracle-menu-index") {
360 "z3ed oracle-menu-index --project=/path/to/oracle-of-secrets "
362 "z3ed oracle-menu-index --table=Menu_ItemCursorPositions "
363 "--missing-bins --format=json"};
364 }
else if (name ==
"oracle-menu-set-offset") {
366 "z3ed oracle-menu-set-offset "
367 "--asm=Menu/menu_select_item.asm "
368 "--table=Menu_ItemCursorPositions --index=0 --row=7 --col=2",
369 "z3ed oracle-menu-set-offset "
370 "--asm=Menu/menu_select_item.asm "
371 "--table=Menu_ItemCursorPositions --index=0 --row=7 --col=2 "
373 }
else if (name ==
"oracle-menu-validate") {
375 "z3ed oracle-menu-validate --project=/path/to/oracle-of-secrets",
376 "z3ed oracle-menu-validate --project=/path/to/oracle-of-secrets "
377 "--strict --max-row=31 --max-col=31"};
378 }
else if (name ==
"oracle-smoke-check") {
380 "z3ed oracle-smoke-check --rom oos168.sfc --format=json",
381 "z3ed oracle-smoke-check --rom oos168.sfc --strict-readiness "
383 "z3ed oracle-smoke-check --rom oos168.sfc "
384 "--min-d6-track-rooms=4 --format=json",
385 "z3ed oracle-smoke-check --rom oos168.sfc "
386 "--report=/tmp/smoke.json"};
388 }
else if (name ==
"simple-chat" || name ==
"chat") {
394 "z3ed simple-chat --rom=zelda3.sfc",
395 "z3ed simple-chat \"What dungeons exist?\" --rom=zelda3.sfc"};
396 }
else if (name.find(
"tools-") == 0) {
398 if (name ==
"tools-list") {
399 metadata.
description =
"List available test helper tools";
402 }
else if (name ==
"tools-harness-state") {
403 metadata.
description =
"Generate WRAM state for test harnesses";
405 "z3ed tools-harness-state --rom=zelda3.sfc --output=state.h"};
406 }
else if (name ==
"tools-extract-values") {
407 metadata.
description =
"Extract vanilla ROM values";
408 metadata.
examples = {
"z3ed tools-extract-values --rom=zelda3.sfc"};
409 }
else if (name ==
"tools-extract-golden") {
410 metadata.
description =
"Extract comprehensive golden data for testing";
412 "z3ed tools-extract-golden --rom=zelda3.sfc --output=golden.h"};
413 }
else if (name ==
"tools-patch-v3") {
414 metadata.
description =
"Create v3 ZSCustomOverworld patched ROM";
416 "z3ed tools-patch-v3 --rom=zelda3.sfc --output=patched.sfc"};
420 }
else if (name.find(
"test-") == 0) {
422 metadata.
description =
"Test discovery and execution";
423 if (name ==
"test-list") {
425 metadata.
examples = {
"z3ed test-list",
"z3ed test-list --format json"};
426 }
else if (name ==
"test-run") {
427 metadata.
examples = {
"z3ed test-run --label stable",
428 "z3ed test-run --label gui"};
429 }
else if (name ==
"test-status") {
431 metadata.
examples = {
"z3ed test-status --format json"};
439 if (!descriptor.summary.empty() &&
440 descriptor.summary !=
"Command summary not provided.") {
445 if (!descriptor.todo_reference.empty() &&
446 descriptor.todo_reference !=
"todo#unassigned") {
450 Register(std::move(handler), metadata);
458 return it->second.get();
462 auto alias_it =
aliases_.find(name);
464 auto handler_it =
handlers_.find(alias_it->second);
466 return handler_it->second.get();
474 const std::string& name)
const {
476 std::string canonical_name = name;
477 auto alias_it =
aliases_.find(name);
479 canonical_name = alias_it->second;
482 auto it =
metadata_.find(canonical_name);
483 return (it !=
metadata_.end()) ? &it->second :
nullptr;
487 const std::string& category)
const {
488 std::vector<std::string> result;
489 for (
const auto& [name, metadata] :
metadata_) {
490 if (metadata.category == category) {
491 result.push_back(name);
498 std::vector<std::string> categories;
499 for (
const auto& [_, metadata] :
metadata_) {
500 if (std::find(categories.begin(), categories.end(), metadata.category) ==
502 categories.push_back(metadata.category);
509 std::vector<std::string> result;
510 for (
const auto& [name, metadata] :
metadata_) {
511 if (metadata.available_to_agent) {
512 result.push_back(name);
519 std::ostringstream out;
520 out <<
"{\n \"commands\": [\n";
523 for (
const auto& [_, metadata] :
metadata_) {
529 out <<
" \"name\": \"" << EscapeJson(metadata.name) <<
"\",\n";
530 out <<
" \"category\": \"" << EscapeJson(metadata.category) <<
"\",\n";
531 out <<
" \"description\": \"" << EscapeJson(metadata.description)
533 out <<
" \"usage\": \"" << EscapeJson(metadata.usage) <<
"\",\n";
534 out <<
" \"available_to_agent\": "
535 << (metadata.available_to_agent ?
"true" :
"false") <<
",\n";
536 out <<
" \"requires_rom\": "
537 << (metadata.requires_rom ?
"true" :
"false") <<
",\n";
538 out <<
" \"requires_grpc\": "
539 << (metadata.requires_grpc ?
"true" :
"false") <<
",\n";
540 if (!metadata.todo_reference.empty()) {
541 out <<
" \"todo_reference\": \""
542 << EscapeJson(metadata.todo_reference) <<
"\",\n";
544 out <<
" \"todo_reference\": \"\",\n";
546 out <<
" \"aliases\": ";
547 AppendStringArray(out, metadata.aliases);
549 out <<
" \"examples\": ";
550 AppendStringArray(out, metadata.examples);
562 return absl::StrFormat(
"Command '%s' not found", name);
565 std::ostringstream help;
566 help <<
"\n\033[1;36m" << metadata->name <<
"\033[0m - "
567 << metadata->description <<
"\n\n";
568 help <<
"\033[1;33mUsage:\033[0m\n";
569 help <<
" " << metadata->usage <<
"\n\n";
571 if (!metadata->examples.empty()) {
572 help <<
"\033[1;33mExamples:\033[0m\n";
573 for (
const auto& example : metadata->examples) {
574 help <<
" " << example <<
"\n";
579 if (metadata->requires_rom) {
580 help <<
"\033[1;33mRequires:\033[0m ROM file (--rom=<path>)\n";
582 if (metadata->requires_grpc) {
583 help <<
"\033[1;33mRequires:\033[0m YAZE running with gRPC enabled\n";
586 if (!metadata->aliases.empty()) {
587 help <<
"\n\033[1;33mAliases:\033[0m "
588 << absl::StrJoin(metadata->aliases,
", ") <<
"\n";
595 const std::string& category)
const {
597 if (commands.empty()) {
598 return absl::StrFormat(
"No commands in category '%s'", category);
601 std::ostringstream help;
602 help <<
"\n\033[1;36m" << category <<
" commands:\033[0m\n\n";
604 for (
const auto& cmd : commands) {
607 help <<
" \033[1;33m" << cmd <<
"\033[0m\n";
608 help <<
" " << metadata->description <<
"\n";
609 if (!metadata->usage.empty()) {
610 help <<
" Usage: " << metadata->usage <<
"\n";
620 std::ostringstream help;
621 help <<
"\n\033[1;36mAll z3ed Commands:\033[0m\n\n";
624 for (
const auto& category : categories) {
632 const std::vector<std::string>& args,
634 std::string* captured_output) {
635 auto* handler =
Get(name);
637 return absl::NotFoundError(absl::StrFormat(
"Command '%s' not found", name));
640 const bool command_help_requested =
641 std::find(args.begin(), args.end(),
"--help") != args.end() ||
642 std::find(args.begin(), args.end(),
"-h") != args.end();
643 if (command_help_requested) {
645 if (captured_output !=
nullptr) {
646 *captured_output = help;
648 std::cout << help <<
"\n";
650 return absl::OkStatus();
653 absl::Status status = handler->Run(args, rom_context, captured_output);
657 if (!status.ok() && status.code() == absl::StatusCode::kInvalidArgument &&
660 if (captured_output !=
nullptr) {
661 if (!captured_output->empty()) {
662 captured_output->append(
"\n\n");
664 captured_output->append(help);
666 std::cout << help <<
"\n";
674 return Get(name) !=
nullptr;
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Single source of truth for all z3ed commands.
void RegisterCliCommands()
std::map< std::string, std::string > aliases_
std::vector< std::string > GetCommandsInCategory(const std::string &category) const
Get all commands in a category.
static CommandRegistry & Instance()
std::string GenerateCategoryHelp(const std::string &category) const
Generate category help text.
std::string ExportFunctionSchemas() const
Export function schemas for AI tool calling (JSON)
const CommandMetadata * GetMetadata(const std::string &name) const
Get command metadata.
std::string GenerateCompleteHelp() const
Generate complete help text (all commands)
std::vector< std::string > GetAgentCommands() const
Get all commands available to AI agents.
std::map< std::string, CommandMetadata > metadata_
absl::Status Execute(const std::string &name, const std::vector< std::string > &args, Rom *rom_context=nullptr, std::string *captured_output=nullptr)
Execute a command by name.
std::map< std::string, std::unique_ptr< resources::CommandHandler > > handlers_
resources::CommandHandler * Get(const std::string &name) const
Get a command handler by name or alias.
bool HasCommand(const std::string &name) const
Check if command exists.
std::vector< std::string > GetCategories() const
Get all categories.
void Register(std::unique_ptr< resources::CommandHandler > handler, const CommandMetadata &metadata)
Register a command handler.
std::string GenerateHelp(const std::string &name) const
Generate help text for a command.
void RegisterHandlers(std::vector< std::unique_ptr< resources::CommandHandler > > handlers)
Register a set of command handlers (idempotent)
Base class for CLI command handlers.
std::string EscapeJson(const std::string &input)
void AppendStringArray(std::ostringstream &out, const std::vector< std::string > &values)
std::vector< std::unique_ptr< resources::CommandHandler > > CreateCliCommandHandlers()
Factory function to create all CLI-level command handlers.