yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
cli_main.cc
Go to the documentation of this file.
1#include <cstdlib>
2#include <iostream>
3#include <optional>
4#include <string>
5#include <string_view>
6#include <vector>
7
8#include "absl/flags/declare.h"
9#include "absl/flags/flag.h"
10#include "absl/strings/match.h"
11#include "absl/strings/str_format.h"
12#include "rom/rom.h"
13#include "cli/cli.h"
15#ifndef __EMSCRIPTEN__
16#include "cli/tui/tui.h"
17#endif
18#include "cli/z3ed_ascii_logo.h"
19#include "yaze_config.h"
20
21#ifdef YAZE_HTTP_API_ENABLED
23#include "util/log.h"
24#endif
25
26// Define all CLI flags
27ABSL_FLAG(bool, tui, false, "Launch interactive Text User Interface");
28ABSL_DECLARE_FLAG(bool, quiet);
29ABSL_FLAG(bool, version, false, "Show version information");
30ABSL_FLAG(bool, self_test, false, "Run self-test diagnostics to verify CLI functionality");
31#ifdef YAZE_HTTP_API_ENABLED
32ABSL_FLAG(int, http_port, 0,
33 "HTTP API server port (0 = disabled, default: 8080 when enabled)");
34ABSL_FLAG(std::string, http_host, "localhost",
35 "HTTP API server host (default: localhost)");
36#endif
37ABSL_DECLARE_FLAG(std::string, rom);
38ABSL_DECLARE_FLAG(std::string, ai_provider);
39ABSL_DECLARE_FLAG(std::string, ai_model);
40ABSL_DECLARE_FLAG(std::string, gemini_api_key);
41ABSL_DECLARE_FLAG(std::string, ollama_host);
42ABSL_DECLARE_FLAG(std::string, prompt_version);
43ABSL_DECLARE_FLAG(bool, use_function_calling);
44
45namespace {
46
48 std::cout << yaze::cli::GetColoredLogo() << "\n";
49 std::cout << absl::StrFormat(" Version %d.%d.%d\n", YAZE_VERSION_MAJOR,
50 YAZE_VERSION_MINOR, YAZE_VERSION_PATCH);
51 std::cout << " Yet Another Zelda3 Editor - Command Line Interface\n";
52 std::cout << " https://github.com/scawful/yaze\n\n";
53}
54
60 std::cout << "\n\033[1;36m=== z3ed Self-Test ===\033[0m\n\n";
61 int passed = 0;
62 int failed = 0;
63
64 auto run_test = [&](const char* name, bool condition) {
65 if (condition) {
66 std::cout << " \033[1;32m✓\033[0m " << name << "\n";
67 ++passed;
68 } else {
69 std::cout << " \033[1;31m✗\033[0m " << name << "\n";
70 ++failed;
71 }
72 };
73
74 // Test 1: Version info is available
75 run_test("Version info available",
76 YAZE_VERSION_MAJOR >= 0 && YAZE_VERSION_MINOR >= 0);
77
78 // Test 2: CLI instance can be created
79 {
80 bool cli_created = false;
81 try {
83 cli_created = true;
84 } catch (...) {
85 cli_created = false;
86 }
87 run_test("CLI instance creation", cli_created);
88 }
89
90 // Test 3: App context is accessible
91 run_test("App context accessible", true); // Always passes if we got here
92
93 // Test 4: ROM class can be instantiated
94 {
95 bool rom_ok = false;
96 try {
97 yaze::Rom test_rom;
98 rom_ok = true;
99 } catch (...) {
100 rom_ok = false;
101 }
102 run_test("ROM class instantiation", rom_ok);
103 }
104
105 // Test 5: Flag parsing works
106 run_test("Flag parsing functional",
107 absl::GetFlag(FLAGS_self_test) == true);
108
109#ifdef YAZE_HTTP_API_ENABLED
110 // Test 6: HTTP API available (if compiled in)
111 run_test("HTTP API compiled in", true);
112#else
113 run_test("HTTP API not compiled (expected)", true);
114#endif
115
116 // Summary
117 std::cout << "\n\033[1;36m=== Results ===\033[0m\n";
118 std::cout << " Passed: \033[1;32m" << passed << "\033[0m\n";
119 std::cout << " Failed: \033[1;31m" << failed << "\033[0m\n";
120
121 if (failed == 0) {
122 std::cout << "\n\033[1;32mAll self-tests passed!\033[0m\n\n";
123 return EXIT_SUCCESS;
124 } else {
125 std::cout << "\n\033[1;31mSome self-tests failed.\033[0m\n\n";
126 return EXIT_FAILURE;
127 }
128}
129
131 auto& registry = yaze::cli::CommandRegistry::Instance();
132 auto categories = registry.GetCategories();
133
134 std::cout << yaze::cli::GetColoredLogo() << "\n";
135 std::cout
136 << " \033[1;37mYet Another Zelda3 Editor - AI-Powered CLI\033[0m\n\n";
137
138 std::cout << "\033[1;36mUSAGE:\033[0m\n";
139 std::cout << " z3ed [command] [flags]\n";
140 std::cout << " z3ed --tui # Interactive TUI mode\n";
141 std::cout << " z3ed help <command|category> # Scoped help\n";
142 std::cout << " z3ed --export-schemas # JSON schemas for agents\n";
143 std::cout << " z3ed --version # Show version\n\n";
144
145 std::cout << "\033[1;36mCATEGORIES:\033[0m\n";
146 for (const auto& category : categories) {
147 auto commands = registry.GetCommandsInCategory(category);
148 std::cout << " \033[1;33m" << category << "\033[0m (" << commands.size()
149 << " commands)\n";
150 }
151 std::cout << "\n";
152
153 std::cout << "\033[1;36mCOMMON FLAGS:\033[0m\n";
154 std::cout << " --rom=<path> Path to ROM file\n";
155 std::cout << " --tui Launch interactive TUI\n";
156 std::cout << " --quiet, -q Suppress output\n";
157 std::cout << " --version Show version\n";
158 std::cout << " --self-test Run self-test diagnostics\n";
159 std::cout << " --help <scope> Show help for command or category\n";
160 std::cout << " --export-schemas Export command schemas as JSON\n";
161#ifdef YAZE_HTTP_API_ENABLED
162 std::cout << " --http-port=<port> HTTP API server port (0=disabled)\n";
163 std::cout
164 << " --http-host=<host> HTTP API server host (default: localhost)\n";
165#endif
166 std::cout << "\n";
167
168 std::cout << "\033[1;36mEXAMPLES:\033[0m\n";
169 std::cout << " z3ed agent test-conversation --rom=zelda3.sfc\n";
170 std::cout << " z3ed rom info --rom=zelda3.sfc\n";
171 std::cout << " z3ed agent message-search --rom=zelda3.sfc --query=\"Master "
172 "Sword\"\n";
173 std::cout << " z3ed dungeon export --rom=zelda3.sfc --id=1\n\n";
174
175 std::cout << "For detailed help: z3ed help <command>\n";
176 std::cout << "For all commands: z3ed --list-commands\n\n";
177}
178
180 std::vector<char*> positional;
181 bool show_help = false;
182 bool show_version = false;
183 bool list_commands = false;
184 bool self_test = false;
185 bool export_schemas = false;
186 std::optional<std::string> help_target;
187 std::optional<std::string> error;
188};
189
190ParsedGlobals ParseGlobalFlags(int argc, char* argv[]) {
191 ParsedGlobals result;
192 if (argc <= 0 || argv == nullptr) {
193 result.error = "Invalid argv provided";
194 return result;
195 }
196
197 result.positional.reserve(argc);
198 result.positional.push_back(argv[0]);
199
200 bool passthrough = false;
201 for (int i = 1; i < argc; ++i) {
202 char* current = argv[i];
203 std::string_view token(current);
204
205 if (!passthrough) {
206 if (token == "--") {
207 passthrough = true;
208 continue;
209 }
210
211 // Help flags
212 if (absl::StartsWith(token, "--help=")) {
213 std::string target(token.substr(7));
214 if (!target.empty()) {
215 result.help_target = target;
216 } else {
217 result.show_help = true;
218 }
219 continue;
220 }
221 if (token == "--help" || token == "-h") {
222 if (i + 1 < argc && argv[i + 1][0] != '-') {
223 result.help_target = std::string(argv[++i]);
224 } else {
225 result.show_help = true;
226 }
227 continue;
228 }
229
230 // Version flag
231 if (token == "--version" || token == "-v") {
232 result.show_version = true;
233 continue;
234 }
235
236 // List commands
237 if (token == "--list-commands" || token == "--list") {
238 result.list_commands = true;
239 continue;
240 }
241
242 // Schema export
243 if (token == "--export-schemas" || token == "--export_schemas") {
244 result.export_schemas = true;
245 continue;
246 }
247
248 // Self-test mode
249 if (token == "--self-test" || token == "--selftest") {
250 result.self_test = true;
251 continue;
252 }
253
254 // TUI mode
255 if (token == "--tui" || token == "--interactive") {
256 absl::SetFlag(&FLAGS_tui, true);
257 continue;
258 }
259
260 // Quiet mode
261 if (token == "--quiet" || token == "-q") {
262 absl::SetFlag(&FLAGS_quiet, true);
263 continue;
264 }
265 if (absl::StartsWith(token, "--quiet=")) {
266 std::string value(token.substr(8));
267 absl::SetFlag(&FLAGS_quiet, value == "true" || value == "1");
268 continue;
269 }
270
271 // ROM path
272 if (absl::StartsWith(token, "--rom=")) {
273 absl::SetFlag(&FLAGS_rom, std::string(token.substr(6)));
274 continue;
275 }
276 if (token == "--rom") {
277 if (i + 1 >= argc) {
278 result.error = "--rom flag requires a value";
279 return result;
280 }
281 absl::SetFlag(&FLAGS_rom, std::string(argv[++i]));
282 continue;
283 }
284
285 // AI provider flags
286 if (absl::StartsWith(token, "--ai_provider=") ||
287 absl::StartsWith(token, "--ai-provider=")) {
288 size_t eq_pos = token.find('=');
289 absl::SetFlag(&FLAGS_ai_provider,
290 std::string(token.substr(eq_pos + 1)));
291 continue;
292 }
293 if (token == "--ai_provider" || token == "--ai-provider") {
294 if (i + 1 >= argc) {
295 result.error = "--ai-provider flag requires a value";
296 return result;
297 }
298 absl::SetFlag(&FLAGS_ai_provider, std::string(argv[++i]));
299 continue;
300 }
301
302 if (absl::StartsWith(token, "--ai_model=") ||
303 absl::StartsWith(token, "--ai-model=")) {
304 size_t eq_pos = token.find('=');
305 absl::SetFlag(&FLAGS_ai_model, std::string(token.substr(eq_pos + 1)));
306 continue;
307 }
308 if (token == "--ai_model" || token == "--ai-model") {
309 if (i + 1 >= argc) {
310 result.error = "--ai-model flag requires a value";
311 return result;
312 }
313 absl::SetFlag(&FLAGS_ai_model, std::string(argv[++i]));
314 continue;
315 }
316
317 if (absl::StartsWith(token, "--gemini_api_key=") ||
318 absl::StartsWith(token, "--gemini-api-key=")) {
319 size_t eq_pos = token.find('=');
320 absl::SetFlag(&FLAGS_gemini_api_key,
321 std::string(token.substr(eq_pos + 1)));
322 continue;
323 }
324 if (token == "--gemini_api_key" || token == "--gemini-api-key") {
325 if (i + 1 >= argc) {
326 result.error = "--gemini-api-key flag requires a value";
327 return result;
328 }
329 absl::SetFlag(&FLAGS_gemini_api_key, std::string(argv[++i]));
330 continue;
331 }
332
333 if (absl::StartsWith(token, "--ollama_host=") ||
334 absl::StartsWith(token, "--ollama-host=")) {
335 size_t eq_pos = token.find('=');
336 absl::SetFlag(&FLAGS_ollama_host,
337 std::string(token.substr(eq_pos + 1)));
338 continue;
339 }
340 if (token == "--ollama_host" || token == "--ollama-host") {
341 if (i + 1 >= argc) {
342 result.error = "--ollama-host flag requires a value";
343 return result;
344 }
345 absl::SetFlag(&FLAGS_ollama_host, std::string(argv[++i]));
346 continue;
347 }
348
349 if (absl::StartsWith(token, "--prompt_version=") ||
350 absl::StartsWith(token, "--prompt-version=")) {
351 size_t eq_pos = token.find('=');
352 absl::SetFlag(&FLAGS_prompt_version,
353 std::string(token.substr(eq_pos + 1)));
354 continue;
355 }
356 if (token == "--prompt_version" || token == "--prompt-version") {
357 if (i + 1 >= argc) {
358 result.error = "--prompt-version flag requires a value";
359 return result;
360 }
361 absl::SetFlag(&FLAGS_prompt_version, std::string(argv[++i]));
362 continue;
363 }
364
365 if (absl::StartsWith(token, "--use_function_calling=") ||
366 absl::StartsWith(token, "--use-function-calling=")) {
367 size_t eq_pos = token.find('=');
368 std::string value(token.substr(eq_pos + 1));
369 absl::SetFlag(&FLAGS_use_function_calling,
370 value == "true" || value == "1");
371 continue;
372 }
373 if (token == "--use_function_calling" ||
374 token == "--use-function-calling") {
375 if (i + 1 >= argc) {
376 result.error = "--use-function-calling flag requires a value";
377 return result;
378 }
379 std::string value(argv[++i]);
380 absl::SetFlag(&FLAGS_use_function_calling,
381 value == "true" || value == "1");
382 continue;
383 }
384
385#ifdef YAZE_HTTP_API_ENABLED
386 // HTTP server flags
387 if (absl::StartsWith(token, "--http-port=") ||
388 absl::StartsWith(token, "--http_port=")) {
389 size_t eq_pos = token.find('=');
390 try {
391 int port = std::stoi(std::string(token.substr(eq_pos + 1)));
392 absl::SetFlag(&FLAGS_http_port, port);
393 } catch (...) {
394 result.error = "--http-port requires an integer value";
395 return result;
396 }
397 continue;
398 }
399 if (token == "--http-port" || token == "--http_port") {
400 if (i + 1 >= argc) {
401 result.error = "--http-port flag requires a value";
402 return result;
403 }
404 try {
405 int port = std::stoi(std::string(argv[++i]));
406 absl::SetFlag(&FLAGS_http_port, port);
407 } catch (...) {
408 result.error = "--http-port requires an integer value";
409 return result;
410 }
411 continue;
412 }
413
414 if (absl::StartsWith(token, "--http-host=") ||
415 absl::StartsWith(token, "--http_host=")) {
416 size_t eq_pos = token.find('=');
417 absl::SetFlag(&FLAGS_http_host, std::string(token.substr(eq_pos + 1)));
418 continue;
419 }
420 if (token == "--http-host" || token == "--http_host") {
421 if (i + 1 >= argc) {
422 result.error = "--http-host flag requires a value";
423 return result;
424 }
425 absl::SetFlag(&FLAGS_http_host, std::string(argv[++i]));
426 continue;
427 }
428#endif
429 }
430
431 result.positional.push_back(current);
432 }
433
434 return result;
435}
436
437} // namespace
438
439int main(int argc, char* argv[]) {
440 // Parse global flags
441 ParsedGlobals globals = ParseGlobalFlags(argc, argv);
442
443 if (globals.error.has_value()) {
444 std::cerr << "Error: " << *globals.error << "\n";
445 std::cerr << "Use --help for usage information.\n";
446 return EXIT_FAILURE;
447 }
448
449 // Handle version flag
450 if (globals.show_version) {
451 PrintVersion();
452 return EXIT_SUCCESS;
453 }
454
455 // Handle self-test flag
456 if (globals.self_test) {
457 absl::SetFlag(&FLAGS_self_test, true); // Ensure flag is set for test
458 return RunSelfTest();
459 }
460
461 auto& registry = yaze::cli::CommandRegistry::Instance();
462
463 if (globals.export_schemas) {
464 std::cout << registry.ExportFunctionSchemas() << "\n";
465 return EXIT_SUCCESS;
466 }
467
468#ifdef YAZE_HTTP_API_ENABLED
469 // Start HTTP API server if requested
470 std::unique_ptr<yaze::cli::api::HttpServer> http_server;
471 int http_port = absl::GetFlag(FLAGS_http_port);
472
473 if (http_port > 0) {
474 std::string http_host = absl::GetFlag(FLAGS_http_host);
475 http_server = std::make_unique<yaze::cli::api::HttpServer>();
476
477 auto status = http_server->Start(http_port);
478 if (!status.ok()) {
479 std::cerr
480 << "\n\033[1;31mWarning:\033[0m Failed to start HTTP API server: "
481 << status.message() << "\n";
482 std::cerr << "Continuing without HTTP API...\n\n";
483 http_server.reset();
484 } else if (!absl::GetFlag(FLAGS_quiet)) {
485 std::cout << "\033[1;32m✓\033[0m HTTP API server started on " << http_host
486 << ":" << http_port << "\n";
487 std::cout << " Health check: http://" << http_host << ":" << http_port
488 << "/api/v1/health\n";
489 std::cout << " Models list: http://" << http_host << ":" << http_port
490 << "/api/v1/models\n\n";
491 }
492 } else if (http_port == 0 && !absl::GetFlag(FLAGS_quiet)) {
493 // Port 0 means explicitly disabled, only show message in verbose mode
494 }
495#endif
496
497 // Handle TUI mode
498#ifndef __EMSCRIPTEN__
499 if (absl::GetFlag(FLAGS_tui)) {
500 // Load ROM if specified before launching TUI
501 std::string rom_path = absl::GetFlag(FLAGS_rom);
502 if (!rom_path.empty()) {
503 auto status = yaze::cli::app_context.rom.LoadFromFile(rom_path);
504 if (!status.ok()) {
505 std::cerr << "\n\033[1;31mError:\033[0m Failed to load ROM: "
506 << status.message() << "\n";
507 // Continue to TUI anyway, user can load ROM from there
508 }
509 }
511 return EXIT_SUCCESS;
512 }
513#else
514 if (absl::GetFlag(FLAGS_tui)) {
515 std::cerr << "TUI mode is not available in WASM builds.\n";
516 return EXIT_FAILURE;
517 }
518#endif
519
520 // Create CLI instance
522
523 // Handle targeted help (command or category)
524 if (globals.help_target.has_value()) {
525 const std::string& target = *globals.help_target;
526 if (target == "all") {
527 std::cout << registry.GenerateCompleteHelp() << "\n";
528 } else if (registry.HasCommand(target)) {
529 std::cout << registry.GenerateHelp(target) << "\n";
530 } else if (!registry.GetCommandsInCategory(target).empty()) {
531 cli.PrintCategoryHelp(target);
532 } else {
533 std::cerr << "\n\033[1;31mError:\033[0m Unknown command or category '"
534 << target << "'\n";
535 std::cerr << "Use --list-commands for a full command list.\n";
536 return EXIT_FAILURE;
537 }
538 return EXIT_SUCCESS;
539 }
540
541 // Handle list commands
542 if (globals.list_commands) {
544 return EXIT_SUCCESS;
545 }
546
547 // Handle general help or no arguments
548 if (globals.show_help || globals.positional.size() <= 1) {
550 return EXIT_SUCCESS;
551 }
552
553 // Run CLI commands
554 auto status = cli.Run(static_cast<int>(globals.positional.size()),
555 globals.positional.data());
556
557 if (!status.ok()) {
558 std::cerr << "\n\033[1;31mError:\033[0m " << status.message() << "\n";
559 std::cerr << "Use --help for usage information.\n";
560 return EXIT_FAILURE;
561 }
562
563 return EXIT_SUCCESS;
564}
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:74
static CommandRegistry & Instance()
absl::Status Run(int argc, char *argv[])
Definition cli.cc:27
void PrintCategoryHelp(const std::string &category) const
Definition cli.cc:267
void PrintCommandSummary() const
Definition cli.cc:271
int main(int argc, char *argv[])
Definition cli_main.cc:439
ABSL_FLAG(bool, tui, false, "Launch interactive Text User Interface")
ABSL_DECLARE_FLAG(bool, quiet)
int RunSelfTest()
Run self-test diagnostics to verify CLI functionality.
Definition cli_main.cc:59
ParsedGlobals ParseGlobalFlags(int argc, char *argv[])
Definition cli_main.cc:190
void ShowMain()
Definition tui.cc:970
std::string GetColoredLogo()
std::optional< std::string > help_target
Definition cli_main.cc:186