nsg-cli 0.1.3

CLI tool for the Neuroscience Gateway (NSG) BRAIN Initiative API
Documentation
use crate::client::NsgClient;
use crate::config::Credentials;
use anyhow::Result;
use clap::Args;
use colored::Colorize;

#[derive(Debug, Args)]
pub struct ListCommand {
    #[arg(long, help = "Show job messages for each job")]
    detailed: bool,

    #[arg(short, long, help = "Limit number of jobs to display")]
    limit: Option<usize>,

    #[arg(
        long,
        default_value = "20",
        help = "Show only the N most recent jobs (default: 20, use --recent 0 to show all)"
    )]
    recent: usize,

    #[arg(long, help = "Show all jobs (override default limit)")]
    all: bool,
}

impl ListCommand {
    pub fn execute(self) -> Result<()> {
        let credentials = Credentials::load()?;
        let client = NsgClient::new(credentials.clone())?;

        println!("{}", "NSG Job List".bold().cyan());
        println!("{}", "=".repeat(80).cyan());
        println!();
        println!(
            "{} Fetching jobs for user: {}",
            "".cyan(),
            credentials.username.bold()
        );
        println!();

        let mut jobs = client.list_jobs()?;

        if jobs.is_empty() {
            println!("{}", "No jobs found".yellow());
            println!();
            println!("You can submit a test job with:");
            println!("  {}", "nsg submit <zip_file> --tool PY_EXPANSE".cyan());
            return Ok(());
        }

        let total_jobs = jobs.len();

        // Apply limit/recent filters
        if self.all {
            // Show all jobs, no filtering
        } else if let Some(limit) = self.limit {
            // Explicit limit takes precedence
            jobs.truncate(limit);
        } else if self.recent > 0 && jobs.len() > self.recent {
            // Default: show N most recent jobs
            jobs.drain(0..jobs.len() - self.recent);
        }

        let showing_jobs = jobs.len();

        if showing_jobs < total_jobs {
            println!(
                "Found {} job(s) total, showing {}",
                total_jobs.to_string().bold(),
                showing_jobs.to_string().bold()
            );
        } else {
            println!("Found {} job(s)", jobs.len().to_string().bold());
        }
        println!();
        println!("{}", "=".repeat(80));

        for (i, job) in jobs.iter().enumerate() {
            println!();
            println!("Job #{}", (i + 1).to_string().bold());
            println!("  ID:        {}", job.job_id.cyan());

            // Display tool name - always show this field
            match &job.tool {
                Some(tool) => println!("  Tool:      {}", tool.yellow()),
                None => println!("  Tool:      {}", "N/A".dimmed()),
            }

            // Display job stage - always show this field
            match &job.job_stage {
                Some(stage) => {
                    let stage_icon = get_stage_icon(stage);
                    println!("  Status:    {} {}", stage_icon, stage.bold());
                }
                None => println!("  Status:    {}", "Unknown".dimmed()),
            }

            // Display failed status - always show this field
            if job.failed {
                println!("  Failed:    {} YES", "".red().bold());
            } else {
                println!("  Failed:    {} No", "".green());
            }

            // Display submission date - always show this field
            match &job.date_submitted {
                Some(date) => println!("  Submitted: {}", format_timestamp(date)),
                None => println!("  Submitted: {}", "N/A".dimmed()),
            }

            // Display completion date - always show this field
            match &job.date_completed {
                Some(date) => println!("  Completed: {}", format_timestamp(date).green()),
                None => println!("  Completed: {}", "Not completed".dimmed()),
            }

            // The --detailed flag now shows job messages
            if self.detailed {
                match client.get_job_status(&job.url) {
                    Ok(status) => {
                        if !status.messages.is_empty() {
                            println!("  Messages:");
                            for msg in &status.messages {
                                println!("    [{}] {}", msg.stage.cyan(), truncate(&msg.text, 80));
                            }
                        }
                    }
                    Err(_) => {
                        println!("  Messages:  {} (failed to fetch)", "?".yellow());
                    }
                }
            }

            println!("  URL:       {}", job.url.dimmed());
            println!("{}", "=".repeat(80));
        }

        println!();
        println!("{}", "Commands:".bold());
        println!("  Check job status:    {}", "nsg status <JOB_ID>".cyan());
        println!("  Download results:    {}", "nsg download <JOB_ID>".cyan());

        if showing_jobs < total_jobs {
            println!();
            println!("{}", "Tip:".bold());
            println!("  Use {} to see all {} jobs", "--all".cyan(), total_jobs);
            println!("  Use {} to see job messages", "--detailed".cyan());
            println!("  Use {} to limit results", "--limit N".cyan());
            println!("  Use {} to show N most recent jobs", "--recent N".cyan());
        }
        println!();

        Ok(())
    }
}

fn get_stage_icon(stage: &str) -> String {
    match stage {
        "COMPLETED" => "".green().bold().to_string(),
        "RUNNING" | "RUN" => "".yellow().bold().to_string(),
        "QUEUE" | "SUBMITTED" => "".cyan().to_string(),
        "FAILED" => "".red().bold().to_string(),
        _ => "?".dimmed().to_string(),
    }
}

fn format_timestamp(ts: &str) -> String {
    use chrono::{DateTime, Utc};
    if let Ok(dt) = ts.parse::<DateTime<Utc>>() {
        dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
    } else {
        ts.to_string()
    }
}

fn truncate(s: &str, max_len: usize) -> String {
    if s.len() <= max_len {
        s.to_string()
    } else {
        format!("{}...", &s[..max_len])
    }
}