Creating CLI Plugins

A CLI plugin is a standalone binary that adds a subcommand to horus. When a user runs horus mycommand, HORUS discovers your plugin binary and executes it, passing through all arguments.

Problem Statement

You need to extend the horus CLI with a custom command for your team or project -- for example, a simulator launcher, a hardware calibration tool, or a deployment helper.

When To Use

  • You want to add a new horus <subcommand> that integrates seamlessly with the CLI
  • You need to distribute a tool that other HORUS users can install from the registry
  • You have project-specific tooling that should feel native to the HORUS workflow

Prerequisites

  • Rust toolchain installed (rustup, cargo)
  • HORUS CLI installed (Installation guide)
  • Basic familiarity with Rust and clap for argument parsing

Zero-Config Convention

Any Rust package named horus-* with a [[bin]] target is automatically detected as a plugin. No extra configuration required.

Example: A package named horus-sim3d with [[bin]] name = "sim3d" automatically provides the horus sim3d command.

Step-by-Step Guide

Step 1: Create a New Rust Project

cargo new horus-mycommand
cd horus-mycommand

Step 2: Set Up Cargo.toml

[package]
name = "horus-mycommand"
version = "0.1.0"
edition = "2021"
description = "My custom HORUS plugin"

[[bin]]
name = "mycommand"
path = "src/main.rs"

[dependencies]
clap = { version = "4", features = ["derive"] }

The key points:

  • Package name starts with horus-
  • The [[bin]] name defines the subcommand (users will run horus mycommand)
  • Use clap or similar for argument parsing

Step 3: Implement the Plugin

// src/main.rs
use clap::Parser;

#[derive(Parser)]
#[command(name = "mycommand", about = "My custom HORUS command")]
struct Cli {
    /// Target to operate on
    #[arg(short, long)]
    target: Option<String>,

    /// Enable verbose output
    #[arg(short, long)]
    verbose: bool,
}

fn main() {
    let cli = Cli::parse();

    // Check if running as a HORUS plugin
    if std::env::var("HORUS_PLUGIN").is_ok() {
        let horus_version = std::env::var("HORUS_VERSION")
            .unwrap_or_else(|_| "unknown".to_string());
        if cli.verbose {
            eprintln!("Running as HORUS plugin (HORUS v{})", horus_version);
        }
    }

    // Your plugin logic here
    match cli.target {
        Some(target) => println!("Operating on: {}", target),
        None => println!("No target specified. Use --help for usage."),
    }
}

Step 4: Build and Test Locally

# Build the plugin
cargo build --release

# Test it standalone
./target/release/mycommand --help

# Test it as a HORUS plugin (simulating the environment)
HORUS_PLUGIN=1 HORUS_VERSION=0.1.0 ./target/release/mycommand --target foo

Step 5: Install Locally

To test with the actual horus CLI, install the binary where HORUS can find it:

# Option A: Copy to global plugin bin directory
mkdir -p ~/.horus/bin
cp target/release/mycommand ~/.horus/bin/horus-mycommand

# Option B: Install via horus from a local path
horus install --plugin horus-mycommand --local

Now horus mycommand --help should work.

Environment Variables

HORUS sets these environment variables when executing plugins:

VariableValueDescription
HORUS_PLUGIN1Always set when running as a plugin
HORUS_VERSIONe.g., 0.1.7Version of the HORUS CLI

Your plugin inherits the user's stdin, stdout, and stderr, so interactive prompts and colored output work normally.

Plugin Discovery

HORUS discovers plugin binaries in this order:

  1. Project plugins.lock.horus/plugins.lock in the current project
  2. Global plugins.lock~/.horus/plugins.lock
  3. Project bin directory.horus/bin/horus-*
  4. Global bin directory~/.horus/bin/horus-*
  5. System PATH — Any horus-* binary in $PATH

The first match wins. This means project-level plugins always override global ones.

Plugin Detection

HORUS discovers plugins by scanning for packages named horus-*. The discovery system reads your Cargo.toml [package] section (name, version, description) and auto-detects the plugin category from the name (e.g., horus-realsense → Camera, horus-rplidar → LiDAR, horus-sim3d → Simulation).

Security

When a plugin is installed through the registry, HORUS records a SHA-256 checksum of the binary. Before each execution, the checksum is verified:

  • If the binary has been modified, HORUS refuses to run it
  • Run horus verify to check all plugin integrity
  • Reinstall a plugin with horus install --plugin <name> if verification fails

Example: A Complete Plugin

Here is a minimal but complete plugin that queries HORUS topic statistics:

use clap::Parser;
use std::path::PathBuf;

#[derive(Parser)]
#[command(name = "topic-stats", about = "Show topic statistics summary")]
struct Cli {
    /// Output as JSON
    #[arg(long)]
    json: bool,
    /// Path to shared memory directory
    #[arg(long, default_value = "/dev/shm/horus")]
    shm_dir: PathBuf,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();

    if !cli.shm_dir.exists() {
        eprintln!("No HORUS topics found at {}", cli.shm_dir.display());
        std::process::exit(1);
    }

    let mut topics = Vec::new();
    for entry in std::fs::read_dir(&cli.shm_dir)? {
        let entry = entry?;
        if entry.file_type()?.is_file() {
            let name = entry.file_name().to_string_lossy().to_string();
            let size = entry.metadata()?.len();
            topics.push((name, size));
        }
    }

    if cli.json {
        println!("{}", serde_json::to_string_pretty(&topics)?);
    } else {
        println!("Found {} topics:", topics.len());
        for (name, size) in &topics {
            println!("  {} ({} bytes)", name, size);
        }
    }

    Ok(())
}

Cargo.toml:

[package]
name = "horus-topic-stats"
version = "0.1.0"
edition = "2021"
description = "Show HORUS topic statistics"

[[bin]]
name = "topic-stats"
path = "src/main.rs"

[dependencies]
clap = { version = "4", features = ["derive"] }
serde_json = "1"

After building and installing, users run:

horus topic-stats
horus topic-stats --json

Common Errors

"no such command: mycommand"

The plugin binary is not in a location HORUS searches. Ensure the binary is named horus-mycommand and is in ~/.horus/bin/, .horus/bin/, or your system PATH.

Plugin runs standalone but not via horus

Check that the [[bin]] name in Cargo.toml matches the subcommand you expect. The binary name mycommand maps to horus mycommand. Also verify the installed binary is named horus-mycommand (with the horus- prefix).

"Checksum mismatch" on execution

The plugin binary was modified after installation. Reinstall with horus install --plugin horus-mycommand to update the recorded checksum.


Next Steps


See Also