deargui-vpl/docs/cli-implementation-example.md

11 KiB

CLI Implementation Example (Headless Mode)

This document shows a concrete implementation of Option 1: Headless Flag from cli.md.

Step 1: Update CLI Argument Parser

Add headless mode and CLI command options to entry_point.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:

// blueprints-cli.h
#pragma once
#include "application.h"
#include <string>

// 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:

// blueprints-cli.cpp
#include "blueprints-cli.h"
#include "core/graph_state.h"
#include <cstdio>

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<std::string, int> 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 <graph.json> --command <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:

// blueprints-example.cpp
#include "app.h"
#include "blueprints-cli.h"
#include <map>
#include <cstdio>

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:

# 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

./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

./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

./build/bin/blueprints-example-console_d.exe --headless --file graph.json --command export --output converted.json --format json

Batch validation

# 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

./build/bin/blueprints-example_d.exe --file graph.json
# Should open normally

Test CLI mode

./build/bin/blueprints-example-console_d.exe --headless --file graph.json --command validate
# Should NOT create any windows

Test without file argument

./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

Integration with CI/CD

Example GitHub Actions workflow:

# .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