572 lines
15 KiB
Markdown
572 lines
15 KiB
Markdown
# 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<GraphEngine> engine);
|
|
|
|
bool Create();
|
|
int Run();
|
|
|
|
private:
|
|
std::unique_ptr<GraphEngine> m_Engine;
|
|
Application m_Application;
|
|
// UI renders m_Engine->GetState()
|
|
};
|
|
|
|
// cli/blueprints_cli.h
|
|
class BlueprintsCLI {
|
|
public:
|
|
BlueprintsCLI(std::unique_ptr<GraphEngine> engine, const ArgsMap& args);
|
|
|
|
int Execute();
|
|
|
|
private:
|
|
std::unique_ptr<GraphEngine> 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<GraphEngine>(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<Command> 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<Command> cmd;
|
|
|
|
if (command == "validate") {
|
|
cmd = std::make_unique<ValidateCommand>();
|
|
} else if (command == "export") {
|
|
std::string output = GetStringArg(args, "output", "");
|
|
std::string format = GetStringArg(args, "format", "json");
|
|
cmd = std::make_unique<ExportCommand>(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<br>Headless Flag | Option 2<br>Separate Exe | Option 3<br>Base Class | Option 4<br>Composition | Option 5<br>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)
|
|
|