use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use crate::config::{self, PinSource};
use crate::versions::{Version, VersionReq};
pub fn tool_dir(language: &str, tool: &str) -> Result<PathBuf> {
Ok(config::linguo_root()?
.join("tools")
.join(language)
.join(tool))
}
pub fn version_dir(language: &str, tool: &str, version: &Version) -> Result<PathBuf> {
Ok(tool_dir(language, tool)?.join(version.to_string()))
}
pub fn exposed_dir(language: &str, tool: &str, version: &Version) -> Result<PathBuf> {
Ok(version_dir(language, tool, version)?.join("exposed"))
}
pub fn installed_versions(language: &str, tool: &str) -> Result<Vec<Version>> {
let dir = tool_dir(language, tool)?;
let entries = match std::fs::read_dir(&dir) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(err).with_context(|| format!("failed to read {}", dir.display())),
};
let mut versions: Vec<Version> = entries
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().is_dir())
.filter_map(|entry| entry.file_name().to_str().and_then(|s| s.parse().ok()))
.collect();
versions.sort();
Ok(versions)
}
pub fn find_installed(language: &str, tool: &str, req: &VersionReq) -> Result<Option<Version>> {
Ok(req.best_match(installed_versions(language, tool)?))
}
pub fn installed_tools(language: &str) -> Result<Vec<String>> {
let dir = config::linguo_root()?.join("tools").join(language);
let entries = match std::fs::read_dir(&dir) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(err).with_context(|| format!("failed to read {}", dir.display())),
};
let mut tools: Vec<String> = entries
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().is_dir())
.filter_map(|entry| entry.file_name().to_str().map(str::to_string))
.collect();
tools.sort();
Ok(tools)
}
pub fn active_exposed_dirs(language: &str, cwd: &Path) -> Result<Vec<PathBuf>> {
let mut dirs = Vec::new();
for pin in config::tool_pins(language, cwd)? {
let Ok(req) = pin.raw.parse::<VersionReq>() else {
continue;
};
if let Some(version) = find_installed(language, &pin.tool, &req)? {
let exposed = exposed_dir(language, &pin.tool, &version)?;
if exposed.is_dir() {
dirs.push(exposed);
}
}
}
Ok(dirs)
}
pub fn list(language: &str) -> Result<()> {
let tools = installed_tools(language)?;
if tools.is_empty() {
println!("no {language} tools installed (try `linguo {language} tool install <name>`)");
return Ok(());
}
let cwd = std::env::current_dir()?;
let pins = config::tool_pins(language, &cwd)?;
for tool in tools {
let pin = pins.iter().find(|p| p.tool == tool);
let active = match pin {
Some(p) => p
.raw
.parse::<VersionReq>()
.ok()
.and_then(|req| find_installed(language, &tool, &req).ok().flatten()),
None => None,
};
for version in installed_versions(language, &tool)? {
let marker = if active == Some(version) {
let source = match &pin.unwrap().source {
PinSource::Project(path) => format!("pinned by {}", path.display()),
PinSource::Global => "global default".to_string(),
};
format!(" * ({source})")
} else {
String::new()
};
println!("{tool} {version}{marker}");
}
}
Ok(())
}
pub fn uninstall(language: &str, tool: &str) -> Result<()> {
let dir = tool_dir(language, tool)?;
if !dir.exists() {
bail!("{language} tool '{tool}' is not installed");
}
std::fs::remove_dir_all(&dir).with_context(|| format!("failed to remove {}", dir.display()))?;
let global = config::linguo_root()?.join(config::GLOBAL_CONFIG);
config::remove_tool_pin(&global, language, tool)?;
println!("uninstalled {language} tool '{tool}'");
Ok(())
}