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.
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 runhorus mycommand) - Use
clapor 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:
| Variable | Value | Description |
|---|---|---|
HORUS_PLUGIN | 1 | Always set when running as a plugin |
HORUS_VERSION | e.g., 0.1.7 | Version 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:
- Project
plugins.lock—.horus/plugins.lockin the current project - Global
plugins.lock—~/.horus/plugins.lock - Project bin directory —
.horus/bin/horus-* - Global bin directory —
~/.horus/bin/horus-* - 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 verifyto 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 {
/// Shared memory directory
#[arg(long, default_value = "/dev/shm/horus")]
shm_dir: PathBuf,
/// Output as JSON
#[arg(long)]
json: bool,
}
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
Next Steps
- Publishing Plugins — Publish your plugin to the HORUS registry
- Managing Plugins — Install, enable/disable, and verify plugins