nap_core/server/
version.rs1use anyhow::{Context, Result};
9use semver::Version;
10use std::process::Command;
11
12pub const PINNED_LORE_VERSION: &str = "0.8.5-nightly";
14
15pub fn detect_lore_version() -> Result<Version> {
17 let output = Command::new("lore").arg("--version").output().context(
18 "Failed to execute 'lore --version'. \
19 Lore CLI is not installed or not on PATH. \
20 Install it with: nap install lore",
21 )?;
22
23 if !output.status.success() {
24 let stderr = String::from_utf8_lossy(&output.stderr);
25 anyhow::bail!(
26 "lore --version exited with status: {}. stderr: {}",
27 output.status,
28 stderr.trim()
29 );
30 }
31
32 let version_str = String::from_utf8_lossy(&output.stdout);
33 parse_lore_version(&version_str)
34}
35
36pub fn detect_loreserver_version() -> Result<Version> {
38 let output = Command::new("loreserver")
39 .arg("--version")
40 .output()
41 .context(
42 "Failed to execute 'loreserver --version'. \
43 Lore server is not installed or not on PATH. \
44 Install it with: nap install lore",
45 )?;
46
47 if !output.status.success() {
48 let stderr = String::from_utf8_lossy(&output.stderr);
49 anyhow::bail!(
50 "loreserver --version exited with status: {}. stderr: {}",
51 output.status,
52 stderr.trim()
53 );
54 }
55
56 let version_str = String::from_utf8_lossy(&output.stdout);
57 parse_lore_version(&version_str)
58}
59
60fn parse_lore_version(version_str: &str) -> Result<Version> {
62 let version_part = version_str.split_whitespace().nth(1).context(format!(
64 "Failed to parse Lore version string '{}'. \
65 Expected format: 'lore <version>' (e.g., 'lore 0.8.5-nightly')",
66 version_str.trim()
67 ))?;
68
69 let version_for_semver = version_part.trim_end_matches("-nightly");
71
72 Version::parse(version_for_semver).context(format!(
73 "Failed to parse '{}' as semver version. \
74 Lore version string may be in an unexpected format.",
75 version_for_semver
76 ))
77}
78
79pub fn check_lore_compatibility(installed_version: &Version) -> Result<bool> {
81 let pinned = Version::parse(PINNED_LORE_VERSION.trim_end_matches("-nightly"))
82 .context("Failed to parse pinned Lore version")?;
83
84 let installed_clean = Version::new(
87 installed_version.major,
88 installed_version.minor,
89 installed_version.patch,
90 );
91
92 let pinned_clean = Version::new(pinned.major, pinned.minor, pinned.patch);
93
94 Ok(installed_clean == pinned_clean)
95}
96
97pub fn verify_lore_installation() -> Result<LoreInstallationStatus> {
99 let cli_version = match detect_lore_version() {
100 Ok(v) => Some(v),
101 Err(e) => {
102 tracing::warn!("Failed to detect Lore CLI version: {}", e);
103 None
104 }
105 };
106
107 let server_version = match detect_loreserver_version() {
108 Ok(v) => Some(v),
109 Err(e) => {
110 tracing::warn!("Failed to detect Lore server version: {}", e);
111 None
112 }
113 };
114
115 let cli_compatible = cli_version
116 .as_ref()
117 .map(|v| check_lore_compatibility(v).unwrap_or(false))
118 .unwrap_or(false);
119
120 let server_compatible = server_version
121 .as_ref()
122 .map(|v| check_lore_compatibility(v).unwrap_or(false))
123 .unwrap_or(false);
124
125 Ok(LoreInstallationStatus {
126 cli_installed: cli_version.is_some(),
127 cli_version,
128 cli_compatible,
129 server_installed: server_version.is_some(),
130 server_version,
131 server_compatible,
132 pinned_version: PINNED_LORE_VERSION.to_string(),
133 })
134}
135
136#[derive(Debug, Clone)]
138pub struct LoreInstallationStatus {
139 pub cli_installed: bool,
140 pub cli_version: Option<Version>,
141 pub cli_compatible: bool,
142 pub server_installed: bool,
143 pub server_version: Option<Version>,
144 pub server_compatible: bool,
145 pub pinned_version: String,
146}
147
148impl LoreInstallationStatus {
149 pub fn is_fully_compatible(&self) -> bool {
151 self.cli_installed && self.cli_compatible && self.server_installed && self.server_compatible
152 }
153
154 pub fn status_message(&self) -> String {
156 let mut messages = vec![];
157
158 if !self.cli_installed {
159 messages.push("Lore CLI is not installed".to_string());
160 } else if !self.cli_compatible {
161 messages.push(format!(
162 "Lore CLI version {:?} is incompatible with pinned version {}",
163 self.cli_version, self.pinned_version
164 ));
165 }
166
167 if !self.server_installed {
168 messages.push("Lore server is not installed".to_string());
169 } else if !self.server_compatible {
170 messages.push(format!(
171 "Lore server version {:?} is incompatible with pinned version {}",
172 self.server_version, self.pinned_version
173 ));
174 }
175
176 if messages.is_empty() {
177 "Lore installation is compatible".to_string()
178 } else {
179 messages.join("; ")
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn test_parse_lore_version() {
190 let version_str = "lore 0.8.5-nightly";
191 let version = parse_lore_version(version_str).unwrap();
192 assert_eq!(version.major, 0);
193 assert_eq!(version.minor, 8);
194 assert_eq!(version.patch, 5);
195 }
196
197 #[test]
198 fn test_parse_loreserver_version() {
199 let version_str = "loreserver 0.8.5-nightly";
200 let version = parse_lore_version(version_str).unwrap();
201 assert_eq!(version.major, 0);
202 assert_eq!(version.minor, 8);
203 assert_eq!(version.patch, 5);
204 }
205
206 #[test]
207 fn test_compatibility_check() {
208 let installed = Version::new(0, 8, 5);
209 assert!(check_lore_compatibility(&installed).unwrap());
210
211 let incompatible = Version::new(0, 7, 0);
212 assert!(!check_lore_compatibility(&incompatible).unwrap());
213 }
214
215 #[test]
216 fn test_installation_status_message() {
217 let status = LoreInstallationStatus {
218 cli_installed: false,
219 cli_version: None,
220 cli_compatible: false,
221 server_installed: false,
222 server_version: None,
223 server_compatible: false,
224 pinned_version: "0.8.5-nightly".to_string(),
225 };
226
227 let message = status.status_message();
228 assert!(message.contains("Lore CLI is not installed"));
229 assert!(message.contains("Lore server is not installed"));
230 }
231}