CLI Command Implementation Patterns
This guide provides patterns and best practices for implementing new CLI commands in Radium.
Command Structure Templateβ
Here's a template for implementing a new command:
//! Command name implementation.
//!
//! Brief description of what the command does.
use anyhow::{Context, Result};
use colored::Colorize;
use radium_core::Workspace;
// ... other imports
/// Execute the command.
///
/// Detailed description of command behavior.
pub async fn execute(
// Required arguments
arg1: Type1,
arg2: Option<Type2>,
// Flags
json: bool,
verbose: bool,
) -> anyhow::Result<()> {
// Early return for JSON output
if json {
return execute_json(arg1, arg2).await;
}
// Human-readable output
println!("{}", "Command Name".bold().cyan());
println!();
// Workspace discovery (if needed)
let workspace = Workspace::discover()
.context("Failed to discover workspace")?;
// Command logic here
// ...
Ok(())
}
/// Execute command with JSON output.
async fn execute_json(
arg1: Type1,
arg2: Option<Type2>,
) -> anyhow::Result<()> {
use serde_json::json;
// Build JSON response
let output = json!({
"status": "success",
"data": { /* ... */ }
});
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
Adding a Command to main.rsβ
- Add command variant to the
Commandenum:
#[derive(Subcommand, Debug)]
enum Command {
// ... existing commands
MyCommand {
/// Argument description
arg: String,
/// Flag description
#[arg(long)]
json: bool,
},
}
- Add routing in the match statement:
match command {
// ... existing matches
Command::MyCommand { arg, json } => {
my_command::execute(arg, json).await?;
}
}
- Import the module at the top:
use commands::{
// ... existing imports
my_command,
};
- Register module in
apps/cli/src/commands/mod.rs:
pub mod my_command;
Common Patternsβ
Workspace Discoveryβ
Always use Workspace::discover() with context:
let workspace = Workspace::discover()
.context("Failed to discover workspace")?;
If the command can work without a workspace, handle the error gracefully:
let workspace = match Workspace::discover() {
Ok(w) => Some(w),
Err(_) => {
if !allow_no_workspace {
anyhow::bail!("Workspace required. Run 'rad init' first.");
}
None
}
};
Model Selectionβ
For commands that need AI models:
use radium_models::ModelSelector;
let selector = ModelSelector::new()
.with_override(engine_override)
.with_model_override(model_override);
let model = selector.select().await
.context("Failed to select model")?;
Output Formattingβ
Human-readable output should be:
- Colorful and informative
- Use consistent symbols (β for success, β for errors)
- Group related information
- Provide actionable feedback
println!("{}", "Section Header".bold().cyan());
println!(" {} Item 1", "β".green());
println!(" {} Item 2: {}", "β’".dimmed(), value.cyan());
JSON output should be:
- Structured and consistent
- Include all relevant data
- Use proper types (strings, numbers, booleans, arrays, objects)
let output = json!({
"status": "success",
"count": items.len(),
"items": items.iter().map(|i| json!({
"id": i.id,
"name": i.name,
})).collect::<Vec<_>>(),
});
Error Handlingβ
Use anyhow::Context for error propagation:
let file = fs::read_to_string(&path)
.context(format!("Failed to read file: {}", path.display()))?;
Provide actionable error messages:
if !workspace.exists() {
anyhow::bail!(
"Workspace not found. Run 'rad init' to create one."
);
}
Input Validationβ
Validate user input early:
if arg.is_empty() {
anyhow::bail!("Argument cannot be empty");
}
if !path.exists() {
anyhow::bail!("Path does not exist: {}", path.display());
}
For file paths, prevent path traversal:
use std::path::Path;
fn validate_path(path: &Path, base: &Path) -> anyhow::Result<()> {
let canonical = path.canonicalize()
.context("Failed to canonicalize path")?;
let base_canonical = base.canonicalize()
.context("Failed to canonicalize base path")?;
if !canonical.starts_with(&base_canonical) {
anyhow::bail!("Path traversal detected");
}
Ok(())
}
Progress Indicationβ
For long-running operations, show progress:
println!("{}", "Processing...".bold());
for (i, item) in items.iter().enumerate() {
print!("\r [{}/{}] {}", i + 1, items.len(), item.name);
// ... process item
}
println!(); // New line after progress
Interactive Promptsβ
Use inquire for interactive prompts:
use inquire::{Confirm, Text, Select};
let confirm = Confirm::new("Proceed?")
.with_default(true)
.prompt()?;
let input = Text::new("Enter value:")
.with_default("default")
.prompt()?;
let choice = Select::new("Select option:", options)
.prompt()?;
Command Categoriesβ
Simple Commandsβ
Commands that just read and display information:
pub async fn execute(json: bool) -> anyhow::Result<()> {
let data = fetch_data().await?;
if json {
output_json(&data)?;
} else {
output_human(&data);
}
Ok(())
}
Commands with Side Effectsβ
Commands that modify state should:
- Validate inputs
- Show what will happen
- Confirm destructive operations
- Provide rollback if possible
pub async fn execute(path: PathBuf, force: bool) -> anyhow::Result<()> {
// Validate
if !path.exists() {
anyhow::bail!("Path does not exist");
}
// Warn about destructive operation
if !force {
let confirm = Confirm::new("This will delete data. Continue?")
.with_default(false)
.prompt()?;
if !confirm {
return Ok(());
}
}
// Execute
perform_operation(&path).await?;
println!("{} Operation completed", "β".green());
Ok(())
}
Commands with AI Integrationβ
Commands that use AI models should:
- Show model selection
- Display thinking/processing status
- Handle rate limits gracefully
- Show token usage
pub async fn execute(prompt: String, model: Option<String>) -> anyhow::Result<()> {
let selector = ModelSelector::new()
.with_model_override(model);
let model = selector.select().await?;
println!("{} Using model: {}", "β’".dimmed(), model.name().cyan());
println!("{} Processing...", "π€".yellow());
let response = model.generate(&prompt).await
.context("AI generation failed")?;
if let Some(usage) = &response.usage {
println!("{} Tokens: {}", "β’".dimmed(), usage.total_tokens);
}
println!("{}", response.content);
Ok(())
}
Testing Your Commandβ
See Testing Patterns for detailed testing guidelines.
Basic test structure:
#[test]
fn test_command_basic() {
let temp_dir = TempDir::new().unwrap();
init_workspace(&temp_dir);
let mut cmd = Command::cargo_bin("radium-cli").unwrap();
cmd.current_dir(temp_dir.path())
.arg("my-command")
.arg("arg-value")
.assert()
.success();
}
Best Practicesβ
- Always support
--jsonflag for scripting and CI/CD - Provide helpful error messages with context and suggestions
- Use consistent formatting across all commands
- Validate inputs early before performing operations
- Show progress for long-running operations
- Handle edge cases gracefully (missing workspace, empty results, etc.)
- Document command behavior in the doc comment
- Follow existing patterns for consistency
Common Pitfallsβ
- Forgetting workspace discovery - Most commands need a workspace
- Not handling errors gracefully - Use
context()for better error messages - Inconsistent output formatting - Follow the established patterns
- Missing JSON support - All commands should support
--json - Not validating inputs - Always validate before processing
- Blocking operations - Use async I/O for file and network operations