thenodes 0.4.0

TheNodes is a modular, plugin-driven P2P node framework for Rust, supporting node-embedded plugins (NEP) and core-as-a-library (CAL) modes with async-first APIs.
Documentation
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)
}