linguo 1.2.0

Cross-platform, multi-language runtime, package, and project manager
pub mod dist;
pub mod project;

use std::path::{Path, PathBuf};

use anyhow::{Context, Result, bail};

use crate::config::Pin;
use crate::store;
use crate::versions::{Version, VersionReq};

pub const LANGUAGE: &str = "node";

pub fn toolchain_path(version: &Version) -> Result<PathBuf> {
    store::toolchain_path(LANGUAGE, version)
}

pub fn upgrade(latest: bool, prune: bool) -> Result<()> {
    let builds = dist::fetch_available()?;
    let available: Vec<Version> = builds.iter().map(|b| b.version).collect();
    // --latest targets the newest LTS line, matching the install default.
    let newest = builds
        .iter()
        .rev()
        .find(|b| b.lts.is_some())
        .or(builds.last())
        .map(|b| b.version);
    store::upgrade(LANGUAGE, &available, newest, latest, prune, &|v| {
        install(Some(v.to_string()))
    })
}

/// nvm/nodenv convention: the nearest `.nvmrc` or `.node-version` holding a
/// plain version (optionally `v`-prefixed). Aliases like `lts/*` or `node`
/// can't map to a pinned install and count as no pin.
pub fn fallback_pin(cwd: &Path) -> Result<Option<Pin>> {
    for dir in cwd.ancestors() {
        for name in [".nvmrc", ".node-version"] {
            let path = dir.join(name);
            if let Some(raw) = store::read_version_file(&path)? {
                let version = raw.strip_prefix('v').unwrap_or(&raw);
                return Ok(store::file_pin(version, &path));
            }
        }
    }
    Ok(None)
}

pub fn install(request: Option<String>) -> Result<()> {
    let builds = dist::fetch_available()?;
    if builds.is_empty() {
        bail!("no builds available for this platform");
    }

    let build = match &request {
        Some(raw) => {
            let req: VersionReq = raw.parse()?;
            builds
                .iter()
                .rev()
                .find(|b| req.matches(&b.version))
                .with_context(|| format!("no available build matches '{raw}'"))?
        }
        None => builds
            .iter()
            .rev()
            .find(|b| b.lts.is_some())
            .unwrap_or_else(|| builds.last().unwrap()),
    };

    let dest = toolchain_path(&build.version)?;
    if dest.exists() {
        eprintln!("node {} is already installed", build.version);
        return Ok(());
    }
    std::fs::create_dir_all(dest.parent().unwrap())
        .with_context(|| format!("failed to create {}", dest.parent().unwrap().display()))?;

    dist::install_build(&build.version, &dest)?;
    eprintln!("installed node {} to {}", build.version, dest.display());
    Ok(())
}

pub fn list(available: bool) -> Result<()> {
    if !available {
        return store::list_installed(LANGUAGE);
    }
    // The full index is hundreds of versions; show the latest of each major.
    let builds = dist::fetch_available()?;
    if builds.is_empty() {
        println!("no builds available for this platform");
        return Ok(());
    }
    let installed = store::installed_versions(LANGUAGE)?;
    let mut previous: Option<&dist::AvailableBuild> = None;
    let mut latest_per_major: Vec<&dist::AvailableBuild> = Vec::new();
    for build in &builds {
        if let Some(prev) = previous
            && prev.version.major != build.version.major
        {
            latest_per_major.push(prev);
        }
        previous = Some(build);
    }
    latest_per_major.extend(previous);
    for build in latest_per_major {
        let lts = build
            .lts
            .as_ref()
            .map(|name| format!(" (lts: {name})"))
            .unwrap_or_default();
        let marker = if installed.contains(&build.version) {
            " (installed)"
        } else {
            ""
        };
        println!("{}{lts}{marker}", build.version);
    }
    println!("(latest release per major line; any exact version can be installed)");
    Ok(())
}