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