mecha10-cli 0.6.3

Mecha10 CLI tool
Documentation
//! AI model availability checks before spawning nodes
//!
//! Used in standalone mode (where node-runner is not available) to make
//! sure any AI models a node's config declares are downloaded before the
//! node is spawned.

use crate::services::ModelService;
use anyhow::Result;
use serde::Deserialize;
use std::path::PathBuf;

/// Minimal config structure to extract model requirements from node configs
#[derive(Deserialize, Default)]
struct NodeModelConfig {
    model: Option<ModelInfo>,
}

#[derive(Deserialize)]
struct ModelInfo {
    name: Option<String>,
}

/// Environment-aware node config wrapper
#[derive(Deserialize)]
struct EnvironmentNodeConfig {
    dev: Option<NodeModelConfig>,
    production: Option<NodeModelConfig>,
}

/// Parse node config from environment-aware format
fn parse_node_config(content: &str) -> Option<NodeModelConfig> {
    // Get environment from env var or default to dev
    let profile = std::env::var("MECHA10_ENVIRONMENT").unwrap_or_else(|_| "dev".to_string());

    // Try to parse as environment-aware config first
    if let Ok(env_config) = serde_json::from_str::<EnvironmentNodeConfig>(content) {
        let config = match profile.as_str() {
            "production" | "prod" => env_config.production,
            _ => env_config.dev,
        };
        return config;
    }

    // Fall back to direct parsing for backwards compatibility
    serde_json::from_str(content).ok()
}

/// Ensure all required AI models are available for nodes
///
/// Reads node configs and downloads missing models before spawning nodes.
/// This is called in standalone mode where node-runner is not available.
pub(super) async fn ensure_node_models_available(node_names: &[String]) -> Result<()> {
    let model_service = match ModelService::with_models_dir("models".into()) {
        Ok(service) => service,
        Err(e) => {
            eprintln!("⚠️  Warning: Could not initialize model service: {}", e);
            return Ok(());
        }
    };

    for node_name in node_names {
        // Strip scope prefix if present (@mecha10/node-name -> node-name)
        let short_name = node_name.strip_prefix("@mecha10/").unwrap_or(node_name);

        // Try to load node config to find model requirements
        // Check config locations: @mecha10 for framework, plain name for project nodes - see
        // `paths::config::node_config_candidates` for the canonical tier order.
        let config_paths = crate::paths::config::node_config_candidates(short_name);

        for config_path in &config_paths {
            let path = PathBuf::from(config_path);
            if !path.exists() {
                continue;
            }

            // Read and parse config
            let content = match std::fs::read_to_string(&path) {
                Ok(c) => c,
                Err(_) => continue,
            };

            // Parse environment-aware config
            let config = match parse_node_config(&content) {
                Some(c) => c,
                None => continue,
            };

            // Check if model is required
            if let Some(model_info) = config.model {
                if let Some(model_name) = model_info.name {
                    let model_path = PathBuf::from(format!("models/{}/model.onnx", model_name));
                    if !model_path.exists() {
                        println!("📦 Downloading model '{}' for node '{}'...", model_name, node_name);
                        match model_service.pull(&model_name, None).await {
                            Ok(_) => println!("✅ Model '{}' ready", model_name),
                            Err(e) => {
                                eprintln!("⚠️  Warning: Failed to download model '{}': {}", model_name, e);
                                eprintln!("   You can manually download with: mecha10 model pull {}", model_name);
                            }
                        }
                    }
                }
            }
            break; // Found and processed config, stop looking
        }
    }

    Ok(())
}