use blvm_sdk::composition::*;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "blvm-compose")]
#[command(about = "Compose Bitcoin nodes from modules", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(long, default_value = "./modules")]
modules_dir: PathBuf,
}
#[derive(Subcommand)]
enum Commands {
Compose {
#[arg(short, long)]
config: PathBuf,
},
Validate {
#[arg(short, long)]
config: PathBuf,
},
GenerateTemplate {
#[arg(short, long)]
output: Option<PathBuf>,
},
#[command(subcommand)]
Modules(ModuleCommands),
}
#[derive(Subcommand)]
enum ModuleCommands {
List,
Install {
source: String,
#[arg(short, long)]
version: Option<String>,
},
Update {
name: String,
version: String,
},
Remove {
name: String,
},
}
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let mut composer = NodeComposer::new(&cli.modules_dir);
match cli.command {
Some(Commands::Compose { config }) => {
println!("Composing node from configuration: {config:?}");
let composed = composer.compose_from_config(&config).await?;
println!("Successfully composed node: {}", composed.spec.name);
println!("Modules: {}", composed.modules.len());
for module in &composed.modules {
println!(
" - {} ({}): {:?}",
module.info.name, module.info.version, module.status
);
}
Ok(())
}
Some(Commands::Validate { config }) => {
println!("Validating configuration: {config:?}");
let node_config = NodeConfig::from_file(&config)?;
let validation = composer.validate_composition(&node_config.to_spec()?)?;
if validation.valid {
println!("✓ Configuration is valid");
if !validation.warnings.is_empty() {
println!("Warnings:");
for warning in &validation.warnings {
println!(" - {warning}");
}
}
Ok(())
} else {
println!("✗ Configuration is invalid:");
for error in &validation.errors {
println!(" - {error}");
}
std::process::exit(1)
}
}
Some(Commands::GenerateTemplate { output }) => {
let template = composer.generate_config();
if let Some(path) = output {
std::fs::write(&path, template)?;
println!("Template written to: {path:?}");
} else {
print!("{template}");
}
Ok(())
}
Some(Commands::Modules(ModuleCommands::List)) => {
composer.registry_mut().discover_modules()?;
let modules = composer.registry().list_modules();
if modules.is_empty() {
println!("No modules found in {:?}", cli.modules_dir);
} else {
println!("Available modules:");
for module in modules {
println!(" - {} ({})", module.name, module.version);
if let Some(desc) = &module.description {
println!(" {desc}");
}
}
}
Ok(())
}
Some(Commands::Modules(ModuleCommands::Install { source, version: _ })) => {
let module_source = if source.starts_with("http://") || source.starts_with("https://") {
let (url, name) = if let Some(pos) = source.find("#") {
(
source[..pos].to_string(),
Some(source[pos + 1..].to_string()),
)
} else {
(source.clone(), None)
};
ModuleSource::Registry { url, name }
} else if source.starts_with("git+") || source.contains("github.com") {
ModuleSource::Git {
url: source,
tag: None,
}
} else {
ModuleSource::Path(PathBuf::from(source))
};
println!("Installing module from: {module_source:?}");
let module = composer.registry_mut().install_module(module_source)?;
println!(
"Successfully installed: {} ({})",
module.name, module.version
);
Ok(())
}
Some(Commands::Modules(ModuleCommands::Update { name, version })) => {
println!("Updating module {name} to version {version}");
let module = composer
.registry_mut()
.update_module(&name, Some(version.as_str()))?;
println!("Successfully updated: {} ({})", module.name, module.version);
Ok(())
}
Some(Commands::Modules(ModuleCommands::Remove { name })) => {
println!("Removing module: {name}");
composer.registry_mut().remove_module(&name)?;
println!("Successfully removed: {name}");
Ok(())
}
None => {
println!("No command specified. Use --help for usage.");
Ok(())
}
}
}