11 KiB
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
- Implement actual validation - Add real graph validation logic
- Add more commands - Convert, merge, diff, etc.
- Add tests - Unit tests for CLI commands
- 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
- CLI Architecture Options - Full architectural discussion
- Console Variants Guide - Console vs GUI variants
- Debugging Guide - How to debug both modes