use anyhow::{Context, Result};
use semver::Version;
use std::process::Command;
pub const PINNED_LORE_VERSION: &str = "0.8.4-portals.9";
pub const PINNED_LORE_REPOSITORY: &str = "portalshq/lore";
pub const PINNED_LORE_INSTALLER_SHA256: &str =
"8e7cc96d1b9100610af6c1bd15ec2febbcb48d26cc7f19de3862496897810b74";
pub const PINNED_LORE_ARTIFACT_MANIFEST_SHA256: &str =
"sha256:dc853999309ec32e2c91acb94f7ef2aad4f080ac4a0f7591d9371d5cd57eb089";
pub const PINNED_LORE_ARTIFACT_MANIFEST_URL: &str =
"https://github.com/portalshq/lore/releases/download/v0.8.4-portals.9/SHA256SUMS";
pub const PINNED_LORE_SIGNATURE_BUNDLE_URL: &str =
"https://github.com/portalshq/lore/releases/download/v0.8.4-portals.9/SHA256SUMS.sigstore.json";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoreVersionInfo {
pub parsed: Version,
pub raw: String,
}
impl std::fmt::Display for LoreVersionInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.raw)
}
}
fn extract_version_string(version_str: &str) -> Result<String> {
let version_part = version_str.split_whitespace().nth(1).context(format!(
"Failed to parse Lore version string '{}'. \
Expected format: 'lore <version>' (e.g., 'lore 0.8.4')",
version_str.trim()
))?;
Ok(version_part.trim().to_string())
}
pub fn detect_lore_version() -> Result<LoreVersionInfo> {
let output = Command::new("lore").arg("--version").output().context(
"Failed to execute 'lore --version'. \
Lore CLI is not installed or not on PATH. \
Install it with: nap install lore",
)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"lore --version exited with status: {}. stderr: {}",
output.status,
stderr.trim()
);
}
let version_str = String::from_utf8_lossy(&output.stdout);
let raw = extract_version_string(&version_str)?;
let parsed = parse_lore_version(&version_str)?;
Ok(LoreVersionInfo { parsed, raw })
}
pub fn detect_loreserver_version() -> Result<LoreVersionInfo> {
let output = Command::new("loreserver")
.arg("--version")
.output()
.context(
"Failed to execute 'loreserver --version'. \
Lore server is not installed or not on PATH. \
Install it with: nap install lore",
)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"loreserver --version exited with status: {}. stderr: {}",
output.status,
stderr.trim()
);
}
let version_str = String::from_utf8_lossy(&output.stdout);
let raw = extract_version_string(&version_str)?;
let parsed = parse_lore_version(&version_str)?;
Ok(LoreVersionInfo { parsed, raw })
}
fn parse_lore_version(version_str: &str) -> Result<Version> {
let version_part = version_str.split_whitespace().nth(1).context(format!(
"Failed to parse Lore version string '{}'. \
Expected format: 'lore <version>' (e.g., 'lore 0.8.4')",
version_str.trim()
))?;
let version_for_semver = version_part.trim_end_matches("-nightly");
Version::parse(version_for_semver).context(format!(
"Failed to parse '{}' as semver version. \
Lore version string may be in an unexpected format.",
version_for_semver
))
}
pub fn check_lore_compatibility(installed: &LoreVersionInfo) -> Result<bool> {
let installed_version = installed.raw.split('+').next().unwrap_or(&installed.raw);
Ok(installed_version == PINNED_LORE_VERSION)
}
pub fn verify_lore_installation() -> Result<LoreInstallationStatus> {
let cli_version = match detect_lore_version() {
Ok(v) => Some(v),
Err(e) => {
tracing::debug!("Lore CLI not detected: {}", e);
None
}
};
let server_version = match detect_loreserver_version() {
Ok(v) => Some(v),
Err(e) => {
tracing::debug!("Lore server not detected: {}", e);
None
}
};
let cli_compatible = cli_version
.as_ref()
.map(|v| check_lore_compatibility(v).unwrap_or(false))
.unwrap_or(false);
let server_compatible = server_version
.as_ref()
.map(|v| check_lore_compatibility(v).unwrap_or(false))
.unwrap_or(false);
Ok(LoreInstallationStatus {
cli_installed: cli_version.is_some(),
cli_version,
cli_compatible,
server_installed: server_version.is_some(),
server_version,
server_compatible,
pinned_version: PINNED_LORE_VERSION.to_string(),
})
}
#[derive(Debug, Clone)]
pub struct LoreInstallationStatus {
pub cli_installed: bool,
pub cli_version: Option<LoreVersionInfo>,
pub cli_compatible: bool,
pub server_installed: bool,
pub server_version: Option<LoreVersionInfo>,
pub server_compatible: bool,
pub pinned_version: String,
}
impl LoreInstallationStatus {
pub fn is_fully_compatible(&self) -> bool {
self.cli_installed && self.cli_compatible && self.server_installed && self.server_compatible
}
pub fn status_message(&self) -> String {
let mut messages = vec![];
if !self.cli_installed {
messages.push("Lore CLI is not installed".to_string());
} else if !self.cli_compatible {
messages.push(format!(
"Lore CLI version '{}' is incompatible with required version '{}'",
self.cli_version
.as_ref()
.map(|v| v.raw.as_str())
.unwrap_or("unknown"),
self.pinned_version
));
}
if !self.server_installed {
messages.push("Lore server is not installed".to_string());
} else if !self.server_compatible {
messages.push(format!(
"Lore server version '{}' is incompatible with required version '{}'",
self.server_version
.as_ref()
.map(|v| v.raw.as_str())
.unwrap_or("unknown"),
self.pinned_version
));
}
if messages.is_empty() {
"Lore installation is compatible".to_string()
} else {
messages.join("; ")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_version_string() {
assert_eq!(extract_version_string("lore 0.8.4").unwrap(), "0.8.4");
assert_eq!(extract_version_string("loreserver 0.8.4").unwrap(), "0.8.4");
assert_eq!(extract_version_string("lore 0.8.4\n").unwrap(), "0.8.4");
}
#[test]
fn test_extract_version_string_failure() {
assert!(extract_version_string("lore").is_err());
assert!(extract_version_string("").is_err());
}
#[test]
fn test_parse_lore_version() {
let version_str = "lore 0.8.4";
let version = parse_lore_version(version_str).unwrap();
assert_eq!(version.major, 0);
assert_eq!(version.minor, 8);
assert_eq!(version.patch, 4);
}
#[test]
fn test_parse_lore_version_with_nightly_suffix() {
let version_str = "lore 0.8.4-nightly";
let version = parse_lore_version(version_str).unwrap();
assert_eq!(version.major, 0);
assert_eq!(version.minor, 8);
assert_eq!(version.patch, 4);
}
#[test]
fn test_parse_loreserver_version() {
let version_str = "loreserver 0.8.4";
let version = parse_lore_version(version_str).unwrap();
assert_eq!(version.major, 0);
assert_eq!(version.minor, 8);
assert_eq!(version.patch, 4);
}
#[test]
fn test_compatibility_exact_match() {
let installed = LoreVersionInfo {
parsed: Version::new(0, 8, 4),
raw: PINNED_LORE_VERSION.to_string(),
};
assert!(check_lore_compatibility(&installed).unwrap());
}
#[test]
fn test_compatibility_ignores_build_metadata() {
let installed = LoreVersionInfo {
parsed: Version::new(0, 8, 4),
raw: format!("{}+283", PINNED_LORE_VERSION),
};
assert!(check_lore_compatibility(&installed).unwrap());
}
#[test]
fn test_compatibility_rejects_nightly_suffix() {
let installed = LoreVersionInfo {
parsed: Version::new(0, 8, 4),
raw: "0.8.4-nightly".to_string(),
};
assert!(!check_lore_compatibility(&installed).unwrap());
}
#[test]
fn test_compatibility_rejects_wrong_channel() {
let installed = LoreVersionInfo {
parsed: Version::new(0, 8, 4),
raw: "0.8.4-stable".to_string(),
};
assert!(!check_lore_compatibility(&installed).unwrap());
}
#[test]
fn test_compatibility_rejects_wrong_version() {
let installed = LoreVersionInfo {
parsed: Version::new(0, 7, 0),
raw: "0.7.0".to_string(),
};
assert!(!check_lore_compatibility(&installed).unwrap());
}
#[test]
fn test_installation_status_message() {
let status = LoreInstallationStatus {
cli_installed: false,
cli_version: None,
cli_compatible: false,
server_installed: false,
server_version: None,
server_compatible: false,
pinned_version: PINNED_LORE_VERSION.to_string(),
};
let message = status.status_message();
assert!(message.contains("Lore CLI is not installed"));
assert!(message.contains("Lore server is not installed"));
}
#[test]
fn test_installation_status_message_incompatible() {
let status = LoreInstallationStatus {
cli_installed: true,
cli_version: Some(LoreVersionInfo {
parsed: Version::new(0, 8, 4),
raw: "0.8.4-nightly".to_string(),
}),
cli_compatible: false,
server_installed: true,
server_version: Some(LoreVersionInfo {
parsed: Version::new(0, 8, 4),
raw: "0.8.4-nightly".to_string(),
}),
server_compatible: false,
pinned_version: PINNED_LORE_VERSION.to_string(),
};
let message = status.status_message();
assert!(message.contains("'0.8.4-nightly'"));
assert!(message.contains(&format!("'{}'", PINNED_LORE_VERSION)));
assert!(!status.is_fully_compatible());
}
#[test]
fn test_pinned_version_constant() {
assert_eq!(PINNED_LORE_VERSION, "0.8.4-portals.9");
}
}