scorpiofs 0.2.2

FUSE-based virtual filesystem with Antares overlay for monorepo builds
Documentation
use std::{net::SocketAddr, path::PathBuf, sync::Arc};

use clap::{Parser, Subcommand};
use reqwest::blocking::Client;
use scorpiofs::{
    antares::{AntaresManager, AntaresPaths},
    daemon::antares::{AntaresDaemon, AntaresServiceImpl},
    util::config,
};

/// Antares build overlay manager (skeleton).
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Path to the configuration file (scorpio config).
    #[arg(long, default_value = "scorpio.toml")]
    config_path: String,
    /// Root path to place per-job upper layers (overrides config when set).
    #[arg(long)]
    upper_root: Option<PathBuf>,
    /// Root path to place per-job CL layers (overrides config when set).
    #[arg(long)]
    cl_root: Option<PathBuf>,
    /// Root path for per-job mountpoints (overrides config when set).
    #[arg(long)]
    mount_root: Option<PathBuf>,
    /// Path to persist mount state as TOML (overrides config when set).
    #[arg(long)]
    state_file: Option<PathBuf>,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// Mount a new Antares job instance.
    Mount {
        /// Unique job identifier.
        job_id: String,
        /// Optional CL layer name; when set, creates a CL passthrough layer placeholder.
        #[arg(long)]
        cl: Option<String>,
    },
    /// Unmount a job instance.
    Umount {
        /// Job identifier to remove.
        job_id: String,
    },
    /// List tracked instances.
    List,
    /// Start HTTP daemon server.
    Serve {
        /// Address to bind to (e.g., "0.0.0.0:2726")
        #[arg(long, default_value = "0.0.0.0:2726")]
        bind: String,
    },
    /// Mount via HTTP daemon (recommended for build systems to ensure unified behavior).
    HttpMount {
        /// Unique job identifier (recommended). If omitted, the daemon will create a UUID-based mount.
        #[arg(long)]
        job_id: Option<String>,
        /// Monorepo path to mount (e.g., "/third-party/mega")
        path: String,
        /// Optional CL identifier
        #[arg(long)]
        cl: Option<String>,
        /// Daemon base URL (e.g., "http://127.0.0.1:2726")
        #[arg(long, default_value = "http://127.0.0.1:2726")]
        endpoint: String,
    },
}

#[tokio::main]
async fn main() {
    let cli = Cli::parse();

    if let Err(e) = config::init_config(&cli.config_path) {
        eprintln!("Failed to load config: {e}");
        std::process::exit(1);
    }

    let mut paths = AntaresPaths::from_global_config();
    if let Some(p) = cli.upper_root {
        paths.upper_root = p;
    }
    if let Some(p) = cli.cl_root {
        paths.cl_root = p;
    }
    if let Some(p) = cli.mount_root {
        paths.mount_root = p;
    }
    if let Some(p) = cli.state_file {
        paths.state_file = p;
    }

    match cli.command {
        Commands::Mount { job_id, cl } => {
            let manager = AntaresManager::new(paths.clone()).await;
            match manager.mount_job(&job_id, cl.as_deref()).await {
                Ok(instance) => {
                    println!(
                        "mounted job {} at {}",
                        job_id,
                        instance.mountpoint.display()
                    );
                }
                Err(err) => {
                    eprintln!("failed to mount job {}: {}", job_id, err);
                    std::process::exit(1);
                }
            }
        }
        Commands::Umount { job_id } => {
            let manager = AntaresManager::new(paths.clone()).await;
            match manager.umount_job(&job_id).await {
                Ok(Some(_)) => println!("unmounted job {}", job_id),
                Ok(None) => {
                    eprintln!("job {} not found", job_id);
                    std::process::exit(1);
                }
                Err(err) => {
                    eprintln!("failed to unmount job {}: {}", job_id, err);
                    std::process::exit(1);
                }
            }
        }
        Commands::List => {
            let manager = AntaresManager::new(paths.clone()).await;
            let items = manager.list().await;
            if items.is_empty() {
                println!("no active jobs");
            } else {
                for it in items {
                    let cl = it
                        .cl_dir
                        .as_ref()
                        .map(|p| p.display().to_string())
                        .unwrap_or_else(|| "(none)".to_string());
                    println!(
                        "job_id={} mount={} upper={} cl={}",
                        it.job_id,
                        it.mountpoint.display(),
                        it.upper_dir.display(),
                        cl
                    );
                }
            }
        }
        Commands::Serve { bind } => {
            // Initialize tracing for daemon mode
            tracing_subscriber::fmt()
                .with_env_filter(
                    tracing_subscriber::EnvFilter::from_default_env()
                        .add_directive("scorpio=info".parse().unwrap()),
                )
                .init();

            let addr: SocketAddr = bind.parse().unwrap_or_else(|e| {
                eprintln!("Invalid bind address '{}': {}", bind, e);
                std::process::exit(1);
            });

            // Create service with new Dicfuse instance
            let service = Arc::new(AntaresServiceImpl::new(None).await);
            let daemon = AntaresDaemon::new(service);

            tracing::info!("Starting Antares daemon on {}", addr);

            if let Err(e) = daemon.serve(addr).await {
                tracing::error!("Daemon error: {}", e);
                std::process::exit(1);
            }
        }
        Commands::HttpMount {
            job_id,
            path,
            cl,
            endpoint,
        } => {
            let client = Client::new();
            let url = format!("{}/mounts", endpoint.trim_end_matches('/'));
            let payload = serde_json::json!({
                "job_id": job_id,
                "path": path,
                "cl": cl,
            });

            let resp = client
                .post(url)
                .header("content-type", "application/json")
                .json(&payload)
                .send();

            match resp {
                Ok(r) if r.status().is_success() => {
                    let created: serde_json::Value = r.json().unwrap_or_else(|e| {
                        eprintln!("failed to parse response json: {e}");
                        std::process::exit(1);
                    });
                    println!("{}", serde_json::to_string_pretty(&created).unwrap());
                }
                Ok(r) => {
                    let status = r.status();
                    let body = r.text().unwrap_or_default();
                    eprintln!("http mount failed: status={} body={}", status, body);
                    std::process::exit(1);
                }
                Err(e) => {
                    eprintln!("http mount request failed: {}", e);
                    std::process::exit(1);
                }
            }
        }
    }
}