use std::collections::HashMap;
use anyhow::Result;
use chrono::{TimeDelta, Utc};
use log::debug;
use rustc_hash::FxHashSet;
use lade_sdk::compat::{self, CompatWarning, spec_for};
use crate::context::InvocationContext;
use crate::global_config::GlobalConfig;
use crate::message_box::MessageBox;
pub fn known_schemes<'a>(uris: impl Iterator<Item = &'a str>) -> Vec<String> {
uris.filter_map(|uri| uri.split_once("://").map(|(scheme, _)| scheme))
.filter(|scheme| spec_for(scheme).is_some())
.map(|scheme| scheme.to_string())
.collect::<FxHashSet<_>>()
.into_iter()
.collect()
}
pub fn all_supported_schemes() -> Vec<String> {
compat::CLI_SPECS
.iter()
.map(|spec| spec.scheme.to_string())
.collect()
}
async fn check_message(schemes: Vec<String>) -> Result<(Vec<CompatWarning>, Vec<String>)> {
if schemes.is_empty() {
return Ok((vec![], vec![]));
}
let config = GlobalConfig::load().await?;
let now = Utc::now();
let day = TimeDelta::try_days(1).unwrap();
let due: Vec<String> = schemes
.into_iter()
.filter(|scheme| match config.cli_check.get(scheme) {
Some(last) => *last + day < now,
None => true,
})
.collect();
if due.is_empty() {
return Ok((vec![], vec![]));
}
let warnings = compat::check(&due, &HashMap::new()).await;
if warnings.is_empty() {
GlobalConfig::update(|c| {
for scheme in &due {
c.cli_check.insert(scheme.clone(), now);
}
})
.await?;
}
Ok((warnings, due))
}
pub async fn warn_outdated(_ctx: &InvocationContext, schemes: Vec<String>) {
match check_message(schemes).await {
Ok((warnings, due)) if !warnings.is_empty() => {
render(&warnings);
let now = Utc::now();
GlobalConfig::update(|c| {
for scheme in &due {
c.cli_check.insert(scheme.clone(), now);
}
})
.await
.ok();
}
Ok(_) => {}
Err(e) => debug!("CLI compatibility check failed: {e}"),
}
}
pub async fn check_schemes(schemes: Vec<String>) -> Result<Vec<CompatWarning>> {
Ok(check_message(schemes).await?.0)
}
fn render(warnings: &[CompatWarning]) {
let mut box_ = MessageBox::new()
.warning()
.line("Some secret CLIs are older than the version Lade is tested against:");
for w in warnings {
box_ = box_.paragraph(format!(
"{} {} is below the supported {}. Update it if you hit issues: {}",
w.name, w.found, w.min, w.install_url
));
}
box_.line("Run `lade status` for details.").print_stderr();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_known_schemes_filters_and_dedupes() {
let uris = [
"op://my.1password.com/v/i/f",
"op://my.1password.com/v/i/g",
"vault://localhost/secret/app/pass",
"plainvalue",
"unknown://host/path",
];
let mut schemes = known_schemes(uris.iter().copied());
schemes.sort();
assert_eq!(schemes, vec!["op".to_string(), "vault".to_string()]);
}
#[test]
fn test_known_schemes_empty() {
assert!(known_schemes(["plain".to_string()].iter().map(|s| s.as_str())).is_empty());
}
#[test]
fn test_all_supported_schemes() {
assert!(all_supported_schemes().contains(&"op".to_string()));
}
}