use clap::Parser;
use std::fs;
use std::sync::Arc;
use tokio::signal;
use tokio::sync::Mutex as TokioMutex;
// Import TheNodes framework
use thenodes::network::{
connect_to_bootstrap_nodes, peer_manager::PeerManager, start_listener, PeerStore,
};
use thenodes::plugin_host::{PluginContext, PluginLoader, PluginManager, PluginRegistrar};
use thenodes::prelude::*;
use thenodes::prompt::run_prompt_mode;
mod app_commands;
mod custom_plugin_host;
use custom_plugin_host::CustomPluginHost;
#[derive(Parser, Debug)]
#[command(author, version, about = env!("CARGO_PKG_DESCRIPTION"))]
struct Args {
/// Path to configuration file (TOML)
#[arg(short, long, default_value = "config.toml")]
config: String,
/// Enable interactive prompt mode
#[arg(long)]
prompt: bool,
/// Enable verbose logging
#[arg(short, long)]
verbose: bool,
/// Plugin directory override
#[arg(long, default_value = "plugins")]
plugin_dir: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
// Initialize logging
if args.verbose {
std::env::set_var("RUST_LOG", "debug");
}
env_logger::init();
// Load configuration
let config = load_config(&args.config)?;
log::info!("Starting app with config: {:?}", config.app_name);
// Initialize TheNodes components
thenodes::events::init_events_from_config(config.logging.as_ref()).await;
let realm = config.realm.clone().unwrap_or_default();
let node_id = config
.node
.as_ref()
.map(|node| node.resolve_node_id())
.unwrap_or_else(|| "unknown-node".to_string());
let peer_store = PeerStore::from_config(&config).await;
let peer_manager = Arc::new(PeerManager::new());
peer_manager.set_delivery_config(
config
.network
.as_ref()
.and_then(|network| network.delivery.clone()),
);
let plugin_context = PluginContext::new(
peer_manager.clone(),
peer_store.clone(),
thenodes::events::dispatcher::handle(),
node_id.clone(),
config.clone(),
!args.prompt,
);
let mut plugin_manager = PluginManager::with_context(plugin_context);
// Initialize custom plugin host
let custom_host = CustomPluginHost::new(config.clone());
plugin_manager.register_handler(Box::new(custom_host));
// Load TheNodes plugins from the standard plugin directory
if std::path::Path::new(&args.plugin_dir).exists() {
log::info!("🔌 Loading plugins from: {}", args.plugin_dir);
let mut plugin_loader = PluginLoader::new();
if let Err(error) = plugin_loader.load_plugins(&args.plugin_dir, &mut plugin_manager) {
log::error!("❌ Failed to load plugins: {}", error);
}
} else {
log::warn!("⚠️ Plugin directory '{}' does not exist", args.plugin_dir);
}
// Start TheNodes networking
let plugin_manager_arc = Arc::new(plugin_manager);
if let Some(context) = &plugin_manager_arc.context {
context.set_plugin_manager(plugin_manager_arc.clone()).await;
}
peer_manager
.configure_connection_lifecycle(
config.clone(),
node_id.clone(),
&plugin_manager_arc,
peer_store.clone(),
!args.prompt,
)
.await;
let listener_handle = tokio::spawn({
let peer_manager = peer_manager.clone();
let plugin_manager = plugin_manager_arc.clone();
let config = config.clone();
let realm = realm.clone();
let node_id = node_id.clone();
let peer_store = peer_store.clone();
let emit_console_errors = !args.prompt;
async move {
if let Err(e) = start_listener(
config.port,
realm,
(*peer_manager).clone(),
plugin_manager,
&config,
node_id,
peer_store,
emit_console_errors,
)
.await
{
log::error!("Listener failed: {}", e);
}
}
});
// Connect to bootstrap nodes
let error_buffer = Arc::new(TokioMutex::new(Vec::new()));
connect_to_bootstrap_nodes(
&config,
realm,
(*peer_manager).clone(),
plugin_manager_arc.clone(),
error_buffer,
!args.prompt,
node_id,
peer_store,
)
.await;
log::info!("🚀 {{APP_NAME}} is running. Press Ctrl+C to shutdown...");
// Handle prompt mode or wait for shutdown
if args.prompt {
log::info!("🎮 Starting interactive prompt mode");
run_prompt_mode(plugin_manager_arc.clone(), config.clone()).await;
} else {
// Wait for shutdown signal
signal::ctrl_c().await?;
}
log::info!("🛑 Shutting down {{APP_NAME}}...");
// Cleanup
listener_handle.abort();
log::info!("✅ {{APP_NAME}} shutdown complete");
Ok(())
}
fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)
.map_err(|e| format!("Failed to read config file '{}': {}", path, e))?;
let config: Config = toml::from_str(&content)
.map_err(|e| format!("Failed to parse config file '{}': {}", path, e))?;
Ok(config)
}