# CLI Implementation Example (Headless Mode) This document shows a concrete implementation of **Option 1: Headless Flag** from [cli.md](cli.md). ## Step 1: Update CLI Argument Parser Add headless mode and CLI command options to `entry_point.cpp`: ```cpp // entry_point.cpp - in CommandLineParser::Parse() CLI::App app{"NodeHub"}; app.allow_extras(); app.add_option("--file", "the source path")->capture_default_str(); // CLI mode options app.add_flag("--headless", "Run without GUI (CLI mode)"); app.add_option("--command", "Command to execute in headless mode (validate, export, execute)") ->capture_default_str() ->check(CLI::IsMember({"validate", "export", "execute", "info"})); app.add_option("--output", "Output file path for export commands")->capture_default_str(); app.add_option("--format", "Output format (json, xml, yaml)")->capture_default_str(); ``` ## Step 2: Add CLI Helper Functions Create `blueprints-cli.h`: ```cpp // blueprints-cli.h #pragma once #include "application.h" #include // Helper to extract string from ArgsMap inline std::string GetStringArg(const ArgsMap& args, const std::string& key, const std::string& defaultValue = "") { auto it = args.find(key); if (it != args.end() && it->second.Type == ArgValue::Type::String) return it->second.String; return defaultValue; } // Helper to extract bool from ArgsMap inline bool GetBoolArg(const ArgsMap& args, const std::string& key, bool defaultValue = false) { auto it = args.find(key); if (it != args.end() && it->second.Type == ArgValue::Type::Bool) return it->second.Bool; return defaultValue; } // CLI command execution int RunCLI(const ArgsMap& args); ``` Create `blueprints-cli.cpp`: ```cpp // blueprints-cli.cpp #include "blueprints-cli.h" #include "core/graph_state.h" #include static int CommandValidate(const std::string& filename) { printf("Validating: %s\n", filename.c_str()); GraphState graph; if (!graph.Load(filename)) { fprintf(stderr, "Error: Failed to load graph from %s\n", filename.c_str()); return 1; } // TODO: Add actual validation logic // For now, just check if it loaded printf("✓ Graph loaded successfully\n"); printf(" Nodes: %zu\n", graph.GetNodes().size()); printf(" Links: %zu\n", graph.GetLinks().size()); // Basic validation bool valid = true; // Check for disconnected pins for (const auto& link : graph.GetLinks()) { if (!graph.FindPin(link.StartPinID)) { fprintf(stderr, "Error: Link references missing start pin\n"); valid = false; } if (!graph.FindPin(link.EndPinID)) { fprintf(stderr, "Error: Link references missing end pin\n"); valid = false; } } if (valid) { printf("✓ Validation passed\n"); return 0; } else { fprintf(stderr, "✗ Validation failed\n"); return 1; } } static int CommandExport(const std::string& filename, const std::string& output, const std::string& format) { printf("Exporting: %s -> %s (%s)\n", filename.c_str(), output.c_str(), format.c_str()); GraphState graph; if (!graph.Load(filename)) { fprintf(stderr, "Error: Failed to load graph from %s\n", filename.c_str()); return 1; } // Export based on format if (format == "json") { if (graph.Save(output)) { printf("✓ Exported to JSON: %s\n", output.c_str()); return 0; } else { fprintf(stderr, "Error: Failed to export to %s\n", output.c_str()); return 1; } } else if (format == "xml" || format == "yaml") { fprintf(stderr, "Error: Format '%s' not yet implemented\n", format.c_str()); return 1; } else { fprintf(stderr, "Error: Unknown format '%s'\n", format.c_str()); return 1; } } static int CommandExecute(const std::string& filename) { printf("Executing: %s\n", filename.c_str()); GraphState graph; if (!graph.Load(filename)) { fprintf(stderr, "Error: Failed to load graph from %s\n", filename.c_str()); return 1; } // TODO: Implement graph execution fprintf(stderr, "Error: Graph execution not yet implemented\n"); return 1; } static int CommandInfo(const std::string& filename) { printf("Graph info: %s\n", filename.c_str()); GraphState graph; if (!graph.Load(filename)) { fprintf(stderr, "Error: Failed to load graph from %s\n", filename.c_str()); return 1; } printf("Nodes: %zu\n", graph.GetNodes().size()); printf("Links: %zu\n", graph.GetLinks().size()); // Node type breakdown std::map nodeTypes; for (const auto& node : graph.GetNodes()) { nodeTypes[node.Type]++; } printf("\nNode Types:\n"); for (const auto& [type, count] : nodeTypes) { printf(" %s: %d\n", type.c_str(), count); } return 0; } int RunCLI(const ArgsMap& args) { std::string filename = GetStringArg(args, "file", ""); if (filename.empty()) { fprintf(stderr, "Error: --file required in headless mode\n"); fprintf(stderr, "\nUsage:\n"); fprintf(stderr, " --headless --file --command [options]\n"); fprintf(stderr, "\nCommands:\n"); fprintf(stderr, " validate Validate graph structure\n"); fprintf(stderr, " export Export to different format\n"); fprintf(stderr, " execute Execute graph (headless)\n"); fprintf(stderr, " info Display graph information\n"); return 1; } std::string command = GetStringArg(args, "command", "validate"); if (command == "validate") { return CommandValidate(filename); } else if (command == "export") { std::string output = GetStringArg(args, "output", ""); if (output.empty()) { fprintf(stderr, "Error: --output required for export command\n"); return 1; } std::string format = GetStringArg(args, "format", "json"); return CommandExport(filename, output, format); } else if (command == "execute") { return CommandExecute(filename); } else if (command == "info") { return CommandInfo(filename); } else { fprintf(stderr, "Error: Unknown command '%s'\n", command.c_str()); return 1; } } ``` ## Step 3: Update blueprints-example.cpp Modify the main entry point to support headless mode: ```cpp // blueprints-example.cpp #include "app.h" #include "blueprints-cli.h" #include #include int Main(const ArgsMap& args) { // Check for headless mode bool headless = GetBoolArg(args, "headless", false); if (headless) { printf("[CLI MODE] Running in headless mode\n"); return RunCLI(args); } // GUI mode (existing code) printf("[GUI MODE] Starting application\n"); App example("Blueprints", args); if (example.Create()) return example.Run(); return 0; } ``` ## Step 4: Update CMakeLists.txt Add the new CLI files to the build: ```cmake # examples/blueprints-example/CMakeLists.txt add_example_executable(blueprints-example blueprints-example.cpp blueprints-cli.h blueprints-cli.cpp # Add this types.h nodes.h nodes.cpp # ... rest of files ) ``` ## Usage Examples Once implemented, you can use it like this: ### Validate a graph ```bash ./build/bin/blueprints-example-console_d.exe --headless --file graph.json --command validate ``` Output: ``` [CLI MODE] Running in headless mode Validating: graph.json ✓ Graph loaded successfully Nodes: 15 Links: 12 ✓ Validation passed ``` ### Get graph info ```bash ./build/bin/blueprints-example-console_d.exe --headless --file graph.json --command info ``` Output: ``` [CLI MODE] Running in headless mode Graph info: graph.json Nodes: 15 Links: 12 Node Types: Math: 5 Input: 3 Output: 2 Group: 5 ``` ### Export to different format ```bash ./build/bin/blueprints-example-console_d.exe --headless --file graph.json --command export --output converted.json --format json ``` ### Batch validation ```bash # PowerShell Get-ChildItem *.json | ForEach-Object { Write-Host "Processing $_" ./build/bin/blueprints-example-console_d.exe --headless --file $_.Name --command validate } # Bash for file in *.json; do echo "Processing $file" ./build/bin/blueprints-example-console_d.exe --headless --file "$file" --command validate done ``` ## Testing ### Test GUI mode still works ```bash ./build/bin/blueprints-example_d.exe --file graph.json # Should open normally ``` ### Test CLI mode ```bash ./build/bin/blueprints-example-console_d.exe --headless --file graph.json --command validate # Should NOT create any windows ``` ### Test without file argument ```bash ./build/bin/blueprints-example-console_d.exe --headless # Should show error and usage ``` ## Benefits ✅ **No UI overhead** - Headless mode doesn't create any windows or ImGui context ✅ **Fast** - Perfect for CI/CD pipelines ✅ **Automatable** - Can be scripted and batched ✅ **Same binary** - No need to maintain separate executables initially ✅ **Reuses CLI parsing** - All argument handling already in place ## Next Steps 1. **Implement actual validation** - Add real graph validation logic 2. **Add more commands** - Convert, merge, diff, etc. 3. **Add tests** - Unit tests for CLI commands 4. **Consider refactoring** - If CLI usage grows, consider Option 3 or 4 from [cli.md](cli.md) ## Integration with CI/CD Example GitHub Actions workflow: ```yaml # .github/workflows/validate-graphs.yml name: Validate Graphs on: [push, pull_request] jobs: validate: runs-on: windows-latest steps: - uses: actions/checkout@v2 - name: Build run: | cmake -S examples -B build cmake --build build --config Release --target blueprints-example-console - name: Validate all graphs run: | Get-ChildItem -Path test-graphs -Filter *.json | ForEach-Object { Write-Host "Validating $_" ./build/bin/blueprints-example-console.exe --headless --file $_.FullName --command validate if ($LASTEXITCODE -ne 0) { exit 1 } } ``` ## See Also - [CLI Architecture Options](cli.md) - Full architectural discussion - [Console Variants Guide](console-variants.md) - Console vs GUI variants - [Debugging Guide](../DEBUGGING.md) - How to debug both modes