yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
command_registry.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <iostream>
5#include <sstream>
6
7#include "absl/strings/str_cat.h"
8#include "absl/strings/str_format.h"
9#include "absl/strings/str_join.h"
11
12namespace yaze {
13namespace cli {
14
15namespace {
16std::string EscapeJson(const std::string& input) {
17 std::string escaped;
18 escaped.reserve(input.size());
19 for (char c : input) {
20 switch (c) {
21 case '\\\\':
22 escaped.append("\\\\");
23 break;
24 case '\"':
25 escaped.append("\\\"");
26 break;
27 case '\n':
28 escaped.append("\\n");
29 break;
30 case '\r':
31 escaped.append("\\r");
32 break;
33 case '\t':
34 escaped.append("\\t");
35 break;
36 default:
37 escaped.push_back(c);
38 }
39 }
40 return escaped;
41}
42
43void AppendStringArray(std::ostringstream& out,
44 const std::vector<std::string>& values) {
45 out << "[";
46 for (size_t i = 0; i < values.size(); ++i) {
47 if (i > 0)
48 out << ", ";
49 out << "\"" << EscapeJson(values[i]) << "\"";
50 }
51 out << "]";
52}
53} // namespace
54
56 static CommandRegistry instance;
57 static bool initialized = false;
58 if (!initialized) {
59 instance.RegisterCliCommands();
60 initialized = true;
61 }
62 return instance;
63}
64
66 std::unique_ptr<resources::CommandHandler> handler,
67 const CommandMetadata& metadata) {
68 std::string name = handler->GetName();
69
70 // Store metadata
71 metadata_[name] = metadata;
72
73 // Register aliases
74 for (const auto& alias : metadata.aliases) {
75 aliases_[alias] = name;
76 }
77
78 // Store handler
79 handlers_[name] = std::move(handler);
80}
81
83 std::vector<std::unique_ptr<resources::CommandHandler>> handlers) {
84 for (auto& handler : handlers) {
85 std::string name = handler->GetName();
86
87 if (handlers_.find(name) != handlers_.end()) {
88 continue;
89 }
90
91 // Build metadata from handler
92 auto descriptor = handler->Describe();
93 CommandMetadata metadata;
94 metadata.name = name;
95 metadata.usage = handler->GetUsage();
96 metadata.available_to_agent = true; // Most commands available to agent
97 metadata.requires_rom = handler->RequiresRom();
98 metadata.requires_grpc = false;
99
100 // Categorize and enhance metadata based on command type
101 if (name.find("resource-") == 0) {
102 metadata.category = "resource";
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"};
107 }
108 } else if (name.find("dungeon-") == 0) {
109 metadata.category = "dungeon";
110 metadata.description = "Dungeon inspection and editing";
111 if (name == "dungeon-describe-room") {
112 metadata.examples = {
113 "z3ed dungeon-describe-room --room=5 --format=json"};
114 } else if (name == "dungeon-stream-plan") {
115 metadata.description =
116 "Read-only stream inventory, alias/overlap, and free-space "
117 "diagnostics; replacement-aware immutable move output is pending";
118 metadata.examples = {
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") {
124 metadata.description =
125 "Place a dungeon sprite (dry-run by default, --write to apply)";
126 metadata.examples = {
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") {
132 metadata.description =
133 "Remove a dungeon sprite by index or exact coordinates";
134 metadata.examples = {
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") {
140 metadata.description =
141 "Place a dungeon object with immutable capacity/COW preflight "
142 "(dry-run by default)";
143 metadata.examples = {
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") {
150 metadata.description =
151 "Remove one ordinary dungeon object with exact identity guards "
152 "(dry-run by default)";
153 metadata.examples = {
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") {
164 metadata.description =
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";
168 metadata.examples = {
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") {
174 metadata.description =
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 "
178 "reopen guards";
179 metadata.examples = {
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") {
191 metadata.description =
192 "Change one existing door type with coordinate/CAS and manifest "
193 "guards (dry-run by default)";
194 metadata.examples = {
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 "
198 "--format=json",
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") {
204 metadata.description =
205 "List existing pot items with raw positions and ROM addresses";
206 metadata.examples = {
207 "z3ed dungeon-list-pot-items --room=0xA8 "
208 "--rom=Roms/oos168.sfc --format=json"};
209 } else if (name == "dungeon-set-pot-item") {
210 metadata.description =
211 "Change one existing pot item with index/position/item CAS and "
212 "manifest guards (dry-run by default)";
213 metadata.examples = {
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 "
217 "--format=json",
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") {
223 metadata.description =
224 "Oracle ROM safety preflight: water-fill region/table, collision "
225 "maps, and optional required-room checks";
226 metadata.examples = {
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") {
233 metadata.description =
234 "Set one or more custom collision tiles in a dungeon room";
235 metadata.examples = {
236 "z3ed dungeon-set-collision-tile --room=0xB8 "
237 "--tiles=\"10,5,0xB7;50,45,0xBA\" --rom=/tmp/oos-work.sfc "
238 "--format=json",
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"};
242 }
243 } else if (name.find("overworld-") == 0) {
244 metadata.category = "overworld";
245 metadata.description = "Overworld inspection and editing";
246 if (name == "overworld-find-tile") {
247 metadata.examples = {
248 "z3ed overworld-find-tile --tile=0x42 --format=json"};
249 } else if (name == "overworld-allocation") {
250 metadata.description =
251 "Area ID allocation map ($00-$BF) and TailMapExpansion check";
252 metadata.examples = {
253 "z3ed overworld-allocation --rom=zelda3.sfc",
254 "z3ed overworld-allocation --rom=oos168x.sfc "
255 "--project=Oracle-of-Secrets.yaze --tail-check --format=json"};
256 }
257 } else if (name.find("project-bundle-") == 0) {
258 metadata.category = "project";
259 metadata.requires_rom = false;
260 if (name == "project-bundle-verify") {
261 metadata.description = "Project bundle verification";
262 metadata.examples = {
263 "z3ed project-bundle-verify --project MyProject.yazeproj "
264 "--format=json",
265 "z3ed project-bundle-verify --project project.yaze "
266 "--report report.json",
267 "z3ed project-bundle-verify --project MyProject.yazeproj "
268 "--check-rom-hash --format=json"};
269 } else if (name == "project-bundle-pack") {
270 metadata.description = "Pack bundle into zip archive";
271 metadata.examples = {
272 "z3ed project-bundle-pack --project MyProject.yazeproj "
273 "--out MyProject.zip --format=json",
274 "z3ed project-bundle-pack --project MyProject.yazeproj "
275 "--out MyProject.zip --overwrite"};
276 } else if (name == "project-bundle-unpack") {
277 metadata.description = "Unpack zip archive into bundle";
278 metadata.examples = {
279 "z3ed project-bundle-unpack --archive MyProject.zip "
280 "--out ./projects --format=json",
281 "z3ed project-bundle-unpack --archive MyProject.zip "
282 "--out ./projects --overwrite",
283 "z3ed project-bundle-unpack --archive MyProject.zip "
284 "--out ./projects --keep-partial-output",
285 "z3ed project-bundle-unpack --archive MyProject.zip "
286 "--out ./projects --dry-run"};
287 }
288 } else if (name.find("rom-") == 0) {
289 metadata.category = "rom";
290 metadata.description = "ROM inspection and validation";
291 if (name == "rom-info") {
292 metadata.examples = {"z3ed rom-info --rom=zelda3.sfc"};
293 } else if (name == "rom-validate") {
294 metadata.examples = {"z3ed rom-validate --rom=zelda3.sfc"};
295 } else if (name == "rom-doctor") {
296 metadata.examples = {"z3ed rom-doctor --rom=zelda3.sfc --format=json"};
297 } else if (name == "rom-diff") {
298 metadata.examples = {
299 "z3ed rom-diff --rom_a=base.sfc --rom_b=target.sfc"};
300 } else if (name == "rom-compare") {
301 metadata.examples = {
302 "z3ed rom-compare --rom=target.sfc --baseline=vanilla.sfc"};
303 }
304 } else if (name.find("emulator-") == 0) {
305 metadata.category = "emulator";
306 metadata.description = "Emulator control and debugging";
307 metadata.requires_grpc = true;
308 if (name == "emulator-set-breakpoint") {
309 metadata.examples = {
310 "z3ed emulator-set-breakpoint --address=0x83D7 --description='NMI "
311 "handler'"};
312 }
313 } else if (name.find("mesen-") == 0) {
314 metadata.category = "mesen2";
315 metadata.description = "Mesen2 socket automation and introspection";
316 metadata.requires_rom = false;
317 if (name == "mesen-state-capture") {
318 metadata.examples = {
319 "z3ed mesen-state-capture --state=foo.state "
320 "--rom-file=Roms/oos168x.sfc --scenario=d6_room_88",
321 "z3ed mesen-state-capture --state=Roms/SaveStates/d6_room_88.state "
322 "--rom-file=Roms/oos168x.sfc --slot=1 "
323 "--states-dir=~/Library/Application\\ Support/Mesen2/SaveStates"};
324 } else if (name == "mesen-state-hook") {
325 metadata.examples = {
326 "z3ed mesen-state-hook --state=foo.state "
327 "--rom-file=Roms/oos168x.sfc --scenario=d6_room_88",
328 "z3ed mesen-state-hook --state=foo.state "
329 "--rom-file=Roms/oos168x.sfc --format=json"};
330 }
331 } else if (name.find("gui-") == 0) {
332 metadata.category = "gui";
333 metadata.description = "GUI automation";
334 metadata.requires_grpc = true;
335 } else if (name.find("hex-") == 0) {
336 metadata.category = "graphics";
337 metadata.description = "Hex data manipulation";
338 } else if (name.find("palette-") == 0) {
339 metadata.category = "graphics";
340 metadata.description = "Palette operations";
341 } else if (name.find("sprite-") == 0) {
342 metadata.category = "graphics";
343 metadata.description = "Sprite operations";
344 } else if (name.find("message-") == 0 || name.find("dialogue-") == 0) {
345 metadata.category = "game";
346 metadata.description = name.find("message-") == 0 ? "Message inspection"
347 : "Dialogue inspection";
348 if (name == "message-source-sync") {
349 metadata.description =
350 "Dry-run or CAS-publish a complete canonical expanded-message "
351 "bundle and deterministic no-org Asar include";
352 metadata.examples = {
353 "z3ed message-source-sync --project=Oracle-of-Secrets.yaze "
354 "--file=/tmp/message-edits.json --format=json",
355 "z3ed message-source-sync --project=Oracle-of-Secrets.yaze "
356 "--file=/tmp/message-edits.json "
357 "--expected-source-sha256=<sha256> --write --format=json"};
358 }
359 } else if (name.find("music-") == 0) {
360 metadata.category = "game";
361 metadata.description = "Music/audio inspection";
362 } else if (name.find("oracle-") == 0) {
363 metadata.category = "oracle";
364 metadata.description = "Oracle-of-Secrets project tooling";
365 if (name == "oracle-menu-index") {
366 metadata.examples = {
367 "z3ed oracle-menu-index --project=/path/to/oracle-of-secrets "
368 "--format=json",
369 "z3ed oracle-menu-index --table=Menu_ItemCursorPositions "
370 "--missing-bins --format=json"};
371 } else if (name == "oracle-menu-set-offset") {
372 metadata.examples = {
373 "z3ed oracle-menu-set-offset "
374 "--asm=Menu/menu_select_item.asm "
375 "--table=Menu_ItemCursorPositions --index=0 --row=7 --col=2",
376 "z3ed oracle-menu-set-offset "
377 "--asm=Menu/menu_select_item.asm "
378 "--table=Menu_ItemCursorPositions --index=0 --row=7 --col=2 "
379 "--write"};
380 } else if (name == "oracle-menu-validate") {
381 metadata.examples = {
382 "z3ed oracle-menu-validate --project=/path/to/oracle-of-secrets",
383 "z3ed oracle-menu-validate --project=/path/to/oracle-of-secrets "
384 "--strict --max-row=31 --max-col=31"};
385 } else if (name == "oracle-smoke-check") {
386 metadata.examples = {
387 "z3ed oracle-smoke-check --rom oos168.sfc --format=json",
388 "z3ed oracle-smoke-check --rom oos168.sfc --strict-readiness "
389 "--format=json",
390 "z3ed oracle-smoke-check --rom oos168.sfc "
391 "--min-d6-track-rooms=4 --format=json",
392 "z3ed oracle-smoke-check --rom oos168.sfc "
393 "--report=/tmp/smoke.json"};
394 }
395 } else if (name == "simple-chat" || name == "chat") {
396 metadata.category = "agent";
397 metadata.description = "AI conversational agent";
398 metadata.available_to_agent = false; // Meta-command
399 metadata.requires_rom = false;
400 metadata.examples = {
401 "z3ed simple-chat --rom=zelda3.sfc",
402 "z3ed simple-chat \"What dungeons exist?\" --rom=zelda3.sfc"};
403 } else if (name.find("tools-") == 0) {
404 metadata.category = "tools";
405 if (name == "tools-list") {
406 metadata.description = "List available test helper tools";
407 metadata.requires_rom = false;
408 metadata.available_to_agent = true;
409 } else if (name == "tools-harness-state") {
410 metadata.description = "Generate WRAM state for test harnesses";
411 metadata.examples = {
412 "z3ed tools-harness-state --rom=zelda3.sfc --output=state.h"};
413 } else if (name == "tools-extract-values") {
414 metadata.description = "Extract vanilla ROM values";
415 metadata.examples = {"z3ed tools-extract-values --rom=zelda3.sfc"};
416 } else if (name == "tools-extract-golden") {
417 metadata.description = "Extract comprehensive golden data for testing";
418 metadata.examples = {
419 "z3ed tools-extract-golden --rom=zelda3.sfc --output=golden.h"};
420 } else if (name == "tools-patch-v3") {
421 metadata.description = "Create v3 ZSCustomOverworld patched ROM";
422 metadata.examples = {
423 "z3ed tools-patch-v3 --rom=zelda3.sfc --output=patched.sfc"};
424 } else {
425 metadata.description = "Test helper tool";
426 }
427 } else if (name.find("test-") == 0) {
428 metadata.category = "test";
429 metadata.description = "Test discovery and execution";
430 if (name == "test-list") {
431 metadata.requires_rom = false;
432 metadata.examples = {"z3ed test-list", "z3ed test-list --format json"};
433 } else if (name == "test-run") {
434 metadata.examples = {"z3ed test-run --label stable",
435 "z3ed test-run --label gui"};
436 } else if (name == "test-status") {
437 metadata.requires_rom = false;
438 metadata.examples = {"z3ed test-status --format json"};
439 }
440 } else {
441 metadata.category = "misc";
442 metadata.description = "Miscellaneous command";
443 }
444
445 // Prefer handler-provided summary if present
446 if (!descriptor.summary.empty() &&
447 descriptor.summary != "Command summary not provided.") {
448 metadata.description = descriptor.summary;
449 }
450
451 // Keep TODO reference if supplied by handler
452 if (!descriptor.todo_reference.empty() &&
453 descriptor.todo_reference != "todo#unassigned") {
454 metadata.todo_reference = descriptor.todo_reference;
455 }
456
457 Register(std::move(handler), metadata);
458 }
459}
460
461resources::CommandHandler* CommandRegistry::Get(const std::string& name) const {
462 // Check direct name
463 auto it = handlers_.find(name);
464 if (it != handlers_.end()) {
465 return it->second.get();
466 }
467
468 // Check aliases
469 auto alias_it = aliases_.find(name);
470 if (alias_it != aliases_.end()) {
471 auto handler_it = handlers_.find(alias_it->second);
472 if (handler_it != handlers_.end()) {
473 return handler_it->second.get();
474 }
475 }
476
477 return nullptr;
478}
479
481 const std::string& name) const {
482 // Resolve alias first
483 std::string canonical_name = name;
484 auto alias_it = aliases_.find(name);
485 if (alias_it != aliases_.end()) {
486 canonical_name = alias_it->second;
487 }
488
489 auto it = metadata_.find(canonical_name);
490 return (it != metadata_.end()) ? &it->second : nullptr;
491}
492
494 const std::string& category) const {
495 std::vector<std::string> result;
496 for (const auto& [name, metadata] : metadata_) {
497 if (metadata.category == category) {
498 result.push_back(name);
499 }
500 }
501 return result;
502}
503
504std::vector<std::string> CommandRegistry::GetCategories() const {
505 std::vector<std::string> categories;
506 for (const auto& [_, metadata] : metadata_) {
507 if (std::find(categories.begin(), categories.end(), metadata.category) ==
508 categories.end()) {
509 categories.push_back(metadata.category);
510 }
511 }
512 return categories;
513}
514
515std::vector<std::string> CommandRegistry::GetAgentCommands() const {
516 std::vector<std::string> result;
517 for (const auto& [name, metadata] : metadata_) {
518 if (metadata.available_to_agent) {
519 result.push_back(name);
520 }
521 }
522 return result;
523}
524
526 std::ostringstream out;
527 out << "{\n \"commands\": [\n";
528
529 bool first = true;
530 for (const auto& [_, metadata] : metadata_) {
531 if (!first)
532 out << ",\n";
533 first = false;
534
535 out << " {\n";
536 out << " \"name\": \"" << EscapeJson(metadata.name) << "\",\n";
537 out << " \"category\": \"" << EscapeJson(metadata.category) << "\",\n";
538 out << " \"description\": \"" << EscapeJson(metadata.description)
539 << "\",\n";
540 out << " \"usage\": \"" << EscapeJson(metadata.usage) << "\",\n";
541 out << " \"available_to_agent\": "
542 << (metadata.available_to_agent ? "true" : "false") << ",\n";
543 out << " \"requires_rom\": "
544 << (metadata.requires_rom ? "true" : "false") << ",\n";
545 out << " \"requires_grpc\": "
546 << (metadata.requires_grpc ? "true" : "false") << ",\n";
547 if (!metadata.todo_reference.empty()) {
548 out << " \"todo_reference\": \""
549 << EscapeJson(metadata.todo_reference) << "\",\n";
550 } else {
551 out << " \"todo_reference\": \"\",\n";
552 }
553 out << " \"aliases\": ";
554 AppendStringArray(out, metadata.aliases);
555 out << ",\n";
556 out << " \"examples\": ";
557 AppendStringArray(out, metadata.examples);
558 out << "\n";
559 out << " }";
560 }
561
562 out << "\n ]\n}";
563 return out.str();
564}
565
566std::string CommandRegistry::GenerateHelp(const std::string& name) const {
567 auto* metadata = GetMetadata(name);
568 if (!metadata) {
569 return absl::StrFormat("Command '%s' not found", name);
570 }
571
572 std::ostringstream help;
573 help << "\n\033[1;36m" << metadata->name << "\033[0m - "
574 << metadata->description << "\n\n";
575 help << "\033[1;33mUsage:\033[0m\n";
576 help << " " << metadata->usage << "\n\n";
577
578 if (!metadata->examples.empty()) {
579 help << "\033[1;33mExamples:\033[0m\n";
580 for (const auto& example : metadata->examples) {
581 help << " " << example << "\n";
582 }
583 help << "\n";
584 }
585
586 if (metadata->requires_rom) {
587 help << "\033[1;33mRequires:\033[0m ROM file (--rom=<path>)\n";
588 }
589 if (metadata->requires_grpc) {
590 help << "\033[1;33mRequires:\033[0m YAZE running with gRPC enabled\n";
591 }
592
593 if (!metadata->aliases.empty()) {
594 help << "\n\033[1;33mAliases:\033[0m "
595 << absl::StrJoin(metadata->aliases, ", ") << "\n";
596 }
597
598 return help.str();
599}
600
602 const std::string& category) const {
603 auto commands = GetCommandsInCategory(category);
604 if (commands.empty()) {
605 return absl::StrFormat("No commands in category '%s'", category);
606 }
607
608 std::ostringstream help;
609 help << "\n\033[1;36m" << category << " commands:\033[0m\n\n";
610
611 for (const auto& cmd : commands) {
612 auto* metadata = GetMetadata(cmd);
613 if (metadata) {
614 help << " \033[1;33m" << cmd << "\033[0m\n";
615 help << " " << metadata->description << "\n";
616 if (!metadata->usage.empty()) {
617 help << " Usage: " << metadata->usage << "\n";
618 }
619 help << "\n";
620 }
621 }
622
623 return help.str();
624}
625
627 std::ostringstream help;
628 help << "\n\033[1;36mAll z3ed Commands:\033[0m\n\n";
629
630 auto categories = GetCategories();
631 for (const auto& category : categories) {
632 help << GenerateCategoryHelp(category);
633 }
634
635 return help.str();
636}
637
638absl::Status CommandRegistry::Execute(const std::string& name,
639 const std::vector<std::string>& args,
640 Rom* rom_context,
641 std::string* captured_output) {
642 auto* handler = Get(name);
643 if (!handler) {
644 return absl::NotFoundError(absl::StrFormat("Command '%s' not found", name));
645 }
646
647 const bool command_help_requested =
648 std::find(args.begin(), args.end(), "--help") != args.end() ||
649 std::find(args.begin(), args.end(), "-h") != args.end();
650 if (command_help_requested) {
651 const std::string help = GenerateHelp(name);
652 if (captured_output != nullptr) {
653 *captured_output = help;
654 } else {
655 std::cout << help << "\n";
656 }
657 return absl::OkStatus();
658 }
659
660 absl::Status status = handler->Run(args, rom_context, captured_output);
661
662 // If a command was invoked without its required arguments, surface full
663 // command help in addition to the normal parser error/usage line.
664 if (!status.ok() && status.code() == absl::StatusCode::kInvalidArgument &&
665 args.empty()) {
666 const std::string help = GenerateHelp(name);
667 if (captured_output != nullptr) {
668 if (!captured_output->empty()) {
669 captured_output->append("\n\n");
670 }
671 captured_output->append(help);
672 } else {
673 std::cout << help << "\n";
674 }
675 }
676
677 return status;
678}
679
680bool CommandRegistry::HasCommand(const std::string& name) const {
681 return Get(name) != nullptr;
682}
683
687
688} // namespace cli
689} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:49
Single source of truth for all z3ed commands.
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.