# CLI Architecture Options ## Current State Currently, both GUI and console variants create the full UI: - Console variant = GUI variant + visible console window - All ImGui/rendering initialization still happens - Window is created even if you just want to process files **Problem:** No true headless/CLI mode for automation, testing, or server environments. ## Goal Enable true CLI operation: - Process graphs without creating windows - Validate, convert, export without UI overhead - Reuse existing CLI argument parsing from `entry_point.cpp` - Keep GUI and CLI code maintainable ## Architecture Options ### Option 1: Headless Mode Flag ⭐ (Recommended for Quick Implementation) **Concept:** Add a `--headless` flag that skips all UI initialization. **Pros:** - Minimal code changes - Reuses all existing infrastructure - Single codebase for both modes - Easy to maintain **Cons:** - Application class still assumes UI might exist - Some refactoring needed to make UI optional - Not the cleanest separation **Implementation:** ```cpp // blueprints-example.cpp int Main(const ArgsMap& args) { // Check for headless mode bool headless = false; auto it = args.find("headless"); if (it != args.end() && it->second.Type == ArgValue::Type::Bool && it->second.Bool) { headless = true; } if (headless) { // CLI mode - no UI return RunCLI(args); } else { // GUI mode - existing code App example("Blueprints", args); if (example.Create()) return example.Run(); return 0; } } int RunCLI(const ArgsMap& args) { // Load graph std::string filename = GetStringArg(args, "file", ""); if (filename.empty()) { fprintf(stderr, "Error: --file required in headless mode\n"); return 1; } // Process without UI GraphState graph; if (!graph.Load(filename)) { fprintf(stderr, "Error: Failed to load %s\n", filename.c_str()); return 1; } // Perform operations std::string command = GetStringArg(args, "command", "validate"); if (command == "validate") { return ValidateGraph(graph); } else if (command == "export") { std::string output = GetStringArg(args, "output", ""); return ExportGraph(graph, output); } // ... more commands return 0; } ``` **Entry point update:** ```cpp // entry_point.cpp - add to CLI parser app.add_option("--headless", "Run without GUI")->capture_default_str(); app.add_option("--command", "Command to execute (validate, export, etc.)")->capture_default_str(); app.add_option("--output", "Output file path")->capture_default_str(); ``` **Usage:** ```bash # Validate graph blueprints-example-console.exe --headless --file graph.json --command validate # Export to different format blueprints-example-console.exe --headless --file graph.json --command export --output graph.xml # GUI mode (existing) blueprints-example.exe --file graph.json ``` --- ### Option 2: Separate CLI Executable **Concept:** Create `blueprints-cli` as a completely separate executable. **Pros:** - Clean separation - No UI dependencies in CLI build - Smaller executable for CLI - Clear intent (different binary = different purpose) **Cons:** - Code duplication risk - Two build targets to maintain - Shared code must be extracted to library **Implementation:** ```cmake # CMakeLists.txt add_library(blueprints-core STATIC core/graph_state.cpp blocks/block.cpp # ... all non-UI code ) # GUI application add_example_executable(blueprints-example blueprints-example.cpp app.cpp # ... UI files ) target_link_libraries(blueprints-example PRIVATE blueprints-core) # CLI application add_executable(blueprints-cli cli/blueprints-cli.cpp cli/commands.cpp ) target_link_libraries(blueprints-cli PRIVATE blueprints-core) # Note: No imgui, no application library ``` ```cpp // cli/blueprints-cli.cpp int main(int argc, char** argv) { CLI::App app{"Blueprints CLI"}; std::string file; std::string command; std::string output; app.add_option("-f,--file", file, "Input graph file")->required(); app.add_option("-c,--command", command, "Command")->required(); app.add_option("-o,--output", output, "Output file"); CLI11_PARSE(app, argc, argv); // Execute command return ExecuteCommand(command, file, output); } ``` **Usage:** ```bash blueprints-cli -f graph.json -c validate blueprints-cli -f graph.json -c export -o graph.xml blueprints-example --file graph.json # GUI ``` --- ### Option 3: Application Base Class Architecture ⭐⭐ (Recommended for Long-term) **Concept:** Separate core logic from presentation. **Pros:** - Clean architecture - Core testable without UI - Natural separation of concerns - Easy to add new frontends (Web, TUI, etc.) **Cons:** - Requires significant refactoring - More files to maintain - Need to identify what's "core" vs "UI" **Implementation:** ```cpp // core/blueprints_engine.h class BlueprintsEngine { public: BlueprintsEngine(); virtual ~BlueprintsEngine() = default; // Core operations (no UI) bool LoadGraph(const std::string& filename); bool SaveGraph(const std::string& filename); bool ValidateGraph(); bool ExecuteGraph(); std::string ExportGraph(const std::string& format); GraphState& GetGraph() { return m_Graph; } protected: GraphState m_Graph; // ... core state }; // app/blueprints_app.h class BlueprintsApp : public BlueprintsEngine { public: BlueprintsApp(const char* name, const ArgsMap& args); // UI operations bool Create(); int Run(); void OnFrame(float deltaTime); private: Application m_Application; ax::NodeEditor::EditorContext* m_Editor; // ... UI state // UI methods void RenderNodes(); void HandleInput(); }; // cli/blueprints_cli.h class BlueprintsCLI : public BlueprintsEngine { public: BlueprintsCLI(const ArgsMap& args); int Execute(); private: ArgsMap m_Args; int CommandValidate(); int CommandExport(); int CommandExecute(); }; ``` ```cpp // blueprints-example.cpp (GUI) int Main(const ArgsMap& args) { BlueprintsApp app("Blueprints", args); if (app.Create()) return app.Run(); return 0; } // blueprints-cli.cpp (CLI) int Main(const ArgsMap& args) { BlueprintsCLI cli(args); return cli.Execute(); } ``` **Usage:** ```bash # Same executable, mode determined by arguments blueprints-example-console.exe --headless --file graph.json --command validate blueprints-example.exe --file graph.json # GUI mode ``` --- ### Option 4: Composition Pattern **Concept:** Core engine is a separate component, Application "has-a" engine. **Pros:** - Very clean separation - Engine easily testable - Can swap out UI completely - Multiple UIs can share same engine **Cons:** - More indirection - Need to manage engine lifetime - Communication between engine and UI **Implementation:** ```cpp // core/graph_engine.h class GraphEngine { public: struct Config { std::string filename; bool autoSave = false; // ... options }; GraphEngine(const Config& config); bool Load(); bool Save(); bool Validate(); void Execute(); // Query state const GraphState& GetState() const { return m_State; } // Modify state void AddNode(const NodeConfig& config); void RemoveNode(NodeId id); void AddLink(PinId from, PinId to); private: Config m_Config; GraphState m_State; }; // app/blueprints_app.h class BlueprintsApp { public: BlueprintsApp(std::unique_ptr engine); bool Create(); int Run(); private: std::unique_ptr m_Engine; Application m_Application; // UI renders m_Engine->GetState() }; // cli/blueprints_cli.h class BlueprintsCLI { public: BlueprintsCLI(std::unique_ptr engine, const ArgsMap& args); int Execute(); private: std::unique_ptr m_Engine; ArgsMap m_Args; }; ``` ```cpp // blueprints-example.cpp int Main(const ArgsMap& args) { bool headless = GetBoolArg(args, "headless", false); GraphEngine::Config config; config.filename = GetStringArg(args, "file", ""); auto engine = std::make_unique(config); if (headless) { BlueprintsCLI cli(std::move(engine), args); return cli.Execute(); } else { BlueprintsApp app(std::move(engine)); if (app.Create()) return app.Run(); return 0; } } ``` --- ### Option 5: Command Pattern **Concept:** CLI commands are objects, both GUI and CLI execute commands. **Pros:** - Undo/redo support naturally - Commands are testable - GUI and CLI share exact same logic - Easy to add scripting **Cons:** - Everything must be a command - More abstraction overhead - Complex for simple operations **Implementation:** ```cpp // commands/command.h class Command { public: virtual ~Command() = default; virtual bool Execute(GraphState& state) = 0; virtual std::string GetName() const = 0; }; // commands/validate_command.h class ValidateCommand : public Command { public: bool Execute(GraphState& state) override { // Validation logic return ValidateGraphImpl(state); } std::string GetName() const override { return "validate"; } }; // commands/export_command.h class ExportCommand : public Command { public: ExportCommand(const std::string& outputPath, const std::string& format) : m_OutputPath(outputPath), m_Format(format) {} bool Execute(GraphState& state) override { return ExportGraphImpl(state, m_OutputPath, m_Format); } std::string GetName() const override { return "export"; } private: std::string m_OutputPath; std::string m_Format; }; // cli/command_runner.h class CommandRunner { public: CommandRunner(GraphState& state) : m_State(state) {} bool Run(std::unique_ptr cmd) { printf("Executing: %s\n", cmd->GetName().c_str()); return cmd->Execute(m_State); } private: GraphState& m_State; }; ``` ```cpp // blueprints-cli.cpp int Main(const ArgsMap& args) { bool headless = GetBoolArg(args, "headless", false); std::string filename = GetStringArg(args, "file", ""); GraphState state; if (!state.Load(filename)) { fprintf(stderr, "Failed to load graph\n"); return 1; } if (headless) { std::string command = GetStringArg(args, "command", "validate"); CommandRunner runner(state); std::unique_ptr cmd; if (command == "validate") { cmd = std::make_unique(); } else if (command == "export") { std::string output = GetStringArg(args, "output", ""); std::string format = GetStringArg(args, "format", "json"); cmd = std::make_unique(output, format); } return runner.Run(std::move(cmd)) ? 0 : 1; } else { // GUI mode - commands triggered by UI actions BlueprintsApp app(state); return app.Run(); } } ``` --- ## Comparison Matrix | Criteria | Option 1
Headless Flag | Option 2
Separate Exe | Option 3
Base Class | Option 4
Composition | Option 5
Commands | |----------|----------|----------|----------|----------|----------| | **Implementation Effort** | ⭐⭐⭐⭐⭐ Easy | ⭐⭐⭐ Moderate | ⭐⭐ Hard | ⭐⭐ Hard | ⭐ Very Hard | | **Maintainability** | ⭐⭐⭐ Good | ⭐⭐⭐ Good | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐ Good | | **Code Reuse** | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐ Good | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent | | **Testing** | ⭐⭐ Fair | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent | | **Clean Separation** | ⭐⭐ Fair | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐ Great | | **Binary Size (CLI)** | Large (full UI) | ⭐⭐⭐⭐⭐ Small | Medium | Medium | Medium | | **Flexibility** | ⭐⭐⭐ Good | ⭐⭐⭐ Good | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent | ## Recommended Approach ### Phase 1: Quick Win (Option 1) Start with **Headless Flag** approach: - Immediate CLI functionality - Minimal code changes - Validates the CLI use cases ### Phase 2: Refactor (Option 3 or 4) Once CLI use cases are clear: - Extract core logic - Either use **Base Class** (if inheritance makes sense) or **Composition** (if more flexibility needed) - Better testing and separation ### Phase 3: Advanced (Optional) If you need undo/redo, scripting, or complex workflows: - Add **Command Pattern** on top of Phase 2 ## CLI Argument Reuse All options can reuse `entry_point.cpp` CLI parsing: ```cpp // entry_point.cpp - add CLI options app.add_flag("--headless", "Run without GUI"); app.add_option("--command", "Command to execute")->capture_default_str(); app.add_option("--output", "Output file path")->capture_default_str(); app.add_option("--format", "Output format")->capture_default_str(); app.add_flag("--validate", "Validate graph and exit"); app.add_flag("--execute", "Execute graph and exit"); ``` The `ArgsMap` is already passed to `Main()`, so all these arguments are available. ## Example CLI Commands ```bash # Validate blueprints-console.exe --headless --file graph.json --command validate # Execute headless blueprints-console.exe --headless --file graph.json --command execute # Export to XML blueprints-console.exe --headless --file graph.json --command export --output graph.xml --format xml # Convert format blueprints-console.exe --headless --file graph.json --command convert --output graph.yaml # Batch processing for file in *.json; do blueprints-console.exe --headless --file "$file" --command validate done ``` ## Next Steps 1. **Decision:** Choose starting approach (recommend Option 1 for quick start) 2. **Identify:** List specific CLI operations needed (validate, export, execute, convert, etc.) 3. **Implement:** Add headless flag and CLI operations 4. **Test:** Verify CLI mode works without creating windows 5. **Refactor:** If needed, move to Option 3 or 4 for better architecture ## See Also - [Console Variants Guide](console-variants.md) - [CMake Options](cmake-options.md) - [Debugging Guide](../DEBUGGING.md)